Увеличение производительности String.prototype показывает, что вызовы функций в 10 раз быстрее

Я хотел бы расширить прототип объекта String с помощью некоторого метода утилиты. Это сработало, но производительность была удивительно низкой. Передача строки в функцию в 10 раз быстрее, чем переопределение метода String.prototype, который делает то же самое. Чтобы убедиться, что это действительно происходит, я создал очень простую функцию count() и соответствующие методы.

(я экспериментировал и создал три разные версии метода.)

function count(str, char) {
    var n = 0;
    for (var i = 0; i < str.length; i++) if (str[i] == char) n++;
    return n;
}

String.prototype.count = function (char) {
    var n = 0;
    for (var i = 0; i < this.length; i++) if (this[i] == char) n++;
    return n;
}

String.prototype.count_reuse = function (char) {
    return count(this, char)
}

String.prototype.count_var = function (char) {
    var str = this;
    var n = 0;
    for (var i = 0; i < str.length; i++) if (str[i] == char) n++;
    return n;
}

// Here is how I measued speed, using Node.js 6.1.0

var STR ='0110101110010110100111010011101010101111110001010110010101011101101010101010111111000';
var REP = 1e3//6;

console.time('func')
for (var i = 0; i < REP; i++) count(STR,'1')
console.timeEnd('func')

console.time('proto')
for (var i = 0; i < REP; i++) STR.count('1')
console.timeEnd('proto')

console.time('proto-reuse')
for (var i = 0; i < REP; i++) STR.count_reuse('1')
console.timeEnd('proto-reuse')

console.time('proto-var')
for (var i = 0; i < REP; i++) STR.count_var('1')
console.timeEnd('proto-var')

Результаты:

func: 705 ms
proto: 10011 ms
proto-reuse: 10366 ms
proto-var: 9703 ms

Как вы видите, разница очень драматична.

Ниже показано, что производительность вызовов методов пренебрежимо медленнее и что код функции он сам медленнее для методов.

function count_dummy(str, char) {
    return 1234;
}

String.prototype.count_dummy = function (char) {
    return 1234; // Just to prove that accessing the method is not the bottle-neck.
}

console.time('func-dummy')
for (var i = 0; i < REP; i++) count_dummy(STR,'1')
console.timeEnd('func-dummy')

console.time('proto-dummy')
for (var i = 0; i < REP; i++) STR.count_dummy('1')
console.timeEnd('proto-dummy')

console.time('func-dummy')
for (var i = 0; i < REP; i++) count_dummy(STR,'1')
console.timeEnd('func-dummy')

Результаты:

func-dummy: 0.165ms
proto-dummy: 0.247ms

Хотя при огромных повторениях (например, 1e8) прототипные методы оказываются в 10 раз более медленными, чем функции, это может быть проигнорировано для этого случая.

Все это может быть связано только с объектом String, потому что простые общие объекты выполняют примерно то же самое, когда вы передаете их функциям или вызываете их методы:

var A = { count: 1234 };

function getCount(obj) { return obj.count }

A.getCount = function() { return this.count }

console.time('func')
for (var i = 0; i < 1e9; i++) getCount(A)
console.timeEnd('func')

console.time('method')
for (var i = 0; i < 1e9; i++) A.getCount()
console.timeEnd('method')

Результаты:

func: 1689.942ms
method: 1674.639ms

Я искал Stackoverflow и binging, но другие, что рекомендация "не расширяет String или Array, потому что вы загрязняете пространство имен" (что не является проблемой для моего конкретного проекта), я не могу найти ничего, связанного с выполнение методов по сравнению с функциями. Так что я должен просто забыть о расширении объекта String из-за падения производительности добавленных методов или об этом больше?

Ответ 1

Это, скорее всего, потому, что вы не используете строгий режим, а значение this внутри вашего метода должно быть добавлено к экземпляру String вместо примитивной строки.

Вы можете подтвердить это, повторив измерения на var STR = new String('01101011…').

Затем исправьте свою реализацию:

String.prototype.count = function (char) {
    "use strict";
    var n = 0;
    for (var i = 0; i < this.length; i++)
        if (this[i] == char)
            n++;
    return n;
};