從賀老微博引出的“遍歷器(Iterators)加速那些奧秘”

我關(guān)注的賀老—賀師俊前輩@johnhax 最近發(fā)表個這樣一條微博:

原微博

雖然這條微博沒有引起大范圍的關(guān)注和討論,但是作為新人巧鸭,我陷入了思考。究竟 V8 引擎做了哪些魔法,達(dá)到了極大限度的優(yōu)化呢瓶埋?

這篇文章,將會深入淺出分析這些優(yōu)化背后的奧秘诊沪。希望大神給予斧正和引導(dǎo)养筒,同時對讀者有所啟發(fā)和幫助。

我們到底在討論什么端姚?

ECMAScript 2015 語言標(biāo)準(zhǔn)規(guī)格介紹了幾種新的數(shù)據(jù)結(jié)構(gòu):比如 Maps 和 Sets(當(dāng)然還有類似 WeakMaps 和 WeakSets等)晕粪,而這幾個新引入的數(shù)據(jù)結(jié)構(gòu)有一個共性,那就是都可以根據(jù)同樣新引入的遍歷協(xié)議(iteration protocol)進(jìn)行遍歷渐裸。這就意味著你可以使用 for...of 循環(huán)或者擴展運算符進(jìn)行操作巫湘。舉一個 Sets 簡單的例子:

const s = new Set([1, 2, 3, 4]);
console.log(...s);
// 1 2 3 4
for (const x of s) console.log(x);
// 1
// 2
// 3
// 4

同樣對于 Maps:

const m = new Map([[1, "1"], [2, "2"], [3, "3"], [4, "4"]]);
console.log(...m);
// (2) [1, "1"] (2) [2, "2"] (2) [3, "3"] (2) [4, "4"]
console.log(...m.keys());
// 1 2 3 4
console.log(...m.values());
//  1 2 3 4
for (const [x, y] of m) console.log(x, "is", y);
// 1 "is" "1"
// 2 "is" "2"
// 3 "is" "3"
// 4 "is" "4"

通過這兩個簡單的例子,展示了最基本的用法昏鹃。感興趣的讀者可以參考 ECMA 規(guī)范: Map Iterator ObjectsSet Iterator Objects尚氛。

然而不幸的是,這些可遍歷的數(shù)據(jù)結(jié)構(gòu)在 V8 引擎中并沒有很好的進(jìn)行優(yōu)化盆顾〉『郑或者說,對于這些實現(xiàn)細(xì)節(jié)優(yōu)化程度很差您宪。包括 ECMAScript 成員 Brian Terlson 也曾在 Twitter 上抱怨奈懒,指出他使用 Sets 來實現(xiàn)一個正則引擎時遇到了惱人的性能問題。

所以宪巨,現(xiàn)在是時間來對他們進(jìn)行優(yōu)化了磷杏!但在著手優(yōu)化前,我們需要先徹底認(rèn)清一個問題:這些數(shù)據(jù)結(jié)構(gòu)處理慢的真實原因究竟是什么捏卓?性能瓶頸到底在哪里极祸?
為了弄清這個問題,我們就需要理解底層實現(xiàn)上怠晴,迭代器究竟是如何工作的遥金?

深入主題探索究竟

為此,我們先從一個簡單的 for...of 循環(huán)說起蒜田。

ES6 借鑒 C++稿械、Java、C# 和 Python 語言冲粤,引入了 for...of 循環(huán)美莫,作為遍歷所有數(shù)據(jù)結(jié)構(gòu)的統(tǒng)一方法页眯。

一個最簡單的使用:

function sum(iterable) {
  let x = 0;
  for (const y of iterable) x += y;
  return x;
}

將這段代碼使用 Babel 進(jìn)行編譯,我們得到:

function sum(iterable) {
  var x = 0;
  var _iteratorNormalCompletion = true;
  var _didIteratorError = false;
  var _iteratorError = undefined;

  try {
    for (var _iterator = iterable[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
      var y = _step.value;
      x += y;
    }
  } catch (err) {
    _didIteratorError = true;
    _iteratorError = err;
  } finally {
    try {
      if (!_iteratorNormalCompletion && _iterator.return) {
        _iterator.return();
      }
    } finally {
      if (_didIteratorError) {
        throw _iteratorError;
      }
    }
  }

  return x;
}

需要告訴大家的一個事實是:目前現(xiàn)代 JavaScript 引擎在本質(zhì)上和 Babel 對 for...of 的去語法糖化處理是相同的厢呵,僅僅是在具體一些細(xì)節(jié)上有差別窝撵。

這個事實出處:

All modern JavaScript engines essentially perform the same desugaring that Babel does here to implement for-of (details vary)

上文引自 V8 核心成員,谷歌工程師 Benedikt Meurer襟铭。

可是上面經(jīng)過 Babel 編譯后的代碼不太好閱讀碌奉。別急,我刪去了煩人的異常處理蝌矛,只保留了最核心的邏輯供大家參考道批,以便研究:

function sum(iterable) {
  var x = 0;
  var iterator = iterable[Symbol.iterator]();
  for (;;) {
    var iterResult = iterator.next();
    if (iterResult.done) break;
    x += iterResult.value;
  }
  return x;
}

理解這段代碼需要的預(yù)備知識需要清楚 for...of 和 Symbol.iterator 方法關(guān)系:

一個數(shù)據(jù)結(jié)構(gòu)只要部署了 Symbol.iterator 屬性,就被視為具有 iterator 接口入撒,就可以用 for...of 循環(huán)遍歷它的成員隆豹。也就是說,for...of 循環(huán)內(nèi)部調(diào)用的是數(shù)據(jù)結(jié)構(gòu)的 Symbol.iterator 方法茅逮。

for...of 循環(huán)可以使用的范圍包括數(shù)組璃赡、Set 和 Map 結(jié)構(gòu)、某些類似數(shù)組的對象(比如 arguments 對象献雅、DOM NodeList 對象)碉考、Generator 對象,以及字符串挺身。

我們仔細(xì)觀察上段段代碼侯谁,便可以發(fā)現(xiàn)迭代器性能優(yōu)化的關(guān)鍵是:保證在循環(huán)中多次重復(fù)調(diào)用的 iterator.next() 能得到最大限度的優(yōu)化,同時章钾,最理想的情況是完全避免對 iterResult 的內(nèi)存分配墙贱。能夠達(dá)到這種目的的幾個手段便是使用類似 store-load propagation, escape analysis 和 scalar replacement of aggregates 先進(jìn)的編譯處理技術(shù)贱傀。

同時惨撇,優(yōu)化后的編譯還需要完全消除迭代器本身 —— iterable[Symbol.iterator] 的調(diào)用分配,而直接在迭代器 backing-store 上直接操作府寒。

事實上魁衙,在數(shù)組和字符串迭代器的優(yōu)化過程中,就是使用了這樣的技術(shù)和理念株搔。具體的實施文檔可以參考這里剖淀。

那么具體到 Set 迭代器的性能問題,其實關(guān)鍵原因在與:其實現(xiàn)上混合了 JavaScript 和 C++ 環(huán)境纤房。比如祷蝌,我們看 %SetIteratorPrototype%.next() 的實現(xiàn):

function SetIteratorNextJS() {
  if (!IS_SET_ITERATOR(this)) {
    throw %make_type_error(kIncompatibleMethodReceiver,
                        'Set Iterator.prototype.next', this);
  }

  var value_array = [UNDEFINED, UNDEFINED];
  var result = %_CreateIterResultObject(value_array, false);
  switch (%SetIteratorNext(this, value_array)) {
    case 0:
      result.value = UNDEFINED;
      result.done = true;
      break;
    case ITERATOR_KIND_VALUES:
      result.value = value_array[0];
      break;
    case ITERATOR_KIND_ENTRIES:
      value_array[1] = value_array[0];
      break;
  }

  return result;
}

這段代碼實際上按部就班做了這么幾件事情:

  • 定義分配了 value_array 數(shù)組,初始化時為兩項 undefined帆卓;
  • 定義分配了 result 對象巨朦,其格式為 {value: value_array, done: false};
  • 調(diào)用 C++ %SetIteratorNext runtime 函數(shù)剑令,將迭代器的 this 和 value_array 作為參數(shù)傳遞糊啡。
  • 根據(jù) C++ %SetIteratorNext runtime 函數(shù)返回結(jié)果,將 result 正確賦值

這就造成什么樣的后果呢吁津?遍歷的每一步我們都在不斷地分配兩個對象:value_array 和 result棚蓄。不管是 V8 的 TurboFan 還是 Crankshaft (V8的優(yōu)化編譯器) 我們都無法消除這樣的操作。更糟糕的是碍脏,每一步遍歷我們都要在 JavaScript 和 C++ 之間進(jìn)行切換梭依。下面的圖,簡要表示了一個簡單的 SUM 函數(shù)在底層的運行流程:

set-iterator-next-js-20170714.png

在 V8 執(zhí)行時典尾,總處在兩個狀態(tài)(事實上更多):執(zhí)行 C++ 代碼和執(zhí)行 JavaScript 當(dāng)中役拴。這兩個狀態(tài)之間的轉(zhuǎn)換開銷巨大。從 JavaScript 到 C++钾埂,是依賴所謂的 CEntryStub 完成河闰,CEntryStub 會觸發(fā) C++ 當(dāng)中指定的 Runtime_Something 函數(shù)(本例中為 Runtime_SetIteratorNext)。所以褥紫,是否可以避免這種轉(zhuǎn)換姜性,以及避免 value_array 和 result 對象的分配又決定了性能的關(guān)鍵。

最新的 %SetIteratorPrototype%.next() 實現(xiàn)正是切中要害髓考,做了這個“關(guān)鍵”的處理部念。我們想要執(zhí)行的代碼在調(diào)用之前就會變得 hot (熱處理),TurboFan 進(jìn)而最終得以熱處理優(yōu)化氨菇。借助所謂的 CodeStubAssembler儡炼,baseline implementation,現(xiàn)在已經(jīng)完全在 JavaScript 層面實現(xiàn)接入门驾。這樣我們僅僅只需要調(diào)用 C++ 來做垃圾回收(在可用內(nèi)存耗盡時)以及異常處理的工作射赛。

基于回調(diào)函數(shù)的遍歷

在遍歷協(xié)議中,JavaScript 同樣提供 Set.prototype.forEach 和 Map.prototype.forEach 方法奶是,來接收一個回調(diào)函數(shù)楣责。這些同樣依賴于 C++ 的處理邏輯,這樣不僅僅需要我們將 JavaScript 轉(zhuǎn)換為 C++聂沙,還要處理回調(diào)函數(shù)轉(zhuǎn)換為 Javascript秆麸,這樣的工作模式如下圖:

set-iterator-foreach-20170714.png

所以,上面使用 CodeStubAssembler 的方式只能處理簡單的非回調(diào)函數(shù)場景及汉。真正完全意義上的優(yōu)化沮趣,包括 forEach 方法的 TurboFan 化還需要一些待開發(fā)的魔法。

優(yōu)化提升 Benchmark

我們使用下面的 benchmark 代碼進(jìn)行優(yōu)化程度的評測:

const s = new Set;
const m = new Map;
for (let i = 0; i < 1e7; ++i) {
  m.set(i, i);
  s.add(i);
}

function SetForOf() {
  for (const x of s) {}
}

function SetForOfEntries() {
  for (const x of s.entries()) {}
}

function SetForEach() {
  s.forEach(function(key, key, set) {});
}

function MapForOf() {
  for (const x of m) {}
}

function MapForOfKeys() {
  for (const x of m.keys()) {}
}

function MapForOfValues() {
  for (const x of m.values()) {}
}

function MapForEach() {
  m.forEach(function(val, key, map) {});
}

const TESTS = [
    SetForOf,
    SetForOfEntries,
    SetForEach,
    MapForOf,
    MapForOfKeys,
    MapForOfValues,
    MapForEach
];

// Warmup.
for (const test of TESTS) {
  for (let i = 0; i < 10; ++i) test();
}

// Actual tests.
for (const test of TESTS) {
  console.time(test.name);
  test();
  console.timeEnd(test.name);
}

在 Chrome60 和 Chrome61 版本的對比中坷随,得到下面圖標(biāo)結(jié)論:

improvements-20170714.png

可見房铭,雖然大幅提升驻龟,但是我們還是得到了一些不太理想的結(jié)果。尤其體現(xiàn)在 SetForOfEntries 和 MapForOf 上缸匪。但是這將會在更長遠(yuǎn)的計劃上進(jìn)行處理翁狐。

總結(jié)

這篇文章只是在大面上介紹了遍歷器性能所面臨的瓶頸和現(xiàn)有的解決方案。通過賀老的微博凌蔬,對一個問題進(jìn)行探究露懒,最終找到 V8 核心成員 Benedikt Meurer 的 Faster Collection Iterators一文,進(jìn)行參考并翻譯砂心。想要完全透徹理解原文章中內(nèi)容懈词,還需要扎實的計算機基礎(chǔ)、v8
引擎工作方式以及編譯原理方面的知識儲備辩诞。

因我才疏學(xué)淺坎弯,入行也不到兩年,更不算計算機科班出身躁倒,拾人牙慧的同時難免理解有偏差和疏漏荞怒。更多 V8 引擎方面的知識,建議大家更多關(guān)注@justjavac秧秉,編譯方面的知識關(guān)注 Vue 作者@尤雨溪 以及他在 JS Conf 上的演講內(nèi)容: 前端工程中的編譯時優(yōu)化褐桌。畢竟,我們關(guān)注前端大 V象迎、網(wǎng)紅的根本并不是為了看熱鬧荧嵌、看撕 b,而是希望在前輩身上學(xué)習(xí)到更多知識砾淌、得到更多啟發(fā)啦撮。

最后,隨著 ECMAScript 的飛速發(fā)展汪厨,讓每一名前端開發(fā)者可能都會感覺到處在不斷地學(xué)習(xí)當(dāng)中赃春,甚至有些疲于奔命。但是在學(xué)習(xí)新的特性和語法之外劫乱,理解更深層的內(nèi)容才是學(xué)習(xí)進(jìn)階的關(guān)鍵织中。

我同樣不知道什么是海,

赤腳站在沙灘上衷戈,

急切地等待著黎明的到來狭吼。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市殖妇,隨后出現(xiàn)的幾起案子刁笙,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,194評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件疲吸,死亡現(xiàn)場離奇詭異座每,居然都是意外死亡,警方通過查閱死者的電腦和手機摘悴,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評論 2 385
  • 文/潘曉璐 我一進(jìn)店門尺栖,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人烦租,你說我怎么就攤上這事〕担” “怎么了叉橱?”我有些...
    開封第一講書人閱讀 156,780評論 0 346
  • 文/不壞的土叔 我叫張陵,是天一觀的道長者蠕。 經(jīng)常有香客問我窃祝,道長,這世上最難降的妖魔是什么踱侣? 我笑而不...
    開封第一講書人閱讀 56,388評論 1 283
  • 正文 為了忘掉前任粪小,我火速辦了婚禮,結(jié)果婚禮上抡句,老公的妹妹穿的比我還像新娘探膊。我一直安慰自己,他們只是感情好待榔,可當(dāng)我...
    茶點故事閱讀 65,430評論 5 384
  • 文/花漫 我一把揭開白布逞壁。 她就那樣靜靜地躺著,像睡著了一般锐锣。 火紅的嫁衣襯著肌膚如雪腌闯。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,764評論 1 290
  • 那天雕憔,我揣著相機與錄音姿骏,去河邊找鬼。 笑死斤彼,一個胖子當(dāng)著我的面吹牛分瘦,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播畅卓,決...
    沈念sama閱讀 38,907評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼擅腰,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了翁潘?” 一聲冷哼從身側(cè)響起趁冈,我...
    開封第一講書人閱讀 37,679評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后渗勘,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體沐绒,經(jīng)...
    沈念sama閱讀 44,122評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,459評論 2 325
  • 正文 我和宋清朗相戀三年旺坠,在試婚紗的時候發(fā)現(xiàn)自己被綠了乔遮。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,605評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡取刃,死狀恐怖蹋肮,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情璧疗,我是刑警寧澤坯辩,帶...
    沈念sama閱讀 34,270評論 4 329
  • 正文 年R本政府宣布,位于F島的核電站崩侠,受9級特大地震影響漆魔,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜却音,卻給世界環(huán)境...
    茶點故事閱讀 39,867評論 3 312
  • 文/蒙蒙 一改抡、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧系瓢,春花似錦阿纤、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,734評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至肌稻,卻和暖如春清蚀,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背爹谭。 一陣腳步聲響...
    開封第一講書人閱讀 31,961評論 1 265
  • 我被黑心中介騙來泰國打工枷邪, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人诺凡。 一個月前我還...
    沈念sama閱讀 46,297評論 2 360
  • 正文 我出身青樓东揣,卻偏偏與公主長得像,于是被迫代替她去往敵國和親腹泌。 傳聞我的和親對象是個殘疾皇子嘶卧,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,472評論 2 348

推薦閱讀更多精彩內(nèi)容