性能:开关与多态性

Perfomance: Switch vs Polymorphism

如果可能的话,我通常更喜欢多态而不是 switch。我发现它更具可读性并且需要的行数更少。我相信这些事实足以继续使用它。但是性能呢?我创建了一个非常简单(而且很糟糕)的工作台,看起来 switch 在我的情况下更快。你能解释一下为什么吗?

https://jsfiddle.net/oqzpfqcg/1/

var class1 = { GetImportantValue: () => 1 };
var class2 = { GetImportantValue: () => 2 };
var class3 = { GetImportantValue: () => 3 };
var class4 = { GetImportantValue: () => 4 };
var class5 = { GetImportantValue: () => 5 };

getImportantValueSwitch = (myClassEnum) => {
    switch (myClassEnum.type) {
        case 'MyClass1': return 1;
        case 'MyClass2': return 2;
        case 'MyClass3': return 3;
        case 'MyClass4': return 4;
        case 'MyClass5': return 5;
    }
}

getImportantValuePolymorphism = (myClass) => myClass.GetImportantValue();

test = () => {
    var INTERATION_COUNT = 10000000;

    var t0 = performance.now();
    for (var i = 0; i < INTERATION_COUNT; i++) {
        getImportantValuePolymorphism(class1);
        getImportantValuePolymorphism(class2);
        getImportantValuePolymorphism(class3);
        getImportantValuePolymorphism(class4);
        getImportantValuePolymorphism(class5);
    }
    var t1 = performance.now();

    var t2 = performance.now();
    for (var i = 0; i < INTERATION_COUNT; i++) {
        getImportantValueSwitch({type: 'MyClass1'});
        getImportantValueSwitch({type: 'MyClass2'});
        getImportantValueSwitch({type: 'MyClass3'});
        getImportantValueSwitch({type: 'MyClass4'});
        getImportantValueSwitch({type: 'MyClass5'});
    }
    var t3 = performance.now();
    var first = t1 - t0;
    var second = t3 - t2;
    console.log("The first sample took " + first + " ms");
    console.log("The second sample took " + second + " ms");
    console.log("first / second =  " + (first/second));
};
test();

据我所知,第一个示例有一个 dynamic/virtual 运行时调用 myClass.GetImportantValue(),仅此而已。但是第二个也有一个 dynamic/virtual 运行时调用 myClassEnum.type 然后 检查开关中的条件

很可能我在代码中有一些错误,但我找不到它。我认为唯一会影响结果的是 performance.now()。不过我觉得影响不大。

我在你的脚本中没有看到 "mistake"。虽然我真的不鼓励以这种方式进行性能测试,但我还是可以根据我的直觉说几句。我没有可靠的、经过良好测试的对照组等结果,所以请对我所说的一切持保留态度。

现在,对我来说,假设第一个选项会吃掉第二个选项是很正常的,因为有一些东西比 js 中的变量访问更昂贵:

  • object 属性 访问(大概 O(1) hash table,但仍然比变量访问慢)
  • 函数调用

如果算上函数调用和对象访问:

  • 第一种情况:5 次调用 [to getImportantValuePolymorphism] x(1 次对象访问 [to myClass] + 1 次函数调用 [to GetImportantValue] ===> 总共 10 个函数调用 + 5 个对象访问
  • 第二种情况:5次调用[到getImportantValueSwitch] + 5次对象访问[到MyClassEnum] ===> 总共5次函数调用+ 5次对象访问

还有一点要提及,在第一种情况下,您有一个调用另一个函数的函数,因此您最终得到一个 作用域链。这样做的净效果很小,但仍然不利于性能。

如果综合以上因素,first会比较慢。但是多少钱?这不容易回答,因为它取决于供应商的实现,但在你的情况下,它在 chrome 中慢了大约 25 倍。假设在第一种情况下我们有 两倍的函数调用和一个作用域链 ,人们会期望它慢 2 或 3 倍,但不是 25。

我认为这种指数级的性能下降是由于你饿死事件循环,这意味着当你给 js 一个同步任务时,因为它是单一的线程化,如果任务是一个繁琐的任务,事件循环将无法继续并卡住一秒钟左右。当人们在远离目标时间范围时看到 setTimeout 或其他异步调用的奇怪行为时,就会出现这个问题。正如我所说,这是因为之前的同步任务花费的时间太长了。在您的情况下,您有一个迭代 1000 万次的同步 for 循环。

为了验证我的假设,将 ITERATION_COUNT 减少到 100000,即减少 100 倍,您会看到在 chrome 中,比率将从 ~20 减少到 ~2。所以底线 1:您观察到的部分低效率是由于您正在耗尽事件循环这一事实,但它仍然没有改变第一个选项较慢的事实

要测试函数调用确实是这里的瓶颈,请将脚本的相关部分更改为:

class1 = class1.GetImportantValue;
class2 = class2.GetImportantValue;
class3 = class3.GetImportantValue;
class4 = class4.GetImportantValue;
class5 = class5.GetImportantValue;

测试:

for (var i = 0; i < INTERATION_COUNT; i++) {
        class1();
        class2();
        class3();
        class4();
        class5();
    }

结果 fiddle:https://jsfiddle.net/ibowankenobi/oqzpfqcg/2/

这次你会看到第一个更快,因为它是(5 次函数调用)对比(5 次函数调用 + 5 次对象访问)。

这里是 V8 开发人员。您的直觉是正确的:这个微基准不是很有用。

一个问题是所有 "classes" 都具有相同的形状,因此 "polymorphic" 实际上是单态的。 (如果你解决了这个问题,请注意 V8 在 <= 4 和 >= 5 的多态情况下具有截然不同的性能特征!)

一个问题是您依赖堆栈上替换 (OSR) 进行优化,因此其性能影响会以一种误导的方式污染您的时间——尤其是对于具有连续两个长的这种模式的函数-运行 循环:它们针对第一个循环进行 OSR 优化,在中间取消优化,然后针对第二个循环再次进行 OSR 优化。

一个问题是编译器内联了很多东西,因此实际执行的机器代码可能与您编写的 JavaScript 代码具有非常不同的结构。特别是在这种情况下,getImportantValueSwitch 被内联,{type: 'MyClass*'} 常量对象创建被省略,生成的代码只是一些非常快的比较。

一个问题是对于小函数,调用开销几乎支配了其他一切。 V8 的优化编译器目前不进行多态内联(因为这并不总是成功),因此调用 () => 1 等函数花费了大量时间。这与它们是动态调度的事实 无关 - 从对象中检索正确的函数非常快,调用它是有开销的。对于较大的函数,您不会注意到它太多,但对于几乎为空的函数,与不执行 any 调用的基于 switch 的替代方案相比,它非常重要.

长话短说:在微基准测试中,人们倾向于测量与我们想要测量的东西无关的奇怪效果;在较大的应用程序中,像这样的大多数实现细节都没有可衡量的影响。编写对您有意义的代码(可读性、可维护性等),让 JavaScript 引擎为其余部分操心! (例外:有时分析表明您的应用程序存在特定瓶颈——在这种情况下,手动优化可能会产生很大影响,但这通常是通过考虑上下文并制定整体算法/控制流程来实现的更有效,而不是遵循简单的经验法则,如 "prefer polymorphism over switch statements"(或相反)。)