单态化用例

本指南通过示例演示了单态化如何提高动态语言的性能,而不会深入介绍单态化的实现方式(在 拆分 指南中介绍)或如何在语言实现中利用单态化(在 报告多态性 指南中介绍)。

单态化 #

为了更好地说明单态化的优势,请考虑一个用 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%。

联系我们