單態化使用案例

本指南透過範例展示單態化如何改善動態語言的效能,而不深入探討單態化的實作方式(在分割指南中說明),或如何在您的語言實作中利用單態化(在回報多型指南中說明)。

單態化 #

為了更好地說明單態化的好處,請考慮一個用 JavaScript 編寫的小範例

function add(arg1, arg2) {
    return arg1 + arg2;
}

function callsAdd() {
    add(1, 2);
    add("foo", "bar");
}

var i = 0;
while (i < 1000) {
    callsAdd();
    i++;
}

如您在此範例中所見,add 函式從 callsAdd 中呼叫,一次使用整數引數,一次使用字串引數。一旦 add 執行足夠次數以進行編譯,其執行設定檔將顯示 + 運算符已使用整數和字串執行,因此將編譯兩種型別的處理常式(即,型別檢查和執行),這會對效能造成影響。可以透過如下重新編寫範例來避免這種情況

function addInt(arg1, arg2) {
    return arg1 + arg2;
}

function addString(arg1, arg2) {
    return arg1 + arg2;

}
function callsAdd() {
    addInt(1, 2);
    addString("foo", "bar");
}

i = 0;
while (i < 1000) {
    callsAdd();
    i++;
}

在此範例中,add 已被複製(分割),使得每個型別設定檔都包含在該函式的單獨副本中(addIntaddString)。結果是,到了編譯時,每個函式只有一個型別設定檔可用,避免了已編譯程式碼中可能成本高昂的型別檢查。

自動偵測適合的候選項目,以及它們的複製,在執行階段執行,這就是我們所謂的單態化。換句話說,它是透過 AST 複製自動對多型節點進行執行階段單態化。

範例 1 - 引數的單態化 #

此範例是上一節中說明範例的擴充版本。add 函式仍然是單態化的目標,並從 action 函式呼叫 3 次,使用 3 組不同的引數(數字、字串和陣列)。執行 action 函式 15 秒,以便有足夠的時間進行預熱,然後執行 60 秒,追蹤每次執行所需的時間,最後報告平均值。執行此程式碼兩次:一次啟用單態化,一次停用單態化,並報告這兩個執行的輸出以及加速比。

function add(arg1, arg2) {
    return arg1 + arg2;
}

var global = 0;

function action() {
    for (var i = 0; i < 10000; i++) {
        global = add(1, 2);
        global = add("foo", "bar");
        global = add([1,2,3], [4,5,6]);
    }
}


// Warm up.
var start = Date.now();
while ((Date.now() - start) < 15000 /* 15 seconds */) {
    action();
}
// Benchmark
var iterations = 0;
var sum = 0;
var start = Date.now();
while ((Date.now() - start) < 60000 /* 60 seconds */) {
    var thisIterationStart = Date.now();
    action();
    var thisIterationTime = Date.now() - thisIterationStart;
    iterations++;
    sum += thisIterationTime;
}
console.log(sum / iterations);

停用單態化的輸出為 4.494225288735564。啟用單態化的輸出為 4.2421633923。加速比約為 5%。

範例 2 - 間接呼叫的單態化 #

此範例稍微複雜一些,並說明單態化如何使高階函式受益。在範例中,定義了 insertionSort 函式,該函式在給定項目陣列和用於比較這些項目的函式的情況下,使用插入排序來排序陣列。定義一個包含 1000 個介於 0 和 1 之間的隨機雙精度值陣列,並使用 4 種不同的排序方法(在 action 函式中)對其排序四次。最後,與先前的範例一樣,預熱 action 函式 15 秒,並報告啟用和停用單態化的情況下,此函式在接下來 60 秒內的平均執行時間。

function insertionSort (items, comparator) {
    for (var i = 0; i < items.length; i++) {
        let value = items[i];
        for (var j = i - 1; j >= 0 && comparator(items[j], value); j--) {
            items[j + 1] = items[j]
        }
        items[j + 1] = value
    }
}

// Random values in an array
var array = new Array(1000);
for (i = 0; i < array.length; i++) {
    array[i] = Math.random();
}


function action() {
    insertionSort(array, function (a, b) { return a < b                                      });
    insertionSort(array, function (a, b) { return a > b                                      });
    insertionSort(array, function (a, b) { return a.toString().length < b.toString().length; });
    insertionSort(array, function (a, b) { return a.toString().length > b.toString().length; });
}

// Warm up.
var start = Date.now();
while ((Date.now() - start) < 15000 /* 15 seconds */) {
    action();
}
// Benchmark
var iterations = 0;
var sum = 0;
var start = Date.now();
while ((Date.now() - start) < 60000 /* 60 seconds */) {
    var thisIterationStart = Date.now();
    action();
    var thisIterationTime = Date.now() - thisIterationStart;
    iterations++;
    sum += thisIterationTime;
}
console.log(sum / iterations);

停用單態化的輸出為 194.05161290322582。啟用單態化的輸出為 175.41071428571428。加速比約為 10%。

與我們聯繫