単相化のユースケース

このガイドでは、例を使用して、単相化によって動的な言語のパフォーマンスを向上させる方法を説明していますが、単相化の実装方法(分割ガイドを参照)や言語実装での単相化の活用方法(多相性のレポート・ガイドを参照)の詳細は説明していません。

単相化

単相化の利点をわかりやすく説明するために、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から1回および文字列引数が指定されたcallsAddから1回コールされます。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++;
}

この例では、各型プロファイルが関数(addIntおよびaddString)の個別のコピーに含まれるように、addが複製(分割)されています。その結果、コンパイル時に、関数ごとに単一の型プロファイルのみが使用可能になり、コンパイルされたコードで負荷がかかる可能性のある型チェックが回避されます。

実行時に実行される適切な候補の検出とその複製の自動化は、単相化と呼ばれます。つまり、これはAST複製を介した多相ノードの実行時自動単相化です。

例1 - 引数の単相化

この例は、前の項で示した例を拡張したものです。add関数は、引き続き単相化のターゲットであり、3セットの異なる引数(数値、文字列および配列)が指定されたaction関数から3回コールされます。ウォームアップに十分な時間を確保するためにaction関数を15秒間実行し、その後60秒間実行して各実行にかかった時間を追跡し、最後に平均をレポートします。このコードを2回実行します。1回は単相化を有効にし、もう1回は有効にせずに実行し、これら2つの実行の出力と高速化をレポートします。

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関数が定義されています。この関数は、項目の配列およびこれらの項目を比較するための関数を指定し、挿入ソートを使用して配列をソートします。0と1の間の1000個のランダムな倍精度値の配列を定義し、(action関数で) 4つの異なるソート・メソッドを使用してこれを4回ソートします。最後に、前の例と同様に、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%です。