js字符串最多存储多少字节?

2018-01-05  本文已影响64人  神刀

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大类使用场景:

  1. 已经要查看内容的字符串:使用flat string思路来实现,本质上说就是用数组形式来存储String的内容;
  2. 拼接字符串但尚未查看其内容:使用“rope”思路或其它延迟拼接的思路来实现。当需要查看其内容时则进行“flatten”操作将其转换为flat string表现形式。最常见rope的内部节点就像二叉树(RopeNode { Left; Right })一样,但也可以有采用更多叉树的设计的节点,或者是用更动态的多叉树实现;
  3. 子串(substring):使用“slice”思路来实现,也就是说它只是一个view,自己并不存储字符内容而只是记录个offset和length,底下的存储共享自其引用的源字符串;
  4. 值得驻留(intern)的字符串:通常也是flat string但可能会有更多的限制,存储它的空间可能也跟普通String不一样。最大的好处是在特殊场景下有些字符串会经常重复出现,或者要经常用于相等性比较,把这些字符串驻留起来可以节省内存(内容相同的字符串只驻留一份),并且后续使用可以使用指针比较来代替完全的相等性比较(因为驻留的时候已经比较过了);
  5. 外来字符串:有时候JavaScript引擎跟外界交互,外界想直接把一个char8_t或者char16_t传给JavaScript引擎当作JavaScript字符串用。JavaScript引擎可能会针对某些特殊场景提供一种包装方式来直接把这些外部传进来的字符串当作JavaScript String,而不拷贝其内容。

在上述5种场景中,涉及存储的时候都可以有

如果把语言层面的一个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"。

其中:

而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相关的、应用于特殊场景的类型。其中比较典型的有:

这个版本的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接口的实现类,其中有:

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

/*

*/

可以看到,SpiderMonkey里的 JSString 是表现ECMAScript String值的基类。它下面的子类的层次设计跟V8的颇有相似之处,完全应对了本回答开头提到的5种场景:

上述所有涉及实际字符串内容的存储的类似都有针对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<ConcatString>::HasVirtualTable(pstLeft))

{

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 <int N>

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);

...

}

上一篇下一篇

猜你喜欢

热点阅读