扩展String.prototype性能表明函数调用速度快了10倍

7

我希望扩展String对象原型以添加一些实用方法。它可以工作,但性能却出奇的低。将字符串传递给函数比覆盖执行相同操作的String.prototype方法快10倍。为了确保这一点,我创建了一个非常简单的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和Bing上搜索过,但除了建议“不要扩展String或Array因为它们会污染命名空间”(对于我的特定项目来说这不是问题)之外,我没有找到与方法性能相比较的函数相关的内容。所以我应该忽略由于添加方法而导致的性能下降而放弃扩展String对象呢?还是有更多需要考虑的呢?


谢谢您的信息,但是您的最后一次实现离“原型”还有很大差距。您可以用函数替换对象,然后添加原型方法,再获取一个新实例。顺便说一下,结果是相同的。 - Morteza Tourani
1个回答

10
这很可能是因为您没有使用严格模式,在您的方法内部,this值被强制转换为一个String实例,而不是原始字符串。这种类型转换以及在String对象上进行的进一步方法调用或属性访问比使用原始值要慢。

您可以通过在var STR = new String('01101011…')上重复测量来确认这一点(至少在2016年之前是可以的),它应该拥有更小的开销。

然后修复您的实现:

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;
};

1
它能工作,但对我来说没有意义!因此我添加了一个新的问题:https://dev59.com/dFoT5IYBdhLWcg3wlgTn - exebook
4
就像我之前所说的,松散模式将'this'值转换为对象,因此需要在每次调用时创建一个'String'实例,对于像你这样的简单方法来说,这是相当大的开销。 - Bergi
@Bergi,我不明白为什么以下代码不能解决问题:const testString = new String('Hello World!'); for (let i = 0; i < 11111111; i++) testString.hash(); 你能否在你的答案中添加一些内容?请参见实时示例 - Ruan Mendes
@JuanMendes 可能只是因为 此代码 中的 charCodeAt 在字符串对象上比在字符串上慢?我发誓在2016年,预先调用 new String 对于访问 this[i] 确实有所帮助。 - Bergi
@Bergi 谢谢,我只是想确保我没有漏掉什么。也许更新答案以解释唯一修复它的方法是“使用严格模式”?我尝试了无数种方法来通过自己包装字符串或直接调用原型方法来防止这个问题,但总是很慢。 - Ruan Mendes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接