#215 ✓ resolved
Juriy Zaytsev

Optimize #bind/#bindAsEventListener

Reported by Juriy Zaytsev | July 9th, 2008 @ 11:11 PM | in 1.6.0.3

Minor optimization of Function#bind. Since 90% of the time, #bind is not used to "prefill" arguments (but only to set "context"), a combination of Array.prototype.concat and $A could be simply avoided. This performance boost should affect the entire library as #bind is one of the "core" methods.

Comments and changes to this ticket

  • Juriy Zaytsev

    Juriy Zaytsev July 9th, 2008 @ 11:12 PM

    • State changed from “new” to “enhancement”
  • Juriy Zaytsev

    Juriy Zaytsev July 9th, 2008 @ 11:52 PM

    • Tag set to patched

    Here are some initial tests in Firefox 2:

    Function.prototype.bind_old = function() {
      if (arguments.length < 2 && Object.isUndefined(arguments[0])) { return this; } 
      var __method = this, args = $A(arguments), object = args.shift(); 
      return function () {
        return __method.apply(object, args.concat($A(arguments)));
      }; 
    }
    
    Function.prototype.bind_new = function () { 
      if (arguments.length < 2 && Object.isUndefined(arguments[0])) { return this; } 
      var __method = this, args = $A(arguments), object = args.shift(); 
      if (args.length) { 
        return function () {
          return __method.apply(object, args.concat($A(arguments)));
        }; 
      } 
      return function () {
        return __method.apply(object, arguments);
      }; 
    }
    
    var bound_old = (function(){ return; }).bind_old({});
    var bound_new = (function(){ return; }).bind_new({});
    
    console.time('old');
    for (var i=0; i<1000; i++ ) {
      bound_old();
    }
    console.timeEnd('old');
    
    console.time('new');
    for (var i=0; i<1000; i++ ) {
      bound_new();
    }
    console.timeEnd('new');
    
    // in FF2
    // old: 20ms;
    // new: 11ms;
    
  • Nick Stakenburg

    Nick Stakenburg July 10th, 2008 @ 12:16 AM

    Nice, great performance boost +1

    IE6: 2.4x faster
    IE7: 2.1x faster
    
  • Juriy Zaytsev

    Juriy Zaytsev July 10th, 2008 @ 01:29 AM

    Nice.

    I expected IE to be sensitive to "concat", but not to such extent : )

    This and "element.method() -> Element.method(@element)" overhaul will make things blazingly fast.

  • John-David Dalton

    John-David Dalton July 10th, 2008 @ 07:15 AM

    I totally dig.

    All my plus are belong to you.

  • GitHub Robot

    GitHub Robot July 17th, 2008 @ 10:23 PM

    • State changed from “enhancement” to “resolved”

    (from [76e6f9fa46e45c8131d549621684a3d473ba0653]) Optimize Function#bind and Function#bindAsEventListener to avoid using Array#concat when only the context argument is given. [#215 state:resolved]

    http://github.com/sstephenson/pr...

  • Ben Newman

    Ben Newman March 4th, 2009 @ 04:36 AM

    • Tag changed from patched to bind, bindaseventlistener, patched, performance

    Whatever happened to this?

    If it's an issue of applying cleanly to the master tip, I've attached an updated version (implemented accidentally before I saw this bug).

  • Juriy Zaytsev

    Juriy Zaytsev March 4th, 2009 @ 05:27 AM

    Ben,

    note that we can do even better by replacing apply with call and skipping arguments passing altogether if none are present (in both branches)

    arguments lookup is relatively expensive in some implementations (and there are optimizations applied when it is not present); call is also faster than apply (in FF, IIRC). Moreover, my original patch always does unnecessary concatenation (in a first branch) even when no arguments were passed to the function.

    IOW, if you want a truly efficient bind, use something like:

    
    Function.prototype.bind = (function(){
      
      var _slice = Array.prototype.slice;
      
      return function(context) {
        var fn = this,
            args = _slice.call(arguments, 1);
    
        if (args.length) { 
          return function() {
            return arguments.length
              ? fn.apply(context, args.concat(_slice.call(arguments)))
              : fn.apply(context, args);
          }
        } 
        return function() {
          return arguments.length
            ? fn.apply(context, arguments)
            : fn.call(context);
        }; 
      }
    })();
    
  • ibolmo

    ibolmo February 21st, 2010 @ 08:25 AM

    • Tag changed from bind, bindaseventlistener, patched, performance to patched, performance

    @Juriy

    How about this version:

    
    Function.prototype.bind = (function(){
        
    return function(context) {
        var fn = this, args = Array.prototype.slice.call(arguments, 1);
        return args.length ? function(){
            return fn.apply(context, arguments.length ? Array.prototype.concat.apply(args, arguments) : args);
        } : function(){
            return fn.apply(context, arguments);
        };
    };
     
    })();
    

    Even though it's less optimized, I think this is the best in terms of readability. If anyone is willing, can you compare these two against the above?

    
    Function.prototype.bind = function(context) {
        var fn = this, args = Array.prototype.slice.call(arguments, 1);
        return function(){
            return fn.apply(context, Array.prototype.concat.apply(args, arguments);
        };
    };
    
  • ibolmo

    ibolmo February 21st, 2010 @ 08:38 AM

    Looks like Juriy's is very fast.

    
    FFx 3.6
    old: 20ms
    old with arg: 30ms
    new: 12ms
    new with arg: 16ms
    newer: 6ms
    newer with arg: 21ms
    even newer: 13ms
    even newer with arg: 18ms
    

    newer is Juriy's
    even newer is mine

    Here's the bench:

    Function.prototype.bind_old = function() {
      if (arguments.length < 2 && Object.isUndefined(arguments[0])) { return this; } 
      var __method = this, args = $A(arguments), object = args.shift(); 
      return function () {
        return __method.apply(object, args.concat($A(arguments)));
      }; 
    }
    
    Function.prototype.bind_new = function () { 
      if (arguments.length < 2 && Object.isUndefined(arguments[0])) { return this; } 
      var __method = this, args = $A(arguments), object = args.shift(); 
      if (args.length) { 
        return function () {
          return __method.apply(object, args.concat($A(arguments)));
        }; 
      } 
      return function () {
        return __method.apply(object, arguments);
      }; 
    }
    
    Function.prototype.bind_newer = (function(){
      
      var _slice = Array.prototype.slice;
      
      return function(context) {
        var fn = this,
            args = _slice.call(arguments, 1);
    
        if (args.length) { 
          return function() {
            return arguments.length
              ? fn.apply(context, args.concat(_slice.call(arguments)))
              : fn.apply(context, args);
          }
        } 
        return function() {
          return arguments.length
            ? fn.apply(context, arguments)
            : fn.call(context);
        }; 
      }
    })();
    
    Function.prototype.bind_even_newer = function(context) {
        var fn = this, args = Array.prototype.slice.call(arguments, 1);
        return function(){
            return fn.apply(context, Array.prototype.concat.apply(args, arguments));
        };
    };
    
    var bound_old = (function(){ return; }).bind_old({});
    var bound_new = (function(){ return; }).bind_new({});
    var bound_newer = (function(){ return; }).bind_newer({});
    var bound_even_newer = (function(){ return; }).bind_even_newer({});
    
    console.time('old');
    for (var i=0; i<1000; i++ ) {
      bound_old();
    }
    console.timeEnd('old');
    
    console.time('old with arg');
    for (var i=0; i<1000; i++ ) {
      bound_old(0, 1, 2, 3, 4, 5);
    }
    console.timeEnd('old with arg');
    
    console.time('new');
    for (var i=0; i<1000; i++ ) {
      bound_new();
    }
    console.timeEnd('new');
    
    console.time('new with arg');
    for (var i=0; i<1000; i++ ) {
      bound_new(0, 1, 2, 3, 4, 5);
    }
    console.timeEnd('new with arg');
    
    console.time('newer');
    for (var i=0; i<1000; i++ ) {
      bound_newer();
    }
    console.timeEnd('newer');
    
    console.time('newer with arg');
    for (var i=0; i<1000; i++ ) {
      bound_newer(0, 1, 2, 3, 4, 5);
    }
    console.timeEnd('newer with arg');
    
    console.time('even newer');
    for (var i=0; i<1000; i++ ) {
      bound_even_newer();
    }
    console.timeEnd('even newer');
    
    console.time('even newer with arg');
    for (var i=0; i<1000; i++ ) {
      bound_even_newer(0, 1, 2, 3, 4, 5);
    }
    console.timeEnd('even newer with arg');
    
  • ibolmo

    ibolmo February 21st, 2010 @ 08:46 AM

    Sorry for the spamming, but this is interesting.

    With this change:

    
    Function.prototype.bind_even_newer = function(context) {
        var fn = this, args = Array.prototype.slice.call(arguments, 1);
        return function(){
            return fn.apply(context, args.length && Array.prototype.concat.apply(args, arguments) || arguments);
        };
    };
    

    You're almost at the same performance as Juriy's:

    run 1:
    newer: 6ms
    newer with arg: 15ms
    even newer: 10ms
    even newer with arg: 16ms
    
    run 2:
    newer: 6ms
    newer with arg: 14ms
    even newer: 9ms
    even newer with arg: 14ms
    
    run 3:
    newer: 7ms
    newer with arg: 20ms
    even newer: 10ms
    even newer with arg: 14ms
    

    As you can see for the empty arguments it's always 3-5 ms off. For the calls with arguments my latest version may perform a little better.

  • Andrea Giammarchi

    Andrea Giammarchi February 21st, 2010 @ 12:06 PM

    Hi there, as Juriy pointed out this page after my post, I have optimized and tested a bit more.
    @ibolmo , your test is incomplete because you are testing only one part and not the whole scenario.

    A bound function should be tested without extra arguments, with at least one of them, and it should be called without arguments, and with at least one of them.

    I would suggest to get rid of all your slower functions and create a better benchmark.
    For each implementation, you should perform 4 tests, using two different bound.

    var bound_base = function(){return;}.bind({});
    var bound_args = function(){return;}.bind({}, 1);
    
    
    console.time('bound_base_no_args'); for (var i=0; i<1000; i++ ) { bound_base(); } console.timeEnd('bound_base_no_args');
    console.time('bound_base_args'); for (var i=0; i<1000; i++ ) { bound_base(2); } console.timeEnd('bound_base_args');
    console.time('bound_args_no_args'); for (var i=0; i<1000; i++ ) { bound_args(); } console.timeEnd('bound_args_no_args');
    console.time('bound_args_args'); for (var i=0; i<1000; i++ ) { bound_args(2); } console.timeEnd('bound_args_args');

    As far as I can tell, my latest revision is the fastest one, at least in FF3.6

    This is the minified version:

    if(Function.prototype.bind==null){Function.prototype.bind=(function(a){function b(e){var d=this;if(1<arguments.length){var c=a.call(arguments,1);return function(){return d.apply(e,arguments.length?c.concat(a.call(arguments)):c)}}return function(){return arguments.length?d.apply(e,arguments):d.call(e)}}return b}(Array.prototype.slice))};
    

    In WebReflection you have the explained one:
    http://webreflection.blogspot.com/2010/02/functionprototypebind.html

    Regards

  • Andrea Giammarchi

    Andrea Giammarchi February 21st, 2010 @ 12:21 PM

    I hope this will help to obtain a better scenario against different implementation

    
    
    
    function bindBench(method, times){
    function f(){return};
    var bound_base = fmethod; var bound_args = f[method]({}, 1);
    console.time(method + ' base no args'); for (var i=times; i--; bound_base()); console.timeEnd(method + ' base no args'); console.time(method + ' base args'); for (var i=times; i--; bound_base(2)); console.timeEnd(method + ' base args');
    console.time(method + ' args no args'); for (var i=times; i--; bound_args()); console.timeEnd(method + ' args no args'); console.time(method + ' args args'); for (var i=times; i--; bound_args(2)); console.timeEnd(method + ' args args');
    }
    // example Function.prototype.bind_even_whathever = function () { };
    // the bench bindBench('bind_even_whathever', 1000);

    With bindBench you can test everything you have already there and in a more consistent way. I would suggest to change 1000 iterations with at least 10000 since 1000 is not truly showing differences while 10000 does, at least in my Atom based Netbook.

    Anyway, this is my result: Atom N270 Netbook and 1000 iteractions against my implementation

    bind base no args: 3ms
    bind base args: 9ms
    bind args no args: 4ms
    bind args args: 19ms
    
    while this is the Juriy one
    bind base no args: 4ms
    bind base args: 10ms
    bind args no args: 5ms
    bind args args: 27ms
    

    If interested I can provide more tests where it matters, low power CPU as mine is ;)

    Regards

  • Andrea Giammarchi

    Andrea Giammarchi February 21st, 2010 @ 12:29 PM

    Sorry guys, last one I forgot ... not only execution time is important, I do believe we are interested into bind performances itself, not only the result then.
    Please add this bench in the top:

    console.time(method + ' creation no args');
    for (var i=times; i--; f method );
    console.timeEnd(method + ' creation no args');
    console.time(method + ' creation args');
    for (var i=times; i--; f[ method ]( {}, 1 ));
    console.timeEnd(method + ' creation args');
    
    and discover that over 10000 iteractions my version, as example, performs 57ms and 195ms against Juriy 165ms and 255ms
    Since we all bind a lot, I guess we should consider these results as well.

    Regards

  • Phred

    Phred February 21st, 2010 @ 03:47 PM

    Comprehensive benchmarks (with pretty graphics):

    10,000 iterations

    100,000 iterations (Change the hash and refresh.)

    Also posted to github if you want to fork. (The code's quick and dirty. Sue me)

    In chromium, bind_newer is amazing, but it may be browser optimization related since the function isn't doing anything.

    Based on the numbers bind_newer looks the best on average for the base functions, base functions with args, and bound functions. "Bound functions with arguments" scores best with the bind_even_newer method consistently. It seems 1 call to Array#concat is faster than a call to Array#concat and Array#slice.

    EDIT:
    Results from Opera 10.5 (latest), IE6, FF3.6, Chromium 5

    bind_old (no args)                      25ms
    bind_old (with args)                    27ms
    bind_old (bound; no args)               24ms
    bind_old (bound with args)              28ms
    
    bind_new (no args)                      9ms
    bind_new (with args)                    9ms
    bind_new (bound; no args)               26ms
    bind_new (bound with args)              48ms
    
    bind_newer (no args)                    7ms
    bind_newer (with args)                  11ms
    bind_newer (bound; no args)             9ms
    bind_newer (bound with args)            30ms
    
    bind_even_newer (no args)               19ms
    bind_even_newer (with args)             21ms
    bind_even_newer (bound; no args)        22ms
    bind_even_newer (bound with args)       26ms
    
    bind_researched (no args)               8ms
    bind_researched (with args)             12ms
    bind_researched (bound; no args)        11ms
    bind_researched (bound with args)       25ms
    

    Based on all your code, I'd like to throw another into the ring. It's bind_newer using bind_even_newer's code for calling a bound function with more arguments.

    Function.prototype.bind_researched = (function(slice, concat) {
      return function(context) {
        var fn = this
            ,args = slice.call(arguments, 1)
        ;
    
        if (args.length) { 
          return function() {
            return arguments.length
              ? fn.apply(context, concat.apply(args, arguments))
              : fn.apply(context, args);
          }
        } 
        return function() {
          return arguments.length
            ? fn.apply(context, arguments)
            : fn.call(context);
        }; 
      }
    })(Array.prototype.slice, Array.prototype.concat);
    
  • Andrea Giammarchi

    Andrea Giammarchi February 21st, 2010 @ 08:42 PM

    @Phred

    var f = function(){

    return [].slice.call(arguments);
    

    }.bind_researched(null, [1,2], 3);

    alert(f([4,5], 6).join("\n"));

    1,2
    3
    4
    5
    6

    apply uses arguments as an array. Concat adds values or concatenates arrays, so it makes arguments flat and the function inconsistent.

    Moreover, bind is widely used in every code, the bind performances itself must be tested, as I have said in my last comment.

    $arguments.concat(slice.call(arguments))

    is the only way to concatenate real arguments, so please update your version and add bind execution time for both arguments and no arguments.

    This would be a complete test, and I kinda already know which one is the fastest :P

  • Andrea Giammarchi

    Andrea Giammarchi February 21st, 2010 @ 10:46 PM

    just noticed @ibolmo even_newer suffers same @Phred problem with concat.apply

    var f = function(){
    
    return [].slice.call(arguments);
    
    }.bind(null, [1,2], 3);
    
    alert(f([4,5], 6).join("\n"));
    

    should be:

    1,2
    3
    4,5
    6

    and not:
    1,2
    3
    4
    5
    6

    Regards

  • ibolmo

    ibolmo February 22nd, 2010 @ 05:51 PM

    @Andrea Great catch. I've come up with a fix to it, but unshift is much slower than slice:

    Function.prototype.bind_fixed_even_newer = function(context) {
        var fn = this, args = (arguments.length > 1) && Array.prototype.slice.call(arguments, 1);
        return function(){
            if (args) Array.prototype.unshift.apply(arguments, args);
            return arguments.length ? fn.apply(context, arguments) : fn.call(context);
        };
    };
    

    The above is the shortest out of all of our solutions, though.

    Here's my "final" submission. It's the closest I could get to you guys without doing curry.
    I'm still trailing you guys (http://grab.by/2ANj), but that's alright. Looks like slice and curry is just too fast.

    Function.prototype.bind_fixed_even_newer = function(context) {
        var fn = this, args = (arguments.length > 1) && Array.prototype.slice.call(arguments, 1);
        return function(){
            if (!args && !arguments.length) return fn.call(context);
            if (arguments.length && args) return fn.apply(context, args.concat(Array.prototype.slice.call(arguments)));
            return fn.apply(context, !args ? arguments : args);
        };
    };
    

Please Sign in or create a free account to add a new ticket.

With your very own profile, you can contribute to projects, track your activity, watch tickets, receive and update tickets through your email and much more.

New-ticket Create new ticket

Create your profile

Help contribute to this project by taking a few moments to create your personal profile. Create your profile »

The Prototype JavaScript library.

Shared Ticket Bins

Referenced by

Pages