Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

Ticket #11264 (closed enhancement: fixed)

Opened 2 years ago

Last modified 2 years ago

Late binding for contextual iterators

Reported by: Ben Assigned to: sam
Priority: normal Milestone:
Component: Prototype Version: edge
Severity: normal Keywords: iterator enumerable context bind
Cc:

Description

Many of the Enumerable methods accept a context argument. I suppose this interface feels more natural to some, since it allows the invocation of bind to be hidden inside the Enumerable method. In any case, I don't want to argue against the wisdom of context arguments. Since context arguments are here to stay, this patch aims to improve how they are handled.

To understand what I'm getting at, consider Enumerable#eachSlice:

eachSlice: function(number, iterator, context) {
  iterator = iterator ? iterator.bind(context) : Prototype.K;
  var index = -number, slices = [], array = this.toArray();
  while ((index += number) < array.length)
    slices.push(array.slice(index, index+number));
  return slices.collect(iterator, context);
}

eachSlice binds iterator to the given context, but it does not explicitly call the iterator. Instead, it passes the newly bound iterator and (useless) context to slices.collect, another Enumerable method. Here's Enumerable#collect:

collect: function(iterator, context) {
  iterator = iterator ? iterator.bind(context) : Prototype.K;
  var results = [];
  this.each(function(value, index) {
    results.push(iterator(value, index));
  });
  return results;
}

See what's wrong here? The already-bound iterator gets bound yet again, so that the operation performed on every value-index pair requires three function calls instead of one. At a minimum one might hope to avoid the second bind, but in fact we can do better.

First of all, since eachSlice doesn't actually use iterator or context except to pass them on to slices.collect, there's no reason to call iterator.bind(context) in eachSlice. Not very exciting, but definitely an improvement:

eachSlice: function(number, iterator, context) {
  var index = -number, slices = [], array = this.toArray();
  while ((index += number) < array.length)
    slices.push(array.slice(index, index+number));
  return slices.collect(iterator, context);
}

Now what about collect? First let me say that I love Function#bind. It's an elegant solution in a lot of use cases. Here, however, I think it's overkill. The iterator.bind(context) call serves only to fix the context of the iterator. None of the currying functionality of bind is needed. For such a minimal use case, the built-in Function#call is more appropriate (i.e., both prettier and faster):

collect: function(iterator, context) {
  iterator = iterator || Prototype.K;
  var results = [];
  this.each(function(value, index) {
    results.push(iterator.call(context, value, index));
  });
  return results;
}

With these changes, two unnecessary function calls are saved each time the iterator is invoked against a value-index pair. Plus, the code is shorter. Great success.

If you look at my patch, you'll see that similar changes have been applied wherever a context argument is supported. I will grant that the eachSlice/collect case is extreme, but only just. Any opportunity to avoid binding an iterator is a chance to save at least one nested function call for every invocation of the iterator.

Furthermore and finally:

  • If the purpose of context arguments is to avoid using bind unnecessarily, then we should avoid using bind unnecessarily. That's a large part of what this patch accomplishes.
  • Any object that supports a call method can now be used as an iterator. This insight is actually what got me started thinking about the patch.
  • All the Array and Enumerable unit tests still pass.

This ticket began with misgivings about context arguments, but it turned into an argument in their favor. Fancy that :)

Best, Ben

Attachments

late_binding.diff (5.2 kB) - added by Ben on 03/02/08 20:14:45.
Using Function#call instead of Function#bind to achieve late binding of iterator contexts
enumerable_benchmarks.html (2.2 kB) - added by samleb on 03/03/08 08:54:07.
Some benchmarks (goes into test/unit)

Change History

03/02/08 20:14:45 changed by Ben

  • attachment late_binding.diff added.

Using Function#call instead of Function#bind to achieve late binding of iterator contexts

03/02/08 20:46:07 changed by Ben

Quick example of my "Any object..." claim:

>>> String.prototype.call = function(self, p) { return self[p] }
function()
>>> s = 'abcde'
"abcde"
>>> [1, 3].map(s, s)
["b", "d"]

03/02/08 22:11:24 changed by foca

+1

Definitely worth it to speed up Enumerable

03/02/08 23:38:04 changed by mislav

+1

great work

03/03/08 08:54:07 changed by samleb

  • attachment enumerable_benchmarks.html added.

Some benchmarks (goes into test/unit)

03/03/08 08:56:06 changed by samleb

I added some benchmarks which reveals that patched Enumerable#each is slighty slower when no context argument is given, but can be 10 times faster when context is present.

+1

03/03/08 17:45:25 changed by Ben

Thanks samleb. I put your benchmarks up here, taking liberty to change the number iterations to 1000 for more consistent results:

http://wrybanter.com/prototype/test/unit/enumerable_benchmarks.html

Also, I created this comparison page for the prototype-core list yesterday, not knowing about the benchmark tool in the unit test library:

http://wrybanter.com/prototype/test/compare.html

Safari seems to benefit the most in both cases, though Firefox and IE also see appreciable gains in most circumstances (never significant losses).

03/03/08 18:09:02 changed by Ben

Oops, I was using my patched prototype.js for both tests! Now that I've fixed that problem, the enumerable_benchmarks results certainly do match up with samleb's appraisal.

03/09/08 06:56:04 changed by tobie

  • status changed from new to closed.
  • resolution set to fixed.

(In [8997]) prototype: Performance improvements for Enumerables. Closes #11264.