Производительность: переключатель против полиморфизма - PullRequest
0 голосов
/ 18 мая 2018

Я обычно предпочитаю полиморфизм вместо переключателя, когда это возможно.Я нахожу это более читабельным и требует меньше строк.Я считаю, что этих фактов достаточно, чтобы продолжать использовать его.Но как насчет производительности?Я создал довольно простую (и плохую) скамью, и похоже, что в моем случае переключение происходит быстрее.Не могли бы вы объяснить, почему?

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 голосов
/ 18 мая 2018

V8 разработчик здесь.Ваша интуиция права: этот микробенчмарк не очень полезен.

Одна проблема заключается в том, что все ваши «классы» имеют одинаковую форму, поэтому «полиморфный» случай на самом деле мономорфен.(Если вы это исправите, учтите, что V8 имеет совершенно разные характеристики производительности для <= 4 и> = 5 полиморфных случаев!)

Одна из проблем заключается в том, что вы полагаетесь на замену в стеке (OSR) дляоптимизация, поэтому влияние на производительность, которое вводит в заблуждение, вводит в заблуждение обманчивым образом, особенно для функций, которые имеют такой шаблон двух последующих длительных циклов: они оптимизированы по OSR для первого цикла, деоптимизированы в середине, а затем OSR-Оптимизирована снова для второго цикла.

Одна проблема заключается в том, что компилятор включает много вещей, поэтому фактически исполняемый машинный код может иметь структуру, совершенно отличную от написанного вами кода JavaScript.В частности, в этом случае getImportantValueSwitch становится встроенным, {type: 'MyClass*'} константные создания объектов исключаются, и в результате получается всего лишь несколько сравнений, которые выполняются очень быстро.

Одна проблема заключается в том, что с небольшими функциямиcall overhead доминирует над всем остальным.Оптимизирующий компилятор V8 в настоящее время не выполняет полиморфное встраивание (потому что это не всегда выигрыш), поэтому значительное время тратится на вызов функций () => 1 и т. Д.Это не имеет отношения к тому факту, что они отправляются динамически - извлечение нужной функции из объекта выполняется довольно быстро, вызывая ее, и это накладные расходы.Для больших функций вы бы этого не заметили, но для почти пустых функций это довольно существенно по сравнению с альтернативой на основе switch, которая не выполняет никаких вызовов .

* 1018Короче говоря: в микробенчмарках каждый измеряет странные эффекты, не связанные с тем, что намеревался измерить;и в более крупных приложениях большинство деталей реализации, подобных этой, не оказывают ощутимого влияния.Напишите код, который имеет смысл для вас (читаемый, поддерживаемый и т. Д.), Пусть движок JavaScript беспокоится обо всем остальном! (Исключение: иногда профилирование указывает, что у вашего приложения есть определенное узкое место - в таких случаях оптимизация вручную может иметь большое влияние, но обычно это достигается с учетом контекста и повышением эффективности всего алгоритма / потока управления).вместо того, чтобы следовать простым эмпирическим правилам, таким как «предпочитать полиморфизм операторам переключателя» (или наоборот).
0 голосов
/ 18 мая 2018

Я не вижу "ошибки" в вашем скрипте. Хотя я действительно не поощряю тестирование производительности таким образом, я все же могу сказать пару вещей, основываясь на своей интуиции. У меня нет надежных, хорошо проверенных результатов с контрольными группами и т. Д., Поэтому принимайте все, что я говорю, с щепоткой соли.

Теперь для меня вполне нормально предположить, что первый вариант пожирает пыль второго, потому что в js есть несколько вещей, более дорогих, чем переменный доступ:

  • доступ к свойству объекта (предположительно O (1) хеш-таблица, но все же медленнее, чем доступ к переменной)
  • вызов функции

Если мы посчитаем вызов функции и доступ к объекту:

  • первый случай: 5 вызовов [к getImportantValuePolymorphism] x (1 доступ к объекту [к myClass] + 1 вызов функции [к GetImportantValue] ===> ВСЕГО из 10 вызовов функции + 5 объектов доступ
  • второй случай: 5 вызовов [к getImportantValueSwitch] + 5 доступ к объекту [к MyClassEnum] ===> ВСЕГО 5 вызовов функции + 5 доступ к объекту

Еще одна вещь, которую стоит упомянуть: в первом случае у вас есть функция, которая вызывает другую функцию, поэтому вы получаете цепочку областей видимости . Чистый эффект от этого незначителен, но все же отрицательно сказывается на производительности.

Если учесть все вышеперечисленные факторы, сначала будет медленнее. А сколько? На это непросто ответить, так как это будет зависеть от реализаций поставщиков, но в вашем случае это примерно в 25 раз медленнее в Chrome. Если предположить, что у нас удвоено количество вызовов функций в первом случае и цепочка областей действия , можно было бы ожидать, что она будет в 2 или 3 раза медленнее, но не 25.

Я полагаю, что это экспоненциальное снижение производительности связано с тем, что истощает цикл обработки событий , что означает, что когда вы даете синхронную задачу js, так как она однопоточна, если задача громоздкий цикл обработки событий не может продолжаться и застревает на секунду или около того. Этот вопрос возникает, когда люди видят странное поведение setTimeout или других асинхронных вызовов при удалении от целевого периода времени. Это, как я уже сказал, из-за того, что предыдущая синхронная задача занимает слишком много времени. В вашем случае у вас есть синхронный цикл for, который повторяется 10 миллионов раз.

Чтобы проверить мою гипотезу, уменьшите 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 вызовов функций) против (5 вызовов функций + 5 доступ к объектам).

...