性能:切换与多态性

时间:2018-05-17 23:16:22

标签: javascript switch-statement polymorphism v8 vtable

我通常更喜欢多态而不是切换。我发现它更具可读性,并且需要更少的线条。我相信这些事实足以继续使用它。但性能呢?我创建了一个非常简单(和不好)的工作台,看起来我的情况下切换速度更快。你能解释一下原因吗?

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();

据我所知,第一个样本有一个动态/虚拟运行时调用myClass.GetImportantValue(),并且就是这样。但是第二个还有一个动态/虚拟运行时调用myClassEnum.type,然后检查交换机中的条件

我很可能在代码中有一些错误,但我找不到它。我认为唯一可以影响结果的是performance.now()。但我认为它并没有那么多影响。

2 个答案:

答案 0 :(得分:2)

V8开发者在这里。你的直觉是对的:这个微基准测试不是很有用。

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

一个问题是您依靠堆栈替换(OSR)进行优化,因此性能影响会以误导的方式污染您的计时 - 特别是对于具有此后两个长期运行模式的函数循环:它们针对第一个循环进行OSR优化,在中间进行优化,然后再次针对第二个循环进行OSR优化。

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

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

长话短说:在微基准测试中,人们倾向于测量与人们想要测量的无关的奇怪效果;在较大的应用程序中,大多数这样的实现细节都没有可测量的影响。编写对您有意义的代码(可读,可维护等),让JavaScript引擎担心其余部分! (例外:有时分析表明您的应用程序存在特定瓶颈 - 在这种情况下,手动优化事物会产生很大影响,但这通常通过考虑上下文并使整体算法/控制流更有效率来实现而不是遵循简单的经验法则,例如“喜欢多态性而不是转换语句”(或者反之亦然)。)

答案 1 :(得分:-1)

我没有看到&#34;错误&#34;在你的脚本中。虽然我真的不鼓励这种方式进行性能测试,但我仍然可以根据自己的直觉说出一些事情。对于控制组等,我没有扎实的,经过良好测试的结果,所以用一小撮盐拿我说的话。

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

  • 对象属性访问(可能是O(1)哈希表,但仍然比变量访问慢)
  • 函数调用

如果我们计算函数调用和对象访问:

  • 第一种情况:5次调用[至getImportantValuePolymorphism] x(1个对象访问[至myClass] + 1个函数调用[至GetImportantValue] ===&gt; TOTAL OF 10个函数调用+5个对象访问
  • 第二种情况:5呼叫[至getImportantValueSwitch] + 5对象访问[至MyClassEnum] ===&gt; 共有5个功能调用+5个对象访问

还有一点需要注意的是,在第一种情况下,您有一个调用另一个函数的函数,因此您最终会得到一个范围链。这种影响很小,但在表现方面仍然不利。

如果我们考虑上述所有因素,首先会慢一些。但是多少钱?这不容易回答,因为它取决于供应商的实施,但在你的情况下,它的铬约慢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();
    }

结果小提琴:https://jsfiddle.net/ibowankenobi/oqzpfqcg/2/

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