[譯] 或許你並不須要 Rust 和 WASM 來提高 JS 的執行效率 — 第二部分

如下內容爲本系列文章的第二部分,若是你還沒看第一部分,請移步或許你並不須要 Rust 和 WASM 來提高 JS 的執行效率 — 第一部分html

我嘗試過三種不一樣的方法對 Base64 VLQ 段進行解碼。前端

第一個是 decodeCached,它與 source-map 使用的默認實現方式徹底相同 — 我已經在上面列出了:android

function decodeCached(aStr) {
    var length = aStr.length;
    var cachedSegments = {};
    var end, str, segment, value, temp = {value: 0, rest: 0};
    const decode = base64VLQ.decode;

    var index = 0;
    while (index < length) {
    // Because each offset is encoded relative to the previous one,
    // many segments often have the same encoding. We can exploit this
    // fact by caching the parsed variable length fields of each segment,
    // allowing us to avoid a second parse if we encounter the same
    // segment again.
    for (end = index; end < length; end++) {
        if (_charIsMappingSeparator(aStr, end)) {
        break;
        }
    }
    str = aStr.slice(index, end);

    segment = cachedSegments[str];
    if (segment) {
        index += str.length;
    } else {
        segment = [];
        while (index < end) {
        decode(aStr, index, temp);
        value = temp.value;
        index = temp.rest;
        segment.push(value);
        }

        if (segment.length === 2) {
        throw new Error('Found a source, but no line and column');
        }

        if (segment.length === 3) {
        throw new Error('Found a source and line, but no column');
        }

        cachedSegments[str] = segment;
    }

    index++;
    }
}
複製代碼

第二個是 decodeNoCaching。它實際上就是沒有緩存的 decodeCached。每一個分段都被單獨解碼。我使用 Int32Array 來進行 segment 存儲,而再也不使用 Arrayios

function decodeNoCaching(aStr) {
    var length = aStr.length;
    var cachedSegments = {};
    var end, str, segment, temp = {value: 0, rest: 0};
    const decode = base64VLQ.decode;

    var index = 0, value;
    var segment = new Int32Array(5);
    var segmentLength = 0;
    while (index < length) {
    segmentLength = 0;
    while (!_charIsMappingSeparator(aStr, index)) {
        decode(aStr, index, temp);
        value = temp.value;
        index = temp.rest;
        if (segmentLength >= 5) throw new Error('Too many segments');
        segment[segmentLength++] = value;
    }

    if (segmentLength === 2) {
        throw new Error('Found a source, but no line and column');
    }

    if (segmentLength === 3) {
        throw new Error('Found a source and line, but no column');
    }

    index++;
    }
}
複製代碼

最後,第三個是 decodeNoCachingNoString,它嘗試經過將字符串轉換爲 utf8 編碼的 Uint8Array 來避免處理 JavaScript 字符串。這個優化受到了下面的啓發:JS VM 將數組加載優化成單獨內存訪問的可能性更高。因爲 JS VM 使用的不一樣的字符串表示層級和結構很是複雜,因此將 String.prototype.charCodeAt 優化到相同的水準會更加困難。git

我對比了兩個版本,一個是將字符串編碼爲 utf8 的版本,另外一個是使用預編碼字符串的版本。用後面的這個「優化」版本,我想評估一下,經過數組 ⇒ 字符串 ⇒ 數組的轉化,能夠給咱們帶來多少的性能提高。「優化」版本的實現方式是咱們將 source map 以加載到數組緩衝區,直接從該緩衝區解析它,而不是先把它轉爲字符串。程序員

let encoder = new TextEncoder();
function decodeNoCachingNoString(aStr) {
    decodeNoCachingNoStringPreEncoded(encoder.encode(aStr));
}

function decodeNoCachingNoStringPreEncoded(arr) {
    var length = arr.length;
    var cachedSegments = {};
    var end, str, segment, temp = {value: 0, rest: 0};
    const decode2 = base64VLQ.decode2;

    var index = 0, value;
    var segment = new Int32Array(5);
    var segmentLength = 0;
    while (index < length) {
    segmentLength = 0;
    while (arr[index] != 59 && arr[index] != 44) {
        decode2(arr, index, temp);
        value = temp.value;
        index = temp.rest;
        if (segmentLength < 5) {
        segment[segmentLength++] = value;
        }
    }

    if (segmentLength === 2) {
        throw new Error('Found a source, but no line and column');
    }

    if (segmentLength === 3) {
        throw new Error('Found a source and line, but no column');
    }

    index++;
    }
}
複製代碼

下面是我在 Chrome Dev66.0.3343.3(V86.6.189)和 Firefox Nightly60.0a1 中運行個人基準測試獲得的結果(2018-02-11):github

不一樣的解碼

注意幾點:算法

  • 在 V8 和 SpiderMonkey 上,使用緩存的版本比的其餘版本都要慢。隨着緩存數量的增長,其性能急劇降低 — 而無緩存版本的性能不會受此影響;
  • 在 SpiderMonkey 上,將字符串轉換爲類型化數組再去解析是有利的,而在 V8 上直接字符訪問的速度就已經足夠快了 - 因此只有在把將字符串到數組的轉換移出基準的狀況下,使用數組是有利的。(例如,你將你的數據一開始就加載到類型數組中)

我很懷疑 V8 團隊近年來沒有改進過 charCodeAt 的性能 — 我清楚地記得 Crankshaft 沒有花費力氣把 charCodeAt 做爲特定字符串的調用方法,反而是將其擴大到全部以字符串表示的代碼塊都能使用,使得從字符串加載字符比從類型數組加載元素慢。typescript

我瀏覽了 V8 問題跟蹤器,發現了下面幾個問題:shell

這些問題的評論當中,有些引用了 2018 年 1 月末之後的提交版本,這代表正在積極地進行 charCodeAt 的性能改善。出於好奇,我決定在 Chrome Beta 版本中從新運行個人基準測試,並與 Chrome Dev 版本進行比較。

Different Decodes

事實上,經過比較能夠發現 V8 團隊的全部提交都是卓有成效的:charCodeAt 的性能從「6.5.254.21」版本到「6.6.189」版本獲得了很大提升。 經過對比「無緩存」和「使用數組」的代碼行,咱們能夠看到,在老版本的 V8 中,charCodeAt 的表現差不少,因此只是將字符串轉換爲「Uint8Array」來加快訪問速度就能夠帶來效果。然而,在新版本的 V8 中,只是在解析內部進行這種轉換的話,並不能帶來任何效果。

可是,若是您能夠不經過轉換,就能直接使用數組而不是字符串,那麼就會帶來性能的提高。 這是爲何呢? 爲了解答這個問題,我在 V8 運行如下代碼:

function foo(str, i) {
    return str.charCodeAt(i);
}

let str = "fisk";

foo(str, 0);
foo(str, 0);
foo(str, 0);
%OptimizeFunctionOnNextCall(foo);
foo(str, 0);
複製代碼
╭─ ~/src/v8/v8 ‹master›
╰─$ out.gn/x64.release/d8 --allow-natives-syntax --print-opt-code --code-comments x.js
複製代碼

這個命令產生了一個巨大的程序集列表,這個證明了個人懷疑,V8 的 「charCodeAt」 仍然沒有針對特定的字符串進行特殊處理。這種弱點彷佛源自 V8 中的這個代碼,它能夠解釋爲何數組訪問速度快於字符串的 charCodeAt 的處理。

解析改進

基於這些發現,咱們能夠從 source-map 解析代碼中刪除被解析分段的緩存,再測試影響效果。

解析和排序時間

就像咱們的基準測試預測的那樣,緩存對總體性能是不利的:刪除它能夠大大提高解析時間。

優化排序 - 算法改進

如今咱們改進了解析性能,讓咱們再看一下排序。

有兩個正在排序的數組:

  1. originalMappings 數組使用 compareByOriginalPositions 比較器進行排序;
  2. generatedMappings 數組使用 compareByGeneratedPositionsDeflated 比較器進行排序。

優化 originalMappings 排序

我首先看了一下 compareByOriginalPositions

function compareByOriginalPositions(mappingA, mappingB, onlyCompareOriginal) {
    var cmp = strcmp(mappingA.source, mappingB.source);
    if (cmp !== 0) {
    return cmp;
    }

    cmp = mappingA.originalLine - mappingB.originalLine;
    if (cmp !== 0) {
    return cmp;
    }

    cmp = mappingA.originalColumn - mappingB.originalColumn;
    if (cmp !== 0 || onlyCompareOriginal) {
    return cmp;
    }

    cmp = mappingA.generatedColumn - mappingB.generatedColumn;
    if (cmp !== 0) {
    return cmp;
    }

    cmp = mappingA.generatedLine - mappingB.generatedLine;
    if (cmp !== 0) {
    return cmp;
    }

    return strcmp(mappingA.name, mappingB.name);
}
複製代碼

我注意到,映射首先由 source 組件進行排序,而後再由其餘組件處理。source 指定映射最早來自哪一個源文件。一個顯而易見的想法是,咱們能夠將 originalMappings 變成數組的集合:originalMappings [i] 是包含第 i 個源文件全部映射的數組,而再也不使用巨大的 originalMappings 數組直接未來自不一樣源文件的映射混在一塊兒。經過這種方式,咱們能夠把從源文件解析出來的映射排序存到不一樣的 originalMappings [i] 數組中,而後對單個較小的數組再進行排序。

其實是個[桶排序](https://en.wikipedia.org/wiki/Bucket_sort)

這是咱們在解析循環中作的:

if (typeof mapping.originalLine === 'number') {
    // This code used to just do: originalMappings.push(mapping).
    // Now it sorts original mappings already by source during parsing.
    let currentSource = mapping.source;
    while (originalMappings.length <= currentSource) {
    originalMappings.push(null);
    }
    if (originalMappings[currentSource] === null) {
    originalMappings[currentSource] = [];
    }
    originalMappings[currentSource].push(mapping);
}
複製代碼

在那以後:

var startSortOriginal = Date.now();
// The code used to sort the whole array:
//     quickSort(originalMappings, util.compareByOriginalPositions);
for (var i = 0; i < originalMappings.length; i++) {
    if (originalMappings[i] != null) {
    quickSort(originalMappings[i], util.compareByOriginalPositionsNoSource);
    }
}
var endSortOriginal = Date.now();
複製代碼

「compareByOriginalPositionsNoSource」比較器幾乎與「compareByOriginalPositions」比較器徹底相同,只是它再也不比較「source」組件 - 根據咱們構造 originalMappings [i] 數組的方式,這樣能夠保證是公平的。

解析和排序時間

這個算法改進可同時提高 V8 和 SpiderMonkey 上的排序速度,還能夠改進 V8 上的解析速度。

解析速度的提高是因爲處理 originalMappings 數組的消下降了:生成一個單一的巨大的 originalMappings 數組比生成多個較小的 originalMappings [i] 數組要消耗更多。不過,這只是個人猜想,沒有通過任何嚴格的分析。

優化 generatedMappings 排序

讓咱們看一下 generatedMappingscompareByGeneratedPositionsDeflated 比較器。

function compareByGeneratedPositionsDeflated(mappingA, mappingB, onlyCompareGenerated) {
    var cmp = mappingA.generatedLine - mappingB.generatedLine;
    if (cmp !== 0) {
    return cmp;
    }

    cmp = mappingA.generatedColumn - mappingB.generatedColumn;
    if (cmp !== 0 || onlyCompareGenerated) {
    return cmp;
    }

    cmp = strcmp(mappingA.source, mappingB.source);
    if (cmp !== 0) {
    return cmp;
    }

    cmp = mappingA.originalLine - mappingB.originalLine;
    if (cmp !== 0) {
    return cmp;
    }

    cmp = mappingA.originalColumn - mappingB.originalColumn;
    if (cmp !== 0) {
    return cmp;
    }

    return strcmp(mappingA.name, mappingB.name);
}
複製代碼

這裏咱們首先比較 generatedLine 的映射。通常對比原始的源文件,可能會生成更多的行,因此將 generatedMappings 分紅多個單獨的數組是沒有意義的。

可是,當我看到解析代碼時,我注意到瞭如下的內容:

while (index < length) {
    if (aStr.charAt(index) === ';') {
    generatedLine++;
    // ...
    } else if (aStr.charAt(index) === ',') {
    // ...
    } else {
    mapping = new Mapping();
    mapping.generatedLine = generatedLine;

    // ...
    }
}
複製代碼

這是代碼中惟一出現 generatedLine 的地方,這意味着 generatedLine 是單調增加的 — 意味着 generatedMappings 數組已經被 generatedLine 排序了,因此對整個數組排序沒有意義。相反,咱們能夠對每一個較小的子數組進行排序。咱們把代碼改爲下面這樣:

let subarrayStart = 0;
while (index < length) {
    if (aStr.charAt(index) === ';') {
    generatedLine++;
    // ...

    // Sort subarray [subarrayStart, generatedMappings.length].
    sortGenerated(generatedMappings, subarrayStart);
    subarrayStart = generatedMappings.length;
    } else if (aStr.charAt(index) === ',') {
    // ...
    } else {
    mapping = new Mapping();
    mapping.generatedLine = generatedLine;

    // ...
    }
}
// Sort the tail.
sortGenerated(generatedMappings, subarrayStart);
複製代碼

我沒有使用 快速排序 來排序子數組,而是決定使用插入排序,相似於一些 VM 用於 Array.prototype.sort 的混合策略。

注意:若是輸入數組已經排序,插入排序會比快速排序更快...事實證實,用於基準測試的映射其實是排序過的。若是咱們指望 generatedMappings 在解析以後幾乎都是被排序過的,那麼在排序以前先簡單地檢查 generatedMappings 是否已經排序會更有效率。

const compareGenerated = util.compareByGeneratedPositionsDeflatedNoLine;

function sortGenerated(array, start) {
    let l = array.length;
    let n = array.length - start;
    if (n <= 1) {
    return;
    } else if (n == 2) {
    let a = array[start];
    let b = array[start + 1];
    if (compareGenerated(a, b) > 0) {
        array[start] = b;
        array[start + 1] = a;
    }
    } else if (n < 20) {
    for (let i = start; i < l; i++) {
        for (let j = i; j > start; j--) {
        let a = array[j - 1];
        let b = array[j];
        if (compareGenerated(a, b) <= 0) {
            break;
        }
        array[j - 1] = b;
        array[j] = a;
        }
    }
    } else {
    quickSort(array, compareGenerated, start);
    }
}
複製代碼

這產生如下結果:

解析和排序時間

排序時間急劇降低,而解析時間稍微增長 — 這是由於代碼將 generatedMappings 做爲解析循環的一部分進行排序,使得咱們的分解略顯無心義。讓咱們對比下改善總時間(解析和排序一塊兒)。

改善總時間

解析和排序時間

如今很明顯,咱們大大提升了總體映射解析性能。

咱們還能夠作些什麼來改善性能嗎?

是的:咱們能夠從 asm.js/WASM 指南中抽出一頁,而不用在 JavaScript 代碼基礎上所有換做使用 Rust。

優化解析 - 下降 GC 壓力

咱們正在分配成千上萬的 Mapping 對象,這給 GC 帶來了至關大的壓力 - 然而咱們並非真的須要這樣的對象 - 咱們能夠將它們打包成一個類型數組。這是個人作法。

幾年前,我對 Typed Objects 提案感到很是興奮,該提案將容許 JavaScript 程序員定義結構體和結構體數組以及不少使人驚喜的東西,這樣很方便。但不幸的是,推進該提案的領導者離開去作其餘方面的工做,這讓咱們不得不要麼本身動手,要麼使用 C++代碼來編寫這些東西。

首先,我將 Mapping 從一個普通對象變成一個指向類型數組的一個包裝器,它將包含咱們全部的映射。

function Mapping(memory) {
    this._memory = memory;
    this.pointer = 0;
}
Mapping.prototype = {
    get generatedLine () {
    return this._memory[this.pointer + 0];
    },
    get generatedColumn () {
    return this._memory[this.pointer + 1];
    },
    get source () {
    return this._memory[this.pointer + 2];
    },
    get originalLine () {
    return this._memory[this.pointer + 3];
    },
    get originalColumn () {
    return this._memory[this.pointer + 4];
    },
    get name () {
    return this._memory[this.pointer + 5];
    },
    set generatedLine (value) {
    this._memory[this.pointer + 0] = value;
    },
    set generatedColumn (value) {
    this._memory[this.pointer + 1] = value;
    },
    set source (value) {
    this._memory[this.pointer + 2] = value;
    },
    set originalLine (value) {
    this._memory[this.pointer + 3] = value;
    },
    set originalColumn (value) {
    this._memory[this.pointer + 4] = value;
    },
    set name (value) {
    this._memory[this.pointer + 5] = value;
    },
};
複製代碼

而後我調整了解析和排序代碼,以下所示:

BasicSourceMapConsumer.prototype._parseMappings = function (aStr, aSourceRoot) {
    // Allocate 4 MB memory buffer. This can be proportional to aStr size to
    // save memory for smaller mappings.
    this._memory = new Int32Array(1 * 1024 * 1024);
    this._allocationFinger = 0;
    let mapping = new Mapping(this._memory);
    // ...
    while (index < length) {
    if (aStr.charAt(index) === ';') {

        // All code that could previously access mappings directly now needs to
        // access them indirectly though memory.
        sortGenerated(this._memory, generatedMappings, previousGeneratedLineStart);
    } else {
        this._allocateMapping(mapping);

        // ...

        // Arrays of mappings now store "pointers" instead of actual mappings.
        generatedMappings.push(mapping.pointer);
        if (segmentLength > 1) {
        // ...
        originalMappings[currentSource].push(mapping.pointer);
        }
    }
    }

    // ...

    for (var i = 0; i < originalMappings.length; i++) {
    if (originalMappings[i] != null) {
        quickSort(this._memory, originalMappings[i], util.compareByOriginalPositionsNoSource);
    }
    }
};

BasicSourceMapConsumer.prototype._allocateMapping = function (mapping) {
    let start = this._allocationFinger;
    let end = start + 6;
    if (end > this._memory.length) {  // Do we need to grow memory buffer?
    let memory = new Int32Array(this._memory.length * 2);
    memory.set(this._memory);
    this._memory = memory;
    }
    this._allocationFinger = end;
    let memory = this._memory;
    mapping._memory = memory;
    mapping.pointer = start;
    mapping.name = 0x7fffffff;  // Instead of null use INT32_MAX.
    mapping.source = 0x7fffffff;  // Instead of null use INT32_MAX.
};

exports.compareByOriginalPositionsNoSource =
    function (memory, mappingA, mappingB, onlyCompareOriginal) {
    var cmp = memory[mappingA + 3] - memory[mappingB + 3];  // originalLine
    if (cmp !== 0) {
    return cmp;
    }

    cmp = memory[mappingA + 4] - memory[mappingB + 4];  // originalColumn
    if (cmp !== 0 || onlyCompareOriginal) {
    return cmp;
    }

    cmp = memory[mappingA + 1] - memory[mappingB + 1];  // generatedColumn
    if (cmp !== 0) {
    return cmp;
    }

    cmp = memory[mappingA + 0] - memory[mappingB + 0];  // generatedLine
    if (cmp !== 0) {
    return cmp;
    }

    return memory[mappingA + 5] - memory[mappingB + 5];  // name
};
複製代碼

正如你所看到的,可讀性確實受到了很大影響。理想狀況下,我但願在須要處理對應分段時分配臨時的「映射」對象。然而,這種代碼風格將嚴重依賴於虛擬機經過_allocation sinking_,_scalar replacement_或其餘相似的優化來消除這些臨時包裝分配的能力。不幸的是,在個人實驗中,SpiderMonkey 沒法很好地處理這樣的代碼,所以我選擇了更多冗長且容易出錯的代碼。

這種幾乎純手工進行內存管理的方式在 JS 中是很少見的。這就是爲何我認爲在這裏值得提出,「oxidized」 source-map 實際上須要用戶手動管理它的生命週期,以確保 WASM 資源被釋放。

從新運行基準測試,證實緩解 GC 壓力產生了很好的改善效果。

從新分配後

從新分配後

有趣的是,在 SpiderMonkey 上,這種方法對於解析和排序都有改善效果,這對我來講真是一個驚喜。

SpiderMonkey 性能斷崖

當我使用這段代碼時,我還發現了 SpiderMonkey 中使人困惑的性能斷崖現象:當我將預置內存緩衝區的大小從 4 MB 增長到 64 MB 來衡量從新分配的消耗時,基準測試顯示當進行第 7 次迭代後性能忽然降低了。

從新分配後

這看起來像某種多態性,但我不能當即就搞明白如何改變數組的大小能夠致使這樣的多態行爲。

我很困惑,但我找到了一個 SpiderMonkey 黑客 Jan de Mooij,他很快識別出 罪魁禍首是 asm.js 從 2012 年開始的相關優化......而後他將它從 SpiderMonkey 中刪除,這樣就不會有人再遇到這種狀況了。

優化分析 - 使用 Uint8Array 替代字符串。

最後,若是咱們使用 Uint8Array 代替字符串來解析,咱們又能夠獲得小的改善效果。

從新分配後

須要咱們重寫 source-map,直接使用類型數組解析映射而再也不使用 JavaScript 的字符串方法 JSON.decode 進行解析。我沒有作過這樣的改寫,但我想應該沒有什麼問題。

對基線的整體改進

這是開始的狀況:

$ d8 bench-shell-bindings.js
...
[Stats samples: 5, total: 24050 ms, mean: 4810 m
s, stddev: 155.91063145276527 ms]
$ sm bench-shell-bindings.js
...
[Stats samples: 7, total: 22925 ms, mean: 3275 ms, stddev: 269.5999093306804 ms]
複製代碼

這是咱們完成時的狀況:

$ d8 bench-shell-bindings.js
...
[Stats samples: 22, total: 25158 ms, mean: 1143.5454545454545 ms, stddev: 16.59358125226469 ms]
$ sm bench-shell-bindings.js
...
[Stats samples: 31, total: 25247 ms, mean: 814.4193548387096 ms, stddev: 5.591064299397745 ms]
複製代碼

從新分配後

從新分配後

這是 4 倍的性能提高!

也許值得注意的是,儘管這並非必須的,但咱們仍然對全部的 originalMappings 數組進行了排序。只有兩個操做使用到 originalMappings

  • allGeneratedPositionsFor 它返回給定線的全部生成位置;
  • eachMapping(..., ORIGINAL_ORDER) 它按照原始順序對全部映射進行迭代。

若是咱們假設 allGeneratedPositionsFor 是最多見的操做,而且咱們只在少數 originalMappings [i] 數組中搜索,那麼不管什麼時候咱們須要搜索其中的一個,咱們均可以經過對 originalMappings [i] 數組進行排序來大大提升解析時間。

最後比較 1 月 19 日的 V8 和 2 月 19 日的 V8 分別對應包含和不包含減小不可信代碼的修改

從新分配後

比較 Oxidized source-map 版本

繼 2 月 19 日發佈這篇文章以後,我收到一些反饋要求將我改進的 source-map 與使用 Rust 和 WASM 的主線的 Oxidized source-map 相比較。

快速查看 parse_mappings 的 Rust 源代碼,發現 Rust 版本沒有排序原始映射,只會生成等價的 generatedMappings 而且排序。爲了匹配這種行爲,我經過註釋掉 originalMappings [i] 數組的排序來調整個人 JS 版本。

這裏是僅僅是解析的對比結果(其中還包括對 generatedMappings 進行排序),而後對全部 generatedMappings 進行解析和迭代。

只有解析時間

解析和迭代次數

請注意,這個對比有點誤導,由於 Rust 版本並未像個人 JS 版本那樣優化 generatedMappings 的排序。

所以,我不會說,「咱們已經成功達到 Rust+WASM 版本的水平」。可是,在這樣成都的性能差別水準下,咱們可能須要從新評估在 source-map 中使用如此複雜的 Rust 是不是真正值得的。

更新(2018 年 2 月 27 日)

source-map 的做者 Nick Fitzgerald 把本文描述的算法已更新到 Rust+WASM 的版本。如下是解析和迭代的對比性能圖表:

解析和迭代次數

正如你能夠看到 WASM+Rust 版本在 SpiderMonkey 上的速度如今增長了大約 15%,而在 V8 上的速度也大體相同。

學習

對於 JavaScript 開發人員

分析器是你的朋友

以各類形式進行分析和性能跟蹤是得到高性能的最佳方法。它容許您在代碼中放置熱點,來揭示運行時的潛在問題。基於這個緣由,不要回避使用像 perf 這樣的底層分析工具 - 「友好」的工具可能不會告訴你整個情況,由於它們隱藏了底層的分析。

不一樣的性能問題須要不一樣的方法去分析並可以可視化地去收集分析結果。必定確保您熟悉各類可用的工具。

算法很重要

可以根據抽象複雜性來推理你的代碼是一項重要的技能。快速排序一個具備十萬個元素的數組好呢?仍是快速排序 3333 個數組,每一個子數組有 30 元素更好呢?

數學計算能夠告訴咱們((100000 log 100000)比(3333 倍的 30 log 30)大 3 倍)- 若是數據量越大,一般可以數學變換就會變得越重要。

除了瞭解對數以外,你還須要知道一些常識,而且可以評估你的代碼在平均和最糟糕的狀況下的使用狀況:哪些操做很常見,昂貴的運算成本如何攤銷,昂貴的運算攤銷帶來的壞處是什麼?

虛擬機也在工做。問題開發者!

不要猶豫,與開發人員討論奇怪的性能問題。並不是全部事情均可以經過改變本身的代碼來解決。俄國諺語說道:「製做罐子的不是上帝!」虛擬機開發人員也是人,他們也同樣會犯錯誤。只要把問題理清,他們也至關擅長把這些問題修復。一封郵件或或一個聊天消息或 DM 可能爲您節省經過外部 C++ 代碼進行調試的時間。

虛擬機仍然須要一點幫助

有時候您也須要編寫一些底層代碼或者瞭解一些底層的實現細節,這樣有助於挖掘 JavaScript 的最後一絲性能。

人們可能但願有更好的語言級別的工具來實現這一點,可是咱們能不能實現還有待觀察。

對於語言實現者/設計者

巧妙的優化必須是可檢測的

若是您的運行時具備任何內置的智能優化,那麼您須要提供一個直觀的工具來診斷這些優化失敗的時間並向開發人員提供可操做的反饋。

在 JavaScript 這樣的語言環境中,至少有像 profiler 這樣的分析工具爲您單個操做提供一種專業化方法來檢測,以肯定虛擬機優化的結果是好是壞而且指出緣由。

這種排序的自檢工具不能依賴於在虛擬機的某個版本上打個特殊的補丁,而後輸出一堆毫無可讀性的調試結果。相反,它應該是你須要的任什麼時候候,只要打開調試工具窗口,它就能把結果呈現出來。

語言和優化必須是朋友

最後,做爲一名語言設計師,您應該嘗試預測語言缺少哪些特性,從而更容易編寫出性能良好的代碼。市場上的用戶是否須要手動設置和管理內存?我肯定他們是的。若是大多數人使用了您的語言最後都寫出大量性能很低的代碼,那就只能經過添加大量的語言特性或者經過其餘途徑來提高代碼的性能。(例如,經過更復雜的優化或請求用戶用 Rust 重構代碼)

如下是一些語言設計的通用法則:若是要爲您的語言添加新特性,請確保運算過程的合理性,並且這些特性很容易被理解和檢測。從整個語言層面去考慮優化工做,而不是對一些使用頻率很低、性能相對較差的非核心特性去作優化工做。

後記

咱們在這篇文章中發現的優化大體分紅三個部分:

  1. 算法改進;
  2. 如何優化徹底獨立的代碼和有潛在依賴關係的代碼;
  3. 針對 V8 的優化方法。

不管您使用哪一種編程語言,都須要考慮到算法性能。當您在自己就「比較慢」的編程語言中使用糟糕的算法時,您能更容易的注意到這一點,可是若是隻是換成使用「比較快」的編程語言,還繼續使用相同的算法,即便問題會有所緩解,但依然沒法從根本上解決問題。這篇文章中的很大一部份內容都致力於這個部分的優化:

  • 對子數組排序優化效果要優於對整個數組進行排序優化;
  • 討論使用或者不使用緩存的優缺點。

第二部分是單態性。因爲多態性而致使的性能下降不是 V8 特有的問題。這也不是一個 JS 特有的問題。您能夠經過不一樣的實現方式,甚至跨語言的去應用單態。有些語言(Rust,實際上)已經在引擎內爲您實現。

最後一個也是最有爭議的部分是參數適配問題。

最後,使用映射表示法進行的優化(將單個對象封裝到單個類型數組中)橫跨了文中說起的三個部分。這是創建在對 GCed 系統的侷限性和性能花銷,以及 JS 虛擬機做了哪些特殊優化的基礎上進行的。

因此... 爲何我選擇了這個標題?這是由於我堅信第三部分涉及的問題都會隨着時間的推移而被修復。其餘部分可經過經常使用編程語言進行跨語言實現。

很顯然,每一個開發人員和每一個團隊均可以自由的去選擇,究竟是花費 N 小時去分析,閱讀和思考他們的 JavaScript 代碼,仍是花費 M 小時用 X 語言重寫他們的東西。

可是:(a)每一個人都須要充分意識到這種選擇是存在的;(b)語言設計者和實現者應該共同努力使這樣的選擇愈來愈不明顯 - 也就是說在語言特徵和工具方面開展工做,減小「第 3 部分」優化的需求。

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索