js字符串最多存储多少字节?
V8的heap上限只有2GB不到,允许分配的单个字符串大小上限更只有大约是512MB不到。JS字符串是UTF16编码保存,所以也就是2.68亿个字符。FF大约也是这个数字。
https://www.zhihu.com/question/61105131
JavaScript字符串底层是如何实现的?
作者:RednaxelaFX
链接:https://www.zhihu.com/question/51132164/answer/124450796
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
目前主流的做法是把String值的实现分为5大类使用场景:
- 已经要查看内容的字符串:使用flat string思路来实现,本质上说就是用数组形式来存储String的内容;
- 拼接字符串但尚未查看其内容:使用“rope”思路或其它延迟拼接的思路来实现。当需要查看其内容时则进行“flatten”操作将其转换为flat string表现形式。最常见rope的内部节点就像二叉树(RopeNode { Left; Right })一样,但也可以有采用更多叉树的设计的节点,或者是用更动态的多叉树实现;
- 子串(substring):使用“slice”思路来实现,也就是说它只是一个view,自己并不存储字符内容而只是记录个offset和length,底下的存储共享自其引用的源字符串;
- 值得驻留(intern)的字符串:通常也是flat string但可能会有更多的限制,存储它的空间可能也跟普通String不一样。最大的好处是在特殊场景下有些字符串会经常重复出现,或者要经常用于相等性比较,把这些字符串驻留起来可以节省内存(内容相同的字符串只驻留一份),并且后续使用可以使用指针比较来代替完全的相等性比较(因为驻留的时候已经比较过了);
- 外来字符串:有时候JavaScript引擎跟外界交互,外界想直接把一个char8_t或者char16_t传给JavaScript引擎当作JavaScript字符串用。JavaScript引擎可能会针对某些特殊场景提供一种包装方式来直接把这些外部传进来的字符串当作JavaScript String,而不拷贝其内容。
在上述5种场景中,涉及存储的时候都可以有
- 使用UTF-16为单元的最常规做法以及使用Latin-1 / ASCII的压缩版本这两种变种。
- 对于flat string,直接把字符串内容粘在对象末尾的“内嵌版”,以及把字符串内容放在独立的数组里的“独立数组版”两个变种。
如果把语言层面的一个String值类型按上述使用场景给拆分成若干种不同的底层实现类型,本质上都是在为内存而优化:要么是减少String的内存使用量(1-byte vs 2-byte、substring等),要么是减少拷贝的次数/长度(rope的按需flatten)。
底层实现类型的数量的增多,会使得相关处理的代码都变得多态,不利于编译器对其做优化,所以这里是有取舍的。如果多态换来的内存收益比不上多态的代码开销的话就得不偿失了。显然,众多JavaScript引擎都选择了在String值类型上细分出多种实现类型,反映了多态在这个地方总体来看是有利的。
把上面的场景(1)、(2)、(3)用代码来举例:
var s1 = "rednaxela"; // flat string, string literal
var s2 = "fx"; // flat string, string literal
var s3 = s1 + s2; // rope ("concat string", "cons string")
var s4 = s3.substring(0, 3); // substring / slice
// 这个操作可能会让s3所引用的String值被flatten为flat string
// 同理,如果执行 s3[0] 下标操作也可能会让原本是rope的String值被flatten
在有用rope来优化字符串拼接的JavaScript引擎上,使用二元+运算符来拼接字符串其实不会直接导致冗余的字符串内容拷贝,只有在需要使用字符串的内容时才会对它做一次批量的flatten操作,做一次拷贝。所以字符串拼接“要用Array.prototype.join()而忌讳用+运算符”的建议就不那么重要了。
=========================================
V8
于是让我们来考察一下V8的String都有上述场景的哪些。
针对5.5.339版本来看:
v8/objects.h at 5.5.339 · v8/v8 · GitHub
// - Name
// - String
// - SeqString
// - SeqOneByteString
// - SeqTwoByteString
// - SlicedString
// - ConsString
// - ExternalString
// - ExternalOneByteString
// - ExternalTwoByteString
// - InternalizedString
// - SeqInternalizedString
// - SeqOneByteInternalizedString
// - SeqTwoByteInternalizedString
// - ConsInternalizedString
// - ExternalInternalizedString
// - ExternalOneByteInternalizedString
// - ExternalTwoByteInternalizedString
// - Symbol
V8里能表示字符串的C++类型有上面这么多种。其中Name是String(ES String Value)与Symbol(ES6 Symbol)的基类。看看String类下面的子类是多么的丰富 >_<
简单说,String的子类都是用于实现ECMAScript的String值类型,从JavaScript层面看它们都是同一个类型——String,也就是说typeof()它们都会得到"string"。
其中:
SeqString就是上面的场景(1)(“flat string”)的实现。其中有SeqOneByteString / SeqTwoByteString分别对应使用1-byte ASCII char与2-byte UTF-16的版本。字符串内容都是直接粘在对象末尾的(“内嵌版”)。
ConsString就是上面的场景(2)(“rope”)的实现。本质上就是把还在拼接中的字符串用二叉树(其实是二叉DAG)的方式先存着,直到要查看其内容时再flatten成SeqString。它自身不存储字符内容所以不关心1-byte还是2-byte。
SlicedString就是上面场景(3)(“slice / substring”)的实现。同上它也不存储字符内容,所以1-byte还是2-byte就看引用的底层String是怎样的。
ExternalString就是上面场景(5)(外部传入的字符串)的实现。这个涉及存储,所以也有1-byte与2-byte两个实际实现。
InternalizedString系列就是上面场景(4)(“interned”)的实现。它的子类跟前面列举的几种类型一一对应。
而String的包装对象类型在V8里则是由StringWrapper来实现:
bool HeapObject::IsStringWrapper() const {
return IsJSValue() && JSValue::cast(this)->value()->IsString();
}
值得注意的是:虽然ECMAScript的String值是值类型的,这并不就是说“String值就是在栈上的”。
正好相反,V8所实现的String值全部都是在V8的GC堆上存储的,传递String值时实际上传递的是指向它的指针。但由于JavaScript的String值是不可变的,所以底层实现无论是真的把String“放在栈上”还是传递指针,对上层应用的JavaScript代码而言都没有区别。
ExternalString虽然特殊但也不例外:它实际存储字符串内容的空间虽然是从外部传进来的,不在V8的GC堆里,但是ExternalString对象自身作为一个对象头还是在GC堆里的,所以该String类型实现逻辑上说还是在GC堆里。
话说V8除了上述String类型外,还有一些跟String相关的、应用于特殊场景的类型。其中比较典型的有:
ReplacementStringBuilder:用于正则表达式的字符串替换等;
IncrementalStringBuilder:// TODO
这个版本的V8对自己字符串拼接实现已经颇有信心,所以 String.prototype.concat 也直接用JavaScript来实现了:
v8/string.js at 5.5.339 · v8/v8 · GitHub
// ECMA-262, section 15.5.4.6
function StringConcat(other /* and more */) { // length == 1
"use strict";
CHECK_OBJECT_COERCIBLE(this, "String.prototype.concat");
var s = TO_STRING(this);
var len = arguments.length;
for (var i = 0; i < len; ++i) {
s = s + TO_STRING(arguments[i]);
}
return s;
}
这就是直接把传入的参数拼接成ConsString返回出去。
V8连标准库函数都用这种代码模式来实现了,同学们也不用担心这样做会太慢啦。
而V8里的 Array.prototype.join 则针对稀疏数组的情况有些有趣的优化:
它会借助一个临时的InternalArray为“string builder”,计算出拼接结果的length之后直接分配一个合适类型和长度的SeqString作为buffer来进行拼接。而这个InternalArray里的内容可以带有编码为Smi的“下一段要拼接的字符串在什么位置(position)和长度(length)”信息,然后从当前位置到下一个要拼接的位置之间填充分隔符,这样就不会在对稀疏数组的join过程中把数组中无值的位置都填充到“string builder”的实体里去了。这是个run-length encoding的思路。
V8还有个有趣的功能:原地缩小对象而不必为了缩小而拷贝。这个有空再具体展开写。
=========================================
Nashorn
让我们看看JDK8u112-b04里的Nashorn实现。
它比V8要简单一些,实现ECMAScript String值的类型都是java.lang.CharSequence接口的实现类,其中有:
- 场景(1)(“flat string”):直接使用Java原生的 java.lang.String 类型,方便用上JVM对String的优化。在一个JDK/JVM自身就有针对1-byte / 2-byte场景做优化的实现上(例如Oracle JDK9 / OpenJDK9的Compact Strings),Nashorn就会自动获得相应的优化;
- 场景(2)(“rope”):不免俗,有个实现了CharSequence接口的ConsString类型;
- 场景(3)(“slice / substring”):直接用java.lang.String.substring()实现,没有额外优化。Oracle JDK / OpenJDK在JDK7后撤销了java.lang.String的子串共享实现,所以Nashorn里的slice() / substring()在这些JDK上会涉及拷贝开销…orz!
- 场景(4)(“intern”):只有少量地方做了intern,是直接用 java.lang.String.intern() 的。
- 场景(5)(外部传入的字符串):没有特别的对应支持。Nashorn面向的用户是其它JVM上的语言(例如Java),所以外部传入的字符串最可能的也就是 java.lang.String ,正好Nashorn自身的flat string就是直接用 java.lang.String ,所以也就不用做什么额外工作来支持这些外来字符串了。
ECMAScript的String包装对象类型则由这个NativeString类型表示:NativeString,里面就是包装着一个代表String值的CharSequence类型引用。
Nashorn在实现 String.prototype.concat() 时没有特别的实现,是直接把参数拼接成一串ConsString然后直接返回没有flatten的ConsString。
=========================================
SpiderMonkey
这里用FIREFOX_AURORA_51_BASE版代码来考察。
总体来说SpiderMonkey里的String的内部实现思路与V8的非常相似。
代码里的注释把设计思路讲解得很清楚了:
http://hg.mozilla.org/mozilla-central/file/fc69febcbf6c/js/src/vm/String.h
/*
JavaScript strings
Conceptually, a JS string is just an array of chars and a length. This array
of chars may or may not be null-terminated and, if it is, the null character
is not included in the length.
To improve performance of common operations, the following optimizations are
made which affect the engine's representation of strings:
-
- The plain vanilla representation is a "flat" string which consists of a
string header in the GC heap and a malloc'd null terminated char array.
-
- To avoid copying a substring of an existing "base" string , a "dependent"
string (JSDependentString) can be created which points into the base
string's char array.
-
- To avoid O(n^2) char buffer copying, a "rope" node (JSRope) can be created
to represent a delayed string concatenation. Concatenation (called
flattening) is performed if and when a linear char array is requested. In
general, ropes form a binary dag whose internal nodes are JSRope string
headers with no associated char array and whose leaf nodes are either flat
or dependent strings.
-
- To avoid copying the leftmost string when flattening, we may produce an
"extensible" string, which tracks not only its actual length but also its
buffer's overall size. If such an "extensible" string appears as the
leftmost string in a subsequent flatten, and its buffer has enough unused
space, we can simply flatten the rest of the ropes into its buffer,
leaving its text in place. We then transfer ownership of its buffer to the
flattened rope, and mutate the donor extensible string into a dependent
string referencing its original buffer.
(The term "extensible" does not imply that we ever 'realloc' the buffer.
Extensible strings may have dependent strings pointing into them, and the
JSAPI hands out pointers to flat strings' buffers, so resizing with
'realloc' is generally not possible.)
-
- To avoid allocating small char arrays, short strings can be stored inline
in the string header (JSInlineString). These come in two flavours:
JSThinInlineString, which is the same size as JSString; and
JSFatInlineString, which has a larger header and so can fit more chars.
-
- To avoid comparing O(n) string equality comparison, strings can be
canonicalized to "atoms" (JSAtom) such that there is a single atom with a
given (length,chars).
-
- To avoid copying all strings created through the JSAPI, an "external"
string (JSExternalString) can be created whose chars are managed by the
JSAPI client.
-
- To avoid using two bytes per character for every string, string characters
are stored as Latin1 instead of TwoByte if all characters are representable
in Latin1.
Although all strings share the same basic memory layout, we can conceptually
arrange them into a hierarchy of operations/invariants and represent this
hierarchy in C++ with classes:
C++ type operations+fields / invariants+properties
========================== =========================================
JSString (abstract) get(Latin1|TwoByte)CharsZ, get(Latin1|TwoByte)Chars, length / -
| \
| JSRope leftChild, rightChild / -
|
JSLinearString (abstract) latin1Chars, twoByteChars / might be null-terminated
| \
| JSDependentString base / -
|
JSFlatString - / null terminated
| |
| +-- JSExternalString - / char array memory managed by embedding
| |
| +-- JSExtensibleString tracks total buffer capacity (including current text)
| |
| +-- JSUndependedString original dependent base / -
| |
| +-- JSInlineString (abstract) - / chars stored in header
| |
| +-- JSThinInlineString - / header is normal
| |
| +-- JSFatInlineString - / header is fat
|
JSAtom - / string equality === pointer equality
|
js::PropertyName - / chars don't contain an index (uint32_t)
Classes marked with (abstract) above are not literally C++ Abstract Base
Classes (since there are no virtual functions, pure or not, in this
hierarchy), but have the same meaning: there are no strings with this type as
its most-derived type.
Atoms can additionally be permanent, i.e. unable to be collected, and can
be combined with other string types to create additional most-derived types
that satisfy the invariants of more than one of the abovementioned
most-derived types:
-
- InlineAtom = JSInlineString + JSAtom (atom with inline chars, abstract)
-
- ThinInlineAtom = JSThinInlineString + JSAtom (atom with inline chars)
-
- FatInlineAtom = JSFatInlineString + JSAtom (atom with (more) inline chars)
Derived string types can be queried from ancestor types via isX() and
retrieved with asX() debug-only-checked casts.
The ensureX() operations mutate 'this' in place to effectively the type to be
at least X (e.g., ensureLinear will change a JSRope to be a JSFlatString).
*/
可以看到,SpiderMonkey里的 JSString 是表现ECMAScript String值的基类。它下面的子类的层次设计跟V8的颇有相似之处,完全应对了本回答开头提到的5种场景:
- 场景(1)(“flat string”):JSFlatString 及其子类。最特别的是它的“inline string”,这是在JSString的共同header里“偷空间”来存储字符内容的设计。这种思路也叫做“small string”优化,我在以前另一个回答里提及过:在stack上做small string或small vector优化比在heap上效率高吗? - RednaxelaFX 的回答
- 场景(2)(“rope”):JSRope 实现了典型的二叉树(二叉DAG)形式的rope。不过它具体用在字符串拼接的时候也有些有趣的优化,上面引用的代码注释以及提到了:flat string下面有一种专门为用作字符串拼接的buffer的类型JSExtensibleString,它可以在拼接过程中有一个比较长的长度,然后等拼接结束确定最终长度后再原地把自己的长度缩短到实际长度。这个功能也跟V8可以原地缩小对象大小的功能类似。
- 场景(3)(“slice / substring”):JSDependentString
- 场景(4)(“intern”):JSAtom 及其子类 js::PropertyName
- 场景(5)(外部传入的字符串):JSExternalString
上述所有涉及实际字符串内容的存储的类似都有针对7-bit Latin1与2-byte UTF-16的特化支持。
=========================================
Chakra / ChakraCore
请参考
@Thomson
大大的回答。回头有空我再写点我的版本。
=========================================
其它JavaScript引擎的细节回头再更新…
编辑于 2016-10-02
310
作者:Thomson
链接:https://www.zhihu.com/question/51132164/answer/124477176
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
R大已经答全了,我就填下Chakra的坑吧。
Chakra的C++实现的String的基类是JavascriptString,保存的基本上就是一个字符串指针(为了跨平台自定义了char16,在Windows上定义成WCHAR。
ChakraCore/JavascriptString.h at master · Microsoft/ChakraCore · GitHub
class JavascriptString _ABSTRACT : public RecyclableObject
{
...
private:
const char16* m_pszValue; // Flattened, '\0' terminated contents
charcount_t m_charLength; // Length in characters, not including '\0'.
为了优化常见使用场景如字符串连接,子串等操作还定义了不少子类:
JavascriptString
|- LiteralString
| |- CompundString
| |- ConcateStringBase
| |- ConcatStringN
| | |- ConcatString
| |- ConcatStringBuilder
| PropertyString
| SingleCharString
| SubString
| WritableString
比如经常使用的字符串连接操作如下:
ChakraCore/JavascriptString.cpp at master · Microsoft/ChakraCore · GitHub,
inline JxavascriptString* JavascriptString::Concat(JavascriptString* pstLeft, JavascriptString* pstRight)
{
if(!pstLeft->IsFinalized())
{
if(CompoundString::Is(pstLeft))
{
return Concat_Compound(pstLeft, pstRight);
}
if(VirtualTableInfo
{
return Concat_ConcatToCompound(pstLeft, pstRight);
}
}
else if(pstLeft->GetLength() == 0 || pstRight->GetLength() == 0)
{
return Concat_OneEmpty(pstLeft, pstRight);
}
if(pstLeft->GetLength() != 1 || pstRight->GetLength() != 1)
{
return ConcatString::New(pstLeft, pstRight);
}
return Concat_BothOneChar(pstLeft, pstRight);
}
对非简单的字符串连接直接构造了ConcatString对象,该对象父类(ConcatStringN)里面有一个JavascriptString指针的数组(ConcatStringN通过模板可连接的JavascriptString数量参数化,ConcatString对应最常见的N=2),在ConcatString的构造函数里面把待连接的两个JavascriptString存进数组,这样可以不用分配内存和做copy。由于左右都是JavascriptString*,同样可以使ConcatString,这样递归下去就会生成R大提到 rope 思路的DAG(我开始没注意到这里的递归,多谢R大指出)。整个字符串的 flatten 是需要的时候再做,借用了lazy computation的想法。
ChakraCore/ConcatString.h at master · Microsoft/ChakraCore · GitHub
template
class ConcatStringN : public ConcatStringBase
{
...
protected:
JavascriptString* m_slots[N]; // These contain the child nodes. 1 slot is per 1 item (JavascriptString*).
};
ChakraCore/ConcatString.cpp at master · Microsoft/ChakraCore · GitHub
ConcatString::ConcatString(JavascriptString* a, JavascriptString* b) :
ConcatStringN<2>(a->GetLibrary()->GetStringTypeStatic(), false)
{
a = CompoundString::GetImmutableOrScriptUnreferencedString(a);
b = CompoundString::GetImmutableOrScriptUnreferencedString(b);
m_slots[0] = a;
m_slots[1] = b;
this->SetLength(a->GetLength() + b->GetLength()); // does not include null character
}
另外对SubString也有类似的优化,直接构造了SubString对象作为JavascriptString的子类对象返回。
ChakraCore/SubString.h at master · Microsoft/ChakraCore · GitHub
class SubString sealed : public JavascriptString
{
void const * originalFullStringReference; // Only here to prevent recycler to free this buffer.
SubString(void const * originalFullStringReference, const char16* subString, charcount_t length, ScriptContext *scriptContext);
ChakraCore/SubString.cpp at master · Microsoft/ChakraCore · GitHub
inline SubString::SubString(void const * originalFullStringReference, const char16* subString, charcount_t length, ScriptContext *scriptContext) :
JavascriptString(scriptContext->GetLibrary()->GetStringTypeStatic())
{
this->SetBuffer(subString);
this->originalFullStringReference = originalFullStringReference;
this->SetLength(length);
...
}