单态化用例

本指南通过示例演示了单态化如何提高动态语言的性能,但并未深入探讨单态化的实现细节(在拆分指南中描述)或如何在您的语言实现中利用单态化(在报告多态性指南中描述)。

单态化 #

为了更好地说明单态化的好处,我们来看一个用 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%。

联系我们