String.prototype.repeat在V8和Chakra中的實現

做者: @flowmemo
源地址: http://flowmemo.github.io/2016/03/25/str...javascript

最近一個left-pad事件搞得javascript圈沸沸揚揚的. 咱們暫且把這個事情放一邊, 來看看left-pad自己的實現.html

left-pad的源碼以下:java

module.exports = leftpad;

function leftpad (str, len, ch) {
  str = String(str);
  var i = -1;
  if (!ch && ch !== 0) ch = ' ';
  len = len - str.length;
  while (++i < len) {
    str = ch + str;
  }
  return str;
}

這段程序的做用是, 給一個字符串str(或能夠轉成str的變量), 用字符ch在左邊補位, 將其補到長度爲len.
固然這個程序沒作充足的參數檢查, 這個就不細說了. 咱們分析一下它的效率:
若是要補n位, 字符串加法的執行次數是n次, 也就是O(n).git

咱們來看一下如何用ES6的String.prototype.repeat來實現這個功能:
假定str是字符串, len是非負整數, ch參數可選(若是有的話必須是長度爲1的字符串) 因此我更喜歡強類型語言.github

function leftpadES6 (str, len, ch) {
  ch = ch||' '
  return ch.repeat(len - str.length) + str
}

固然還沒完, 這麼寫的效率怎麼樣呢, 咱們得看一下js引擎對String.prototype.repeat的實現.算法

V8

下面是Chrome/Chromuim的js引擎V8的實現, 直接用js寫的
源碼地址: https://code.google.com/p/chromium/codes...瀏覽器

// ES6, section 21.1.3.13
function StringRepeat(count) {
  CHECK_OBJECT_COERCIBLE(this, "String.prototype.repeat");
  
  var s = TO_STRING(this);
  var n = TO_INTEGER(count);

  if (n < 0 || n === INFINITY) throw MakeRangeError(kInvalidCountValue);

  // Early return to allow an arbitrarily-large repeat of the empty string.
  if (s.length === 0) return "";

  // The maximum string length is stored in a smi, so a longer repeat
  // must result in a range error.
  if (n > %_MaxSmi()) throw MakeRangeError(kInvalidCountValue);

  var r = "";
  while (true) {
    if (n & 1) r += s;
    n >>= 1;
    if (n === 0) return r;
    s += s;
  }
}

忽略參數檢查, 咱們來關注算法自己. 這個算法的核心是, 使用字符串重複次數count的二進制表示, 經過字符串的自加來減小字符串加法的次數. 這個算法和快速冪算法很是像. 詳細的算法解釋能夠看這篇文章: http://www.2ality.com/2014/01/efficient-...函數

舉例個例子, 若是count = 6 , 那個它的二進制表示就爲1102 = 4*1 + 2*1 + 1*0. 也就是說對於長度爲6的字符串s, 有
s.repeat(6) = s.repeat(4) + s.repeat(2)工具

注意到每次循環最多有兩次字符串加法的操做, 而循環次數約等於logn, 因此按字符串加法的次數來記它的複雜度爲O(logn)優化

Firefox的實現相似, 地址在這裏https://dxr.mozilla.org/mozilla-central/... .

Chakra

好了, 咱們來看看微軟的Edge瀏覽器所使用的js引擎, Chakra對String.prototype.repeat的實現, 它是用的C++.
源碼地址: https://github.com/Microsoft/ChakraCore/...

Chakra中實現repeat分了兩個函數, 一個是JavascriptString::EntryRepeat, 它的主要是作一些初始化工做, 參數檢查, 特殊狀況的處理.核心算法是JavascriptString::RepeatCore, 代碼以下

JavascriptString* JavascriptString::RepeatCore(JavascriptString* currentString, charcount_t count, ScriptContext* scriptContext)
  {
    Assert(currentString != nullptr);
    Assert(currentString->GetLength() > 0);
    Assert(count > 0);

    const char16* currentRawString = currentString->GetString();
    int currentLength = currentString->GetLength();

    charcount_t finalBufferCount = UInt32Math::Add(UInt32Math::Mul(count, currentLength), 1);
    char16* buffer = RecyclerNewArrayLeaf(scriptContext->GetRecycler(), char16, finalBufferCount);

    if (currentLength == 1)
    {
        wmemset(buffer, currentRawString[0], finalBufferCount - 1);
        buffer[finalBufferCount - 1] = '\0';
    }
    else
    {
        char16* bufferDst = buffer;
        size_t bufferDstSize = finalBufferCount;

        for (charcount_t i = 0; i < count; i += 1)
        {
            js_wmemcpy_s(bufferDst, bufferDstSize, currentRawString, currentLength);
            bufferDst += currentLength;
            bufferDstSize -= currentLength;
        }
        Assert(bufferDstSize == 1);
        *bufferDst = '\0';
    }

    return JavascriptString::NewWithBuffer(buffer, finalBufferCount - 1, scriptContext);
  }

看起來很長是嗎? 不要被嚇到了, 咱們只關心核心算法, 其實就這一小段:

if (currentLength == 1)
{
    wmemset(buffer, currentRawString[0], finalBufferCount - 1);
    buffer[finalBufferCount - 1] = '\0';
}
else
{
    char16* bufferDst = buffer;
    size_t bufferDstSize = finalBufferCount;

    for (charcount_t i = 0; i < count; i += 1)
    {
        js_wmemcpy_s(bufferDst, bufferDstSize, currentRawString, currentLength);
        bufferDst += currentLength;
        bufferDstSize -= currentLength;
    }
    Assert(bufferDstSize == 1);
    *bufferDst = '\0';
}

大意是若是字符串長度自己爲1, 也就是一個字符, 那就直接用wmemset(相似於memset)將一塊內存全用這個字符填充; 若是不爲字符串長度不爲1, 就一次鏈接一個字符串. 忽略js_wmemcpy_s

咱們以前說了, V8實現的字符串加法的操做次數是O(logn)的, 可是, 咱們要把一個字符串重複n次, 必定要得在要對O(n)的內存進行寫操做.
update1: 通過@哦胖茶巨巨的提醒(http://weibo.com/2451315930/DowQCo6wN), V八、ChakraCore 和 Rhino 底層實現字符串拼接用的都是rope(tree). 對於rope來講字符串鏈接不須要爲新字符串開闢被內存並把內容寫進去, 而是合併兩個二叉樹, 詳見這裏: https://en.wikipedia.org/wiki/Rope_%28da... .

這麼看的話, 在不考慮rope攤平的狀況下, 僅從算法複雜度的角度來看V8的rope鏈接和快速冪實現是比Chakra的要好. Chakra裏字符串也用了rope, 可是對repeat的實現沒有用rope, 直接就是複製內存. 字符串在內存是就是以連續內存的形式存儲的話, 把一個字符串重複n次, 必定要得在要對O(n)的內存進行寫操做, 因此快速冪優化意義不大.

至於V8的這種字符串加法用rope、js實現用快速冪的方法好, 仍是Chakra這種直接複製內存的方法好, 我沒有跑benchmark, 就不下結論了.

最後想說的是, 雖然這篇文章關注的是實現算法, 可是參數檢查、邊界條件的處理, 也很是的重要, 千萬不能以爲無所謂. 能夠說工具函數中edge case的處理常常比算法更爲頭疼...

P.S.1 我在知道V8, Chakra的字符串實現用了rope後對本文進行過修改
*P.S.2 我以前搞錯了, V8中字符串「+」操做符不依賴String.prototype.concat, 實際正好相反,是String.prototype.concat的實現直接用了字符串「+」運算. 源碼: https://code.google.com/p/chromium/codes...

相關文章
相關標籤/搜索