Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Invocation of javascript class methods is 20-40 times slower than same function

While benchmarking code in my library I came across very interesting and disappointing finding - it looks like invocation of class method is 20-40 times slower than of the same standalone function.

Shortly, for me it means - "for better performance - do not implement very-frequently called functionality via JS classes". Accessing library functions via "namespace object" also has performance penalty. That problem may affect approach to design a library.

I feel I'm missing something. Please let me know if there is something wrong in perf-tests below.

Here is summary of results, windows + intel 9600k (nodejs 14, Chrome 86, FF82)

          ops/s in:   node14 slow   Ch86 slow   FF82 slow
function(a, b)          4195        3934        1625
namespace.method(a, b)   210 x20    2702 x1.5   1305 x1.24
instance.method(a, b)    126 x33     162 x24      89 x18
prototype.method(a, b)   105 x40     154 x25      57 x28
class.method(a, b)       110 x38     153 x25      57 x28

Test script for https://benchmarkjs.com/, portions where also copy/pasted to https://jsbench.me/ for in-browser testing.

const Benchmark = require('benchmark');
const suite = new Benchmark.Suite("function-vs-method");
const nCycles = 1000000;

suite
    .add('function(a, b)', function () {
        function method(a, b) {
            return a + b;
        }

        for( let i = 0; i < nCycles; i++ ) {
            method(i, i)
        }
    })
    .add('namespace.method(a, b)', function () {
        function method(a, b) {
            return a + b;
        }

        let namespace = {}
        namespace.method = method

        for( let i = 0; i < nCycles; i++ ) {
            namespace.method(i, i)
        }
    })
    .add('instance.method(a, b)', function () {
        function method(a, b) {
            return a + b;
        }

        function Test() {}
        let instance = new Test()
        instance.method = method

        for( let i = 0; i < nCycles; i++ ) {
            instance.method(i, i)
        }
    })
    .add('prototype.method(a, b)', function () {
        Test.prototype.method = (a, b) => {
            return a + b;
        }

        function Test() {
        }

        let proto = new Test()
        for( let i = 0; i < nCycles; i++ ) {
            proto.method(i, i)
        }
    })
    .add('class.method(a, b)', function () {
        class Test {
            method(a, b) {
                return a + b;
            }
        }

        let c = new Test()
        for( let i = 0; i < nCycles; i++ ) {
            c.method(i, i)
        }
    })
    // add listeners
    .on('cycle', function(event) {
        console.log(String(event.target));
    })
    .on('complete', function() {
        console.log('Fastest is ' + this.filter('fastest').map('name'));
    })
    .run();

like image 660
Xtra Coder Avatar asked May 17 '26 11:05

Xtra Coder


1 Answers

You are including the initialization in your tests instead of just comparing the method/function call.

Example: You are comparing "declaring and calling a function" vs "declaring a class, instantiating a class object and calling a class mehtod".

If you extract the setups outside of the actual tests, the results look quite different.

Additionally, each run of your code with nCycles and the for loop yielded quite different results. I changed it to one invocation and now see constant results across test runs.

Disclaimer: I haven't done much benchmarking so far but wanted to try my best at an answer. @everyone please feel free to explain why I might be on the wrong track. @op please don't blindly trust this answer.

const Benchmark = require('benchmark');
const suite = new Benchmark.Suite('function-vs-method');

function method (a, b) {
  return a + b;
}

let namespace = {};
namespace.method = function (a, b) {
  return a + b;
};


function Test () {
}

let instance = new Test();
instance.method = function (a, b) {
  return a + b;
};


TestProto.prototype.method = function (a, b) {
  return a + b;
};

function TestProto () {
}

let proto = new TestProto();

class TestClass {
  method (a, b) {
    return a + b;
  }
}

let c = new TestClass();

suite
  .add('function(a, b)', function () {
    method(1, 2);
  })
  .add('namespace.method(a, b)', function () {
    namespace.method(1, 2);
  })
  .add('instance.method(a, b)', function () {
    instance.method(1, 2);
  })
  .add('prototype.method(a, b)', function () {
    proto.method(1, 2);
  })
  .add('class.method(a, b)', function () {
    c.method(1, 2);
  })
  // add listeners
  .on('cycle', function (event) {
    console.log(String(event.target));
  })
  .on('complete', function () {
    console.log('Fastest is ' + this.filter('fastest').map('name'));
  })
  .run();

Log:

function(a, b) x 1,395,900,922 ops/sec ±0.11% (93 runs sampled)
namespace.method(a, b) x 1,397,715,256 ops/sec ±0.06% (98 runs sampled)
instance.method(a, b) x 1,397,494,031 ops/sec ±0.08% (96 runs sampled)
prototype.method(a, b) x 1,395,546,437 ops/sec ±0.09% (98 runs sampled)
class.method(a, b) x 1,398,311,922 ops/sec ±0.07% (96 runs sampled)
Fastest is class.method(a, b),namespace.method(a, b),instance.method(a, b),function(a, b)
like image 195
Barthy Avatar answered May 19 '26 00:05

Barthy