Глубокая цепочка наследования замедляет поиск методов в движке JavaScript V8? - PullRequest
3 голосов
/ 12 июня 2019

Я пишу базовый класс для игры на TypeScript.Он получил такие функции, как отправка сообщений, управление ресурсами и т. Д. Вдохновленный Mixins , я написал следующий код (скомпилированный в JavaScript):

function Messenger(Base) {
    return class Messenger extends Base {
        $dispatch(e) {
           // TODO
        }
    };
}
function ResourceManager(Base) {
    return class ResourceManager extends Base {
        $loadRes(key) {
            // TODO
            return Promise.resolve({});
        }
    };
}
class Component {
}
class GameBase extends Component {
    start() {
        console.log('start');
    }
    init() {
        console.log('init');
    }
}
const Klass = ResourceManager(Messenger(GameBase));
var gg = new Klass();
gg.start();

Насколько я знаю, когда япопытайтесь вызвать gg.start, механизм JavaScript ищет цепочку прототипов, и она немного длиннее в этом случае, и она становится длинной события, когда растут миксины: method lookup

Isэто замедляет поиск метода?V8 оптимизировал этот процесс поиска и могу ли я просто игнорировать издержки поиска?

Ответы [ 2 ]

2 голосов
/ 12 июня 2019

V8 разработчик здесь.Это сложная проблема;краткий ответ - «это зависит».

Тривиально верно, что обход длинной цепочки прототипов при поиске занимает больше времени.Однако, если это делается только один или два раза, тогда это время, как правило, слишком мало, чтобы иметь значение.

Итак, следующий вопрос: как часто будет такой поиск?V8 пытается кэшировать результаты поиска всякий раз, когда это возможно (ищите термин «встроенные кэши», если хотите узнать больше);Эффективность такого кеширования, как и всего кеширования, критически зависит от количества различных наблюдаемых случаев.

Так что, если ваш код в основном "мономорфен" (т. е. при любом данном foo.bar поиске, foo всегдаимеют одинаковый тип / форму, включая одну и ту же цепочку прототипов) или полиморфную низкую степень (до четырех различных типов foo), тогда полный обход цепочки прототипов необходимо выполнить только один раз (или до четырех раз соответственно)), и после этого будут использоваться кэшированные результаты, поэтому, если вы выполняете такой код тысячи раз, вы не увидите разницы в производительности между цепочками прототипов, длина которых составляет один или сотни шагов.

Вклс другой стороны, если у вас есть загрузки свойств или хранилища, которые видят много разных типов (как это обычно бывает в определенных средах, где каждый отдельный поиск проходит через какую-то центральную функцию getProperty(object, property) { /* do some framework stuff, and then: */ return object[property]; }), то кэширование становится бесполезным, и V8 должен выполнитьполный поиск каждый раз.Это особенно медленно для длинных цепочек прототипов, но, тем не менее, оно всегда намного медленнее, чем кешируемые случаи (даже для коротких цепочек прототипов).

В заключение, если вы несколько осторожны в отношении общего проекта программы и избегаетеимея много разных типов в одних и тех же местах кода, вы можете легко позволить себе очень длинные цепочки прототипов.Фактически, сохранение как можно большей части вашего кода мономорфно типизированным имеет тенденцию оказывать значительно большее влияние, чем сохранение коротких цепочек прототипов.С другой стороны, более короткие длины прототипа действительно облегчают жизнь двигателя, и лично я бы сказал, что они могут (если вы не переусердствуете) также улучшить читаемость, так что при прочих равных условиях,Я бы посоветовал вам максимально упростить вашу объектную модель.

1 голос
/ 12 июня 2019

Я написал небольшой тест, чтобы увидеть, сколько будет стоить поиск по цепочке прототипов (будьте осторожны, он заблокирует ваш браузер при нажатии на «Выполнить фрагмент кода»; скорее, запустите его в Node локально):

function generateObjectWithPrototype(prototype) {
    const f = function() {};
    f.prototype = prototype;
    return new f();
}

const originalObject = new (function() {
    this.doSomething = function() {};
})();

let currentObject = originalObject;
for (let i = 0; i < 60001; i++) {
    currentObject = generateObjectWithPrototype(currentObject);
    const start = +new Date();
    currentObject.doSomething();
    const end = +new Date();
    if (i % 10000 === 0) {
        console.log(`Iteration ${i}: Took ${end - start}ms`);
    }
}

Результат:

Iteration 0: Took 0ms
Iteration 10000: Took 0ms
Iteration 20000: Took 1ms
Iteration 30000: Took 1ms
Iteration 40000: Took 2ms
Iteration 50000: Took 3ms
Iteration 60000: Took 4ms

Таким образом, в этом случае для глубины прототипа 60 000 дополнительное время, необходимое для нахождения метода doSomething(), составляет примерно 4 мс. Я бы сказал, что это пренебрежимо.

...