Skip to content

Instantly share code, notes, and snippets.

@briancavalier
Last active August 29, 2015 14:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save briancavalier/7bf084aa017b46c3eac3 to your computer and use it in GitHub Desktop.
Save briancavalier/7bf084aa017b46c3eac3 to your computer and use it in GitHub Desktop.

This is a simple micro-benchmark of four ways to curry JavaScript functions. The benchmark focuses on measuring the performance of the resulting curried functions, not on the performance of currying the functions in the first place. That'd also be an interesting comparison, especially since two of these approaches rely on dynamic compilation at curry-time. Typically, though, functions should be curried once at load/init/startup time, so the runtime performance of the resulting curried functions is typically more important to the overall application.

Note: I'm not a micro-benchmarking expert, so I might be doing something dumb! If you see a problem, please suggest a fix :)

Approaches

unscriptable dynamic compile

This one was taken from @unscriptable's gist that uses dynamic compilation to generate the final function in the curry chain.

briancavalier dynamic compile

This is a variant of @unscriptable's approach that uses an iife to capture the original function in the compilation step. It has the potentially useful feature of being able to name the final function, which could be helpful when debugging.

briancavalier args.length dispatch

This is one I wrote a while back that uses a switch to avoid the notoriously slow Function.prototype.apply up to a certain arity--I picked 5 because that's what I needed when I wrote it. If you didn't care about thisArg, it could avoid Function.prototype.call as well, but that's much less of a perf hit than apply.

lodash

This one uses lodash.curry, which seems to take a slightly different approach from all of the above. It's always good to see how you're doing perf-wise vs. anything @jdalton touches :)

Results

The results captured here are copied from just one run. It's representative of the relative performance of each, but you should feel free to run the tests yourself :)

Potentially interesting observations

  1. It seems clear that the total number of function calls involved in each test is the dominating factor.
  2. The dynamic compilation makes a difference, but not a huge difference over args.length dispatch.
    • It's likely that dynamic compilation is much slower at curry-time
  3. The fast concat helper for accumulating args makes a huge difference over using Array.prototype.slice and Array.prototype.concat.

Note on lodash

I don't have an explanation for lodash's slow times for the add(1)(2)(3) and add(1,2)(3) tests, as I didn't fully grok the algorithm it uses. I could be doing something wrong, so please let me know and I'll correct it!

1000000 iterations
---------------------------------------------
uncurried
---------------------------------------------
100 add(1,2,3)
---------------------------------------------
unscriptable dynamic compile
---------------------------------------------
184 add(1,2,3)
190 add(1,2) then many add(3)
362 add(1)(2)(3)
279 add(1,2)(3)
---------------------------------------------
briancavalier args.length dispatch
---------------------------------------------
182 add(1,2,3)
190 add(1,2) then many add(3)
371 add(1)(2)(3)
288 add(1,2)(3)
---------------------------------------------
briancavalier dynamic compile
---------------------------------------------
173 add(1,2,3)
190 add(1,2) then many add(3)
350 add(1)(2)(3)
265 add(1,2)(3)
---------------------------------------------
lodash
---------------------------------------------
241 add(1,2,3)
454 add(1,2) then many add(3)
8527 add(1)(2)(3)
5077 add(1,2)(3)
// Using _.curry for comparison
var _ = require('lodash');
// Function to curry for testing
function add(x, y, z) {
return x + y + z;
}
var i, n = 1000000;
console.log(n + ' iterations');
//-----------------------------------------------------------------
// Uncurried control
// Run twice for JIT warm-up
//-----------------------------------------------------------------
console.log('---------------------------------------------');
console.log('uncurried');
console.log('---------------------------------------------');
var r = new Array(n);
for(i=0; i<r.length; ++i) {
r[i] = add(1,2,3);
}
r = new Array(n);
var start = Date.now();
for(i=0; i<r.length; ++i) {
r[i] = add(1,2,3);
}
console.log(Date.now() - start + '\tadd(1,2,3)');
//-----------------------------------------------------------------
// Compare four approaches
//-----------------------------------------------------------------
testCurry('unscriptable dynamic compile', curry3(add));
testCurry('briancavalier dynamic compile', curry1(add));
testCurry('briancavalier args.length dispatch', curry2(add));
testCurry('lodash', _.curry(add));
//-----------------------------------------------------------------
// unscriptable dynamic compile
// https://gist.github.com/unscriptable/bb9f62add400fa45af61
//-----------------------------------------------------------------
function curry3 (f) {
var arity = f.length;
var params = [];
var end = createEnd3(f, arity);
return createCurried3(params, arity, end);
}
function createEnd3 (f, arity) {
var src = 'return f(';
for (var i = 0; i < arity; i++) {
src += (i ? ', p[' : 'p[') + i + ']';
}
src += ');';
var endCall = new Function ('f', 'p', src);
return function end (p) {
return endCall(f, p);
};
}
function createCurried3 (collected, arity, end) {
return function () {
var params = concat(collected, arguments);
return params.length < arity
? createCurried3(params, arity, end)
: end(params);
};
}
//-----------------------------------------------------------------
// briancavalier dynamic compile
// https://gist.github.com/briancavalier/d2385e8fbca53c8d056b
//-----------------------------------------------------------------
function curry1 (f) {
var arity = f.length;
var params = [];
var end = createEnd(f, arity);
return createCurried(params, arity, end);
}
function createEnd (f, arity) {
var src = 'return function ' + f.name + '_curried (args) { return f(';
for (var i = 0; i < arity; i++) {
src += (i ? ', args[' : 'args[') + i + ']';
}
src += '); };';
return (new Function ('f', src)(f));
}
function createCurried (collected, arity, end) {
return function () {
var params = concat(collected, arguments);
return params.length < arity
? createCurried(params, arity, end)
: end(params);
};
}
//-----------------------------------------------------------------
// briancavalier args.length dispatch
//-----------------------------------------------------------------
function curry2(f, arity) {
var a = arguments.length > 1 ? arity : f.length;
return a < 2 ? f : curryArity(f, a, []);
}
function curryArity(f, arity, args) {
return function() {
var accum = concat(args, arguments);
return accum.length < arity
? curryArity(f, arity, accum)
: runCurried(f, accum, this);
};
}
function runCurried(f, args, thisArg) {
switch(args.length) {
// Currying a 0 or 1-arg function would be useless
case 2: return f.call(thisArg, args[0], args[1]);
case 3: return f.call(thisArg, args[0], args[1], args[2]);
case 4: return f.call(thisArg, args[0], args[1], args[2], args[3]);
case 5: return f.call(thisArg, args[0], args[1], args[2], args[3], args[4]);
default:
return f.apply(thisArg, args);
}
}
//-----------------------------------------------------------------
// fast array concat helper
//-----------------------------------------------------------------
function concat(a, b) {
var al = a.length;
var bl = b.length;
var c = new Array(al + bl);
var i;
for(i=0; i<al; ++i) {
c[i] = a[i];
}
for(i=0; i<bl; ++i) {
c[i+al] = b[i];
}
return c;
}
//-----------------------------------------------------------------
// test runner
// Each test is repeated to account for JIT warm-up. Only the
// second is timed.
//-----------------------------------------------------------------
function testCurry(name, f) {
console.log('---------------------------------------------');
console.log(name);
console.log('---------------------------------------------');
var r, i, start;
// Test 1: No partial application
// This test is important since it's desirable for currying to have
// a minimal impact on "regular" function calls in JS.
r = new Array(n);
for(i=0; i<r.length; ++i) {
r[i] = f(1,2,3);
}
r = new Array(n);
start = Date.now();
for(i=0; i<r.length; ++i) {
r[i] = f(1,2,3);
}
console.log(Date.now() - start + '\tadd(1,2,3)');
// Test 2: Partially apply all args except last
// This is a primary use case for currying, ie to a produce a one-arg
// function which is then called many times.
var partiallyApplied = f(1,2);
r = new Array(n);
for(i=0; i<r.length; ++i) {
r[i] = partiallyApplied(3);
}
r = new Array(n);
start = Date.now();
for(i=0; i<r.length; ++i) {
r[i] = partiallyApplied(3);
}
console.log(Date.now() - start + '\tadd(1,2) then many add(3)');
// Test 3: Individually apply each arg
// You'll likely never do this, but it's a worthwhile perf test, esp
// in comparison to the first test above (ie fully applying the function
// in one go)
r = new Array(n);
for(i=0; i<r.length; ++i) {
r[i] = f(1)(2)(3);
}
r = new Array(n);
start = Date.now();
for(i=0; i<r.length; ++i) {
r[i] = f(1)(2)(3);
}
console.log(Date.now() - start + '\tadd(1)(2)(3)');
// Test 4: Partially apply all args except the last, then immediately
// provide the last. Somewhat similar to tests 2 and 3
r = new Array(n);
for(i=0; i<r.length; ++i) {
r[i] = f(1,2)(3);
}
r = new Array(n);
start = Date.now();
for(i=0; i<r.length; ++i) {
r[i] = f(1,2)(3);
}
console.log(Date.now() - start + '\tadd(1,2)(3)');
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment