Skip to content

Performance improvements to function.js + "unwrap()" support #236

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
jwestbrook opened this issue Jun 5, 2014 · 23 comments
Open

Performance improvements to function.js + "unwrap()" support #236

jwestbrook opened this issue Jun 5, 2014 · 23 comments

Comments

@jwestbrook
Copy link
Collaborator

previous lighthouse ticket #599
by Robert Kieffer (broofa)


This is a followup to this (loooong) thread:

http://groups.google.com/group/prototype-scriptaculous/browse_thread/thread/1e62f11be350eb1d/734d8208ec4775b5?hl=en&

The attached patch updates the various methods in function.js to use the argument marchalling optimization discussed in the above thread. It also adds support for attaching a "_wrappedFunction" reference that is used by the (new) unwrap() method to provide access to the original function.

@jwestbrook
Copy link
Collaborator Author

Juriy Zaytsev
March 17th, 2009 @ 04:24 PM

Let's not mix perf. optimization with addition of unwrap. I, for one, would want to see its use case, before polluting Function.prototype with yet another method.
Also, l0 and l1 are really unfortunate names. It took me few seconds to figure out that there's actually no assignment of 10 to f : )

var f = l0 ? function(){
  ...
} : function(){
  ...
}

Something like this would be more clear:

if (boundArgsLen) {
  f = function(){ ... }
}
else {
  f = function(){ ... }
}

Also, how come there's no #1/#2 optimizations for anything other than bind?

@jwestbrook
Copy link
Collaborator Author

Tobie Langel
March 17th, 2009 @ 04:25 PM

Thanks kangax.
I'm uneasy about the unwrap support because it's not part of ES 3.1.
There's a important security reason for that, you might want to pass around a bound function while not allowing access to the unbound one.

@jwestbrook
Copy link
Collaborator Author

Tobie Langel
March 17th, 2009 @ 04:38 PM

Oops... thanks Robert!
Also, what's the cost of doing:

function bind(context) {
  if (arguments.length < 2 && Object.isUndefined(context)) return this;
  var __method = this, args = slice.call(arguments, 1), l0 = args.length;
  var f = l0 ? function() {
      args.length = l0;
      update(args, arguments);
      return __method.apply(context, args);
    } : function() {
      return arguments.length
        ? __method.apply(context, arguments)
        : __method.call(context);
    };
  f._wrappedFunction = this;
  return f;
}

@jwestbrook
Copy link
Collaborator Author

Robert Kieffer (broofa)
March 17th, 2009 @ 05:25 PM

New patch attached.
Tobie, Good point about the security issues with unwrap(). Hadn't occured to me. I've removed unwrap() support ( damn it! ;-) )
kangax, Re: handling case #1/#2 via apply .vs. switch, I think the only place it was missing was in curry(), right? All the other methods (where it might apply) are guaranteed to have non-zero-length args. I've fixed curry by refactoring the bind()/curry() code so curry just leverages the bind() implementation. I'm not sure what you two will think of the code though... let me know. :)
[BTW, it looks like this change adds ~120 bytes to the final size of prototype.js when gzip'ed]

@jwestbrook
Copy link
Collaborator Author

Robert Kieffer (broofa)
March 17th, 2009 @ 05:27 PM

kangax: regarding cost of update(), I believe i addressed that in http://groups.google.com/group/prototype-scriptaculous/msg/ec9ddbaa20b7fe76?hl=en

@jwestbrook
Copy link
Collaborator Author

Tobie Langel
March 17th, 2009 @ 05:38 PM

Could you possibly try it without your modified implementation, just resetting the args array like in the example above?
Perf costs might be due to variable look-ups rather than function call.

@jwestbrook
Copy link
Collaborator Author

Robert Kieffer (broofa)
March 17th, 2009 @ 05:42 PM

whups, meant that last comment for Tobie.
Also, forgot to address kangax's comment about l0/l1 naming. Uhm... yeah. The fact the "l" looks like a one (1) doesn't help either, does it? :P
We could do "len0" and "len1" I guess. I don't have strong opinions, other than it seemed like a good idea to keep the names short since this particular pattern was likely to be appear in more than one spot.

@jwestbrook
Copy link
Collaborator Author

Juriy Zaytsev
March 17th, 2009 @ 06:00 PM

@broofa
#delay can curry arguments as well, so it could benefit from optimization.
It currently looks like:

function delay(timeout) { 
    var __method = this, args = slice.call(arguments, 1);
    timeout = timeout * 1000
    return window.setTimeout(function() {
      return __method.apply(__method, args);
    }, timeout);
  }

And could be optimized easily like so:

function delay(timeout) {
    var fnOrig = this, 
        fnBound,
        args = slice.call(arguments, 1);

    timeout = timeout * 1000

    if (args.length) {
      fnBound = function() {
        return fnOrig.apply(fnOrig, args);
      }
    }
    else {
      fnBound = function() {
        return fnOrig.call(fnOrig);
      }
    }
    return window.setTimeout(fnBound, timeout);
  }

Even better, since I don't see a reason to call receiver function in a context of itself (why do we even do it in the first place?), simply alias fnOrig to fnBound:

function delay(timeout) {
  var fnOrig = this,
      args = slice.call(arguments, 1);

  timeout = timeout * 1000;

  var fnBound = args.length
    ? function(){ return fnOrig.apply(fnOrig, args) }
    : fnOrig

  return window.setTimeout(fnBound, timeout);
}

This way, if you don't pass arguments when calling delay there's no unnecessary burden of creating a wrapper function only to invoke slow apply on original function with an empty array. fn.delay(1) gets practically equivalent to setTimeout(fn, 1000).

@jwestbrook
Copy link
Collaborator Author

Robert Kieffer (broofa)
March 17th, 2009 @ 06:25 PM

Okay, updated the performance test page. http://www.broofa.com/Tools/JSLitmus/tests/PrototypeBind2.html It now has the following tests: * "Kangax" - As before * "Patch" - Version from last patch I submitted () * "custom update()" - Was "Less Improved". Uses a custom update() implementation. * "trunk's update()" - This is your implementation, Tobie. Uses current trunk version of update().
(
) I removed the "useThis" support from the patch version because it has an adverse affect on performance (due to the extra closure var reference?)
Here's the results I get: * IE: http://tinyurl.com/dxes2h * FF: http://tinyurl.com/dhe5cd * Safari: http://tinyurl.com/cul9v3 * Opera: http://tinyurl.com/cpylno * Chrome: http://tinyurl.com/cw4wrv
It looks like both Firefox and IE suffer a bit from the extra method call. Especially Firefox.

@jwestbrook
Copy link
Collaborator Author

Tobie Langel
March 17th, 2009 @ 07:02 PM

Even better, since I don't see a reason to call receiver function in a context of itself (why do we even do it in the first place?)

Check with Andrew, but if we don't bind it, scope will fall to the global scope, so that's a backward compatibility issue.

@jwestbrook
Copy link
Collaborator Author

Robert Kieffer (broofa)
March 17th, 2009 @ 07:05 PM

Re: delay(), I don't really see the point in having fnOrig as the context either. Not only is it rather arbitrary, it's inconsistent with what happens if you pass a function directly to setTimeout(). So I'd suggest the following for delay():

function delay(timeout) {
  var __method = this, args = slice.call(arguments, 1);

  return window.setTimeout(args.length ?
    function(){ return __method.apply(null, args) } : __method,
    timeout*1000);
}

'nuther patch file attached. (only 66 byte delta to prototype.js.gz this time :) )

@jwestbrook
Copy link
Collaborator Author

Tobie Langel
March 17th, 2009 @ 07:06 PM

We could do "len0" and "len1" I guess. I don't have strong opinions, other than it seemed like a good idea to keep the names short since this particular pattern was likely to be appear in more than one spot.

We have a habit of using meaningful names in Prototype. maybe: argLength and curriedArgLength

@jwestbrook
Copy link
Collaborator Author

Juriy Zaytsev
March 17th, 2009 @ 07:17 PM

Check with Andrew, but if we don't bind it, scope will fall to the global scope, so that's a backward compatibility issue.

Of course. This actually looks like an explicit measure to prevent accidental global pollution - bind function to itself rather than to a global scope :)
While it is a back-compat issue, I think it's very unlikely that someone is relying on context being that of a function object. I'm pretty sure people either work with already bound function, or do not rely on this in the first place.
Tobie, what do you think about delay change?
Side effect, of course, is that function will be called in a global context (just like setTimeout does), but we save on an extra closure by removing useless invocation with an empty array.

@jwestbrook
Copy link
Collaborator Author

Tobie Langel
March 17th, 2009 @ 07:21 PM

Tobie, what do you think about delay change?

I'd like to have Andrew's opinion.

@jwestbrook
Copy link
Collaborator Author

Tobie Langel
March 17th, 2009 @ 07:22 PM

FWIW, I don't think performance issues are really an issue on delayed calls.

@jwestbrook
Copy link
Collaborator Author

Robert Kieffer (broofa)
March 17th, 2009 @ 07:28 PM

FWIW, I don't think performance issues are really an issue on delayed calls.

Agreed. delay() is introducing a 0.01sec delay. I was gonna say something about this before but held off in the interest of getting a patch together that made you folks happy.
On a different note, is there a reason "method" is the only var prefixed with ""? Seems like it should be just, "method".

@jwestbrook
Copy link
Collaborator Author

Juriy Zaytsev
March 17th, 2009 @ 07:32 PM

@broofa
I would go with fn; method (in a sense of a property of an object) is not even correct at times.

@jwestbrook
Copy link
Collaborator Author

Robert Kieffer (broofa)
March 19th, 2009 @ 01:05 PM

So where are we at with this? Is another patch needed? If so, with what changes?
Kangax, you were going to talk to Andrew about the delay() context, right?

@jwestbrook
Copy link
Collaborator Author

Juriy Zaytsev
March 19th, 2009 @ 03:58 PM

@broofa
Last patch LGTM. The only thing I would add is a short comment about your optimization - avoidance of array creation - somewhere in the beginning of a functional "module".
Other than that, we're good to go :)
Tobie, Andrew?

@jwestbrook
Copy link
Collaborator Author

Robert Kieffer (broofa)
March 19th, 2009 @ 04:33 PM

Removed first two patch files since they've been superseded.
Added a patch file that's the same as the other one, but with "_method" renamed to "fn". Take your pick (I prefer the 'fn' name, fwiw)

@jwestbrook
Copy link
Collaborator Author

Andrew Dupont
March 19th, 2009 @ 04:54 PM

I doubt anyone is relying on the "default" scope of Function#delay, so I'm fine with that change.

@jwestbrook
Copy link
Collaborator Author

Robert Kieffer (broofa)
April 12th, 2009 @ 03:11 PM

'Hey, just noticed that these changes don't seem to be in the latest 1.6.1 RC (or git repo). Are these still planned for 1.6.1?

@jwestbrook
Copy link
Collaborator Author

Robert Kieffer (broofa)
September 6th, 2009 @ 04:09 PM

Revised patch to include inline documentation, and descriptive var names, as per this thread:
http://groups.google.com/group/prototype-core/browse_thread/thread/cf8c287e231a0192?hl=en

From 169405e9f62d01b6d8fdf8f9b9e19b21fb955d2e Mon Sep 17 00:00:00 2001
From: Robert Kieffer <[email protected]>
Date: Sun, 6 Sep 2009 07:04:18 -0700
Subject: [PATCH] performance improvements to bind() and related methods

---
 src/lang/function.js |   72 +++++++++++++++++++++++++++++++------------------
 1 files changed, 45 insertions(+), 27 deletions(-)

diff --git a/src/lang/function.js b/src/lang/function.js
index f535766..401613e 100644
--- a/src/lang/function.js
+++ b/src/lang/function.js
@@ -39,14 +39,28 @@ Object.extend(Function.prototype, (function() {
    *  specified by `object`.
   **/
   function bind(context) {
-    if (arguments.length < 2 && Object.isUndefined(arguments[0])) return this;
-    var __method = this, args = slice.call(arguments, 1);
-    return function() {
-      var a = merge(args, arguments);
-      return __method.apply(context, a);
-    }
-  }
+    // No args or object to bind to? just return the function
+    if (arguments.length < 2 && Object.isUndefined(context)) return this;
+
+    // Put bound arguments into 'args', which we'll reuse for every call rather
+    // than creating a new array each time.
+    var fn = this, args = slice.call(arguments, 1), curriedArgsLength = args.length;
+    return curriedArgsLength ? function() { // When there are bound arguments
+        var argsLength = arguments.length;

+        // Append arguments to the array of bound arguments.  (We do this
+        // inline to avoid function call overhead, and on a single line to make
+        // it easier to step through in a debugger)
+        args.length = curriedArgsLength + argsLength; while (argsLength--) args[curriedArgsLength+argsLength] = arguments[argsLength];
+        return fn.apply(context, args);
+      } : function() { // When there are no bound arguments
+        // Call fn() using fastest method possible (depending on # of arguments)
+        return arguments.length
+          ? fn.apply(context, arguments)
+          : fn.call(context);
+      };
+  }
+  
   /** related to: Function#bind
    *  Function#bindAsEventListener(object[, args...]) -> Function
    *  - object (Object): The object to bind to.
@@ -56,11 +70,14 @@ Object.extend(Function.prototype, (function() {
    *  executing.
   **/
   function bindAsEventListener(context) {
-    var __method = this, args = slice.call(arguments, 1);
+    // Like bind(), but adds 'event' argument. See bind() for implementation details
+    var fn = this, args = slice.call(arguments, 0), curriedArgsLength = args.length;
     return function(event) {
-      var a = update([event || window.event], args);
-      return __method.apply(context, a);
-    }
+      args[0] = event || window.event;
+      var argsLength = arguments.length;
+      args.length = curriedArgsLength + argsLength; while (argsLength--) args[curriedArgsLength+argsLength] = arguments[argsLength];
+      return fn.apply(context, args);
+    };
   }

   /**
@@ -73,12 +90,14 @@ Object.extend(Function.prototype, (function() {
    *  _and_ modify its execution scope at the same time.
   **/
   function curry() {
+    // Like bind(), but w/out bound object. See bind() for implementation details
     if (!arguments.length) return this;
-    var __method = this, args = slice.call(arguments, 0);
+    var fn = this, args = slice.call(arguments, 0), curriedArgsLength = args.length;
     return function() {
-      var a = merge(args, arguments);
-      return __method.apply(this, a);
-    }
+      var argsLength = arguments.length;
+      args.length = curriedArgsLength + argsLength; while (argsLength--) args[curriedArgsLength+argsLength] = arguments[argsLength];
+      return fn.apply(this, args);
+    };
   }

   /**
@@ -95,11 +114,11 @@ Object.extend(Function.prototype, (function() {
    *  [[Function#defer]].
   **/
   function delay(timeout) {
-    var __method = this, args = slice.call(arguments, 1);
-    timeout = timeout * 1000
-    return window.setTimeout(function() {
-      return __method.apply(__method, args);
-    }, timeout);
+    var fn = this, args = slice.call(arguments, 1);
+
+    return window.setTimeout(args.length
+      ? function() {return fn.apply(null, args);}
+      : fn, timeout*1000);
   }

   /**
@@ -130,11 +149,11 @@ Object.extend(Function.prototype, (function() {
    *  even preventing the original function from being called.
   **/
   function wrap(wrapper) {
-    var __method = this;
+    var fn = this;
     return function() {
-      var a = update([__method.bind(this)], arguments);
+      var a = update([fn.bind(this)], arguments);
       return wrapper.apply(this, a);
-    }
+    };
   }

   /**
@@ -146,10 +165,10 @@ Object.extend(Function.prototype, (function() {
   **/
   function methodize() {
     if (this._methodized) return this._methodized;
-    var __method = this;
+    var fn = this;
     return this._methodized = function() {
       var a = update([this], arguments);
-      return __method.apply(null, a);
+      return fn.apply(null, a);
     };
   }

@@ -162,6 +181,5 @@ Object.extend(Function.prototype, (function() {
     defer:               defer,
     wrap:                wrap,
     methodize:           methodize
-  }
+  };
 })());
-
-- 
1.5.6.5

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants