7

I wanted to extend String object prototype with some utility method. It worked, but the performance was surprisingly low. Passing a string to a function is 10x times faster than overriding the String.prototype method that is doing the same thing. To make sure this really happens I created a very simple count() function and the corresponding methods.

(I was experimenting, and created three different versions of the method.)

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')

Results:

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

As you can see the difference is dramatic.

The below proves that performance of method calls is neglectably slower, and that the function code it self is slower for methods.

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')

Results:

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

Although on huge repetitions (like 1e8) prototyped methods proves to be 10x times slower than functions, this can be ignored for this case.

All this may be related only to a String object, because simple generic objects perform about the same when you pass them to functions or call their methods:

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')

Results:

func: 1689.942ms
method: 1674.639ms

I've been searching on Stackoverflow and binging, but other that the recommendation "do not extend String or Array because you pollute the name space" (which is not a problem for my particular project), I cannot find anything related to performance of methods compared to functions. So should I simply forget about extending the String object due to performance drop of added methods or there is more about it?

exebook
  • 32,014
  • 33
  • 141
  • 226
  • Thanks for your information but, you're last implementation is far away from `prototype` you can replace object with function and then add prototype method. then get a new instance of it. BTW result is the same. – Morteza Tourani Jul 16 '16 at 05:37

1 Answers1

10

This is most likely because you are not using strict mode, and the this value inside your method is getting coerced to a String instance instead of being a primitive string. This coercion, and further method calls or property accesses on the String object, are slower than using primitive values.

You can (Edit: could, at least, in 2016) confirm this by repeating your measurement on var STR = new String('01101011…') which should have less overhead.

Then fix your implementation:

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;
};
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • 1
    It works, but does not make sense to me! Thus I added a new question: http://stackoverflow.com/questions/38411552/why-use-strict-improves-performance-10x-in-this-example – exebook Jul 16 '16 at 13:10
  • 4
    @exebook: As I said, sloppy mode casts the `this` value to an object, so it needs to create a `String` instance on every call, which is quite some overhead for such a simple method as yours – Bergi Jul 16 '16 at 15:35
  • @Bergi I don't understand why the following doesn't fix the problem: `const testString = new String('Hello World!'); for (let i = 0; i < 11111111; i++) testString.hash();` Would you be able to add to your answer? See [live example](https://jsfiddle.net/mendesjuan/31z27uef/) – Ruan Mendes Dec 29 '21 at 13:37
  • @JuanMendes Maybe it's just that `charCodeAt` in [this code](https://stackoverflow.com/q/70513684/1048572) is slower on string objects than on strings? I swear that in 2016, calling `new String` upfront did make a difference for accessing `this[i]`. – Bergi Dec 29 '21 at 14:03
  • @Bergi Thanks, just trying to make sure I wasn't missing anything. Maybe update the answer to explain that the only way to fix it is to "use strict"? I tried a million things to prevent this problem by boxing the string myself or by calling the prototype methods directly and it's always slow. – Ruan Mendes Dec 29 '21 at 15:11