From 8577816234ac1d908eed8382c32b79db30884316 Mon Sep 17 00:00:00 2001 From: John-David Dalton Date: Sun, 15 Jul 2012 03:51:28 -0400 Subject: [PATCH] Add optimizations for large arrays to `_.difference`, `_.intersection`, and `_.without`. Former-commit-id: 26d55a6a3340e77b5269b2003d20def3fe77bca9 --- lodash.js | 55 ++++++++++-- perf/perf.js | 249 +++++++++++++++++++++++++++++++++++++++++++++++++-- test/test.js | 19 ++++ 3 files changed, 311 insertions(+), 12 deletions(-) diff --git a/lodash.js b/lodash.js index bc96d53fb..6d39dafc0 100644 --- a/lodash.js +++ b/lodash.js @@ -439,6 +439,41 @@ /*--------------------------------------------------------------------------*/ + /** + * Creates a new function optimized for searching large arrays for a given `value`, + * starting at `fromIndex`, using strict equality for comparisons, i.e. `===`. + * + * @private + * @param {Array} array The array to search. + * @param {Mixed} value The value to search for. + * @param {Number} [fromIndex=0] The index to start searching from. + * @param {Number} [largeSize=30] The length at which an array is considered large. + * @returns {Boolean} Returns `true` if `value` is found, else `false`. + */ + function cachedContains(array, fromIndex, largeSize) { + fromIndex || (fromIndex = 0); + + var length = array.length, + isLarge = (length - fromIndex) >= (largeSize || 30), + cache = isLarge ? {} : array; + + if (isLarge) { + // init value cache + var value, + index = fromIndex - 1; + + while (++index < length) { + value = array[index]; + (hasOwnProperty.call(cache, value) ? cache[value] : (cache[value] = [])).push(value); + } + } + return function(value) { + return isLarge + ? hasOwnProperty.call(cache, value) && indexOf(cache[value], value) > -1 + : indexOf(cache, value, fromIndex) > -1; + } + } + /** * Creates compiled iteration functions. The iteration function will be created * to iterate over only objects if the first argument of `options.args` is @@ -1224,10 +1259,11 @@ } var index = -1, length = array.length, - flattened = concat.apply(result, arguments); + flattened = concat.apply(result, arguments), + contains = cachedContains(flattened, length); while (++index < length) { - if (indexOf(flattened, array[index], length) < 0) { + if (!contains(array[index])) { result.push(array[index]); } } @@ -1390,12 +1426,15 @@ var value, index = -1, length = array.length, - others = slice.call(arguments, 1); + others = slice.call(arguments, 1), + cache = []; while (++index < length) { value = array[index]; if (indexOf(result, value) < 0 && - every(others, function(other) { return indexOf(other, value) > -1; })) { + every(others, function(other, index) { + return (cache[index] || (cache[index] = cachedContains(other)))(value); + })) { result.push(value); } } @@ -1847,10 +1886,11 @@ return result; } var index = -1, - length = array.length; + length = array.length, + contains = cachedContains(arguments, 1, 20); while (++index < length) { - if (indexOf(arguments, array[index], 1) < 0) { + if (!contains(array[index])) { result.push(array[index]); } } @@ -2774,7 +2814,8 @@ } } } - } else { + } + else { // objects with different constructors are not equivalent if ('constructor' in a != 'constructor' in b || a.constructor != b.constructor) { return false; diff --git a/perf/perf.js b/perf/perf.js index 3b7c18bec..f945ba866 100644 --- a/perf/perf.js +++ b/perf/perf.js @@ -95,7 +95,6 @@ belt = this.name == 'Lo-Dash' ? lodash : _; var index, - length, limit = 20, object = {}, objects = Array(limit), @@ -104,7 +103,7 @@ nestedNumbers = [1, [2], [3, [[4]]]], twoNumbers = [12, 21]; - for (index = 0, length = limit; index < length; index++) { + for (index = 0; index < limit; index++) { numbers[index] = index; object['key' + index] = index; objects[index] = { 'num': index }; @@ -132,7 +131,7 @@ funcNames = belt.functions(lodash); // potentially expensive - for (index = 0, length = this.count; index < length; index++) { + for (index = 0; index < this.count; index++) { bindAllObjects[index] = belt.clone(lodash); } } @@ -187,13 +186,50 @@ numbers2 = Array(limit), nestedNumbers2 = [1, [2], [3, [[4]]]]; - for (index = 0, length = limit; index < length; index++) { + for (index = 0; index < limit; index++) { numbers2[index] = index; object2['key' + index] = index; objects2[index] = { 'num': index }; } } + if (typeof multiArrays != 'undefined') { + var twentyFiveValues = Array(25), + twentyFiveValues2 = Array(25), + fiftyValues = Array(50), + fiftyValues2 = Array(50), + seventyFiveValues = Array(75), + seventyFiveValues2 = Array(75), + lowerChars = 'abcdefghijklmnopqrstuvwxyz'.split(''), + upperChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''); + + for (index = 0; index < 75; index++) { + if (index < 26) { + if (index < 20) { + twentyFiveValues[index] = lowerChars[index]; + twentyFiveValues2[index] = upperChars[index]; + } + else if (index < 25) { + twentyFiveValues[index] = + twentyFiveValues2[index] = index; + } + fiftyValues[index] = + seventyFiveValues[index] = lowerChars[index]; + + fiftyValues2[index] = + seventyFiveValues2[index] = upperChars[index]; + } + else { + if (index < 50) { + fiftyValues[index] = index; + fiftyValues2[index] = index + (index < 40 ? 75 : 0); + } + seventyFiveValues[index] = index; + seventyFiveValues2[index] = index + (index < 60 ? 75 : 0); + } + } + } + if (typeof template != 'undefined') { var tplData = { 'header1': 'Header1', @@ -489,6 +525,54 @@ }) ); + suites.push( + Benchmark.Suite('`_.difference` iterating 25 elements') + .add('Lo-Dash', { + 'fn': function() { + lodash.difference(twentyFiveValues, twentyFiveValues2); + }, + 'teardown': 'function multiArrays(){}' + }) + .add('Underscore', { + 'fn': function() { + _.difference(twentyFiveValues, twentyFiveValues2); + }, + 'teardown': 'function multiArrays(){}' + }) + ); + + suites.push( + Benchmark.Suite('`_.difference` iterating 50 elements') + .add('Lo-Dash', { + 'fn': function() { + lodash.difference(fiftyValues, fiftyValues2); + }, + 'teardown': 'function multiArrays(){}' + }) + .add('Underscore', { + 'fn': function() { + _.difference(fiftyValues, fiftyValues2); + }, + 'teardown': 'function multiArrays(){}' + }) + ); + + suites.push( + Benchmark.Suite('`_.difference` iterating 75 elements') + .add('Lo-Dash', { + 'fn': function() { + lodash.difference(seventyFiveValues, seventyFiveValues2); + }, + 'teardown': 'function multiArrays(){}' + }) + .add('Underscore', { + 'fn': function() { + _.difference(seventyFiveValues, seventyFiveValues2); + }, + 'teardown': 'function multiArrays(){}' + }) + ); + /*--------------------------------------------------------------------------*/ suites.push( @@ -725,6 +809,54 @@ }) ); + suites.push( + Benchmark.Suite('`_.intersection` iterating 25 elements') + .add('Lo-Dash', { + 'fn': function() { + lodash.intersection(twentyFiveValues, twentyFiveValues2); + }, + 'teardown': 'function multiArrays(){}' + }) + .add('Underscore', { + 'fn': function() { + _.intersection(twentyFiveValues, twentyFiveValues2); + }, + 'teardown': 'function multiArrays(){}' + }) + ); + + suites.push( + Benchmark.Suite('`_.intersection` iterating 50 elements') + .add('Lo-Dash', { + 'fn': function() { + lodash.intersection(fiftyValues, fiftyValues2); + }, + 'teardown': 'function multiArrays(){}' + }) + .add('Underscore', { + 'fn': function() { + _.intersection(fiftyValues, fiftyValues2); + }, + 'teardown': 'function multiArrays(){}' + }) + ); + + suites.push( + Benchmark.Suite('`_.intersection` iterating 75 elements') + .add('Lo-Dash', { + 'fn': function() { + lodash.intersection(seventyFiveValues, seventyFiveValues2); + }, + 'teardown': 'function multiArrays(){}' + }) + .add('Underscore', { + 'fn': function() { + _.intersection(seventyFiveValues, seventyFiveValues2); + }, + 'teardown': 'function multiArrays(){}' + }) + ); + /*--------------------------------------------------------------------------*/ suites.push( @@ -759,7 +891,6 @@ /*--------------------------------------------------------------------------*/ - suites.push( Benchmark.Suite('`_.isEqual` comparing primitives and objects (edge case)') .add('Lo-Dash', { @@ -1218,6 +1349,54 @@ }) ); + suites.push( + Benchmark.Suite('`_.union` iterating an array of 25 elements') + .add('Lo-Dash', { + 'fn': function() { + lodash.union(twentyFiveValues, twentyFiveValues2); + }, + 'teardown': 'function multiArrays(){}' + }) + .add('Underscore', { + 'fn': function() { + _.union(twentyFiveValues, twentyFiveValues2); + }, + 'teardown': 'function multiArrays(){}' + }) + ); + + suites.push( + Benchmark.Suite('`_.union` iterating an array of 50 elements') + .add('Lo-Dash', { + 'fn': function() { + lodash.union(fiftyValues, fiftyValues2); + }, + 'teardown': 'function multiArrays(){}' + }) + .add('Underscore', { + 'fn': function() { + _.union(fiftyValues, fiftyValues2); + }, + 'teardown': 'function multiArrays(){}' + }) + ); + + suites.push( + Benchmark.Suite('`_.union` iterating an array of 75 elements') + .add('Lo-Dash', { + 'fn': function() { + lodash.union(seventyFiveValues, seventyFiveValues2); + }, + 'teardown': 'function multiArrays(){}' + }) + .add('Underscore', { + 'fn': function() { + _.union(seventyFiveValues, seventyFiveValues2); + }, + 'teardown': 'function multiArrays(){}' + }) + ); + /*--------------------------------------------------------------------------*/ suites.push( @@ -1258,6 +1437,66 @@ /*--------------------------------------------------------------------------*/ + suites.push( + Benchmark.Suite('`_.without`') + .add('Lo-Dash', function() { + lodash.without(numbers, 9, 12, 14, 15); + }) + .add('Underscore', function() { + _.without(numbers, 9, 12, 14, 15); + }) + ); + + suites.push( + Benchmark.Suite('`_.without` iterating an array of 25 elements') + .add('Lo-Dash', { + 'fn': function() { + lodash.without.apply(lodash, [twentyFiveValues].concat(twentyFiveValues2)); + }, + 'teardown': 'function multiArrays(){}' + }) + .add('Underscore', { + 'fn': function() { + _.without.apply(_, [twentyFiveValues].concat(twentyFiveValues2)); + }, + 'teardown': 'function multiArrays(){}' + }) + ); + + suites.push( + Benchmark.Suite('`_.without` iterating an array of 50 elements') + .add('Lo-Dash', { + 'fn': function() { + lodash.without.apply(lodash, [fiftyValues].concat(fiftyValues2)); + }, + 'teardown': 'function multiArrays(){}' + }) + .add('Underscore', { + 'fn': function() { + _.without.apply(_, [fiftyValues].concat(fiftyValues2)); + }, + 'teardown': 'function multiArrays(){}' + }) + ); + + suites.push( + Benchmark.Suite('`_.without` iterating an array of 75 elements') + .add('Lo-Dash', { + 'fn': function() { + lodash.without.apply(lodash, [seventyFiveValues].concat(seventyFiveValues2)); + }, + 'teardown': 'function multiArrays(){}' + }) + .add('Underscore', { + 'fn': function() { + _.without.apply(_, [seventyFiveValues].concat(seventyFiveValues2)); + }, + 'teardown': 'function multiArrays(){}' + }) + ); + + /*--------------------------------------------------------------------------*/ + if (Benchmark.platform + '') { log(Benchmark.platform + ''); } diff --git a/test/test.js b/test/test.js index bcd82c6d2..2139f8e51 100644 --- a/test/test.js +++ b/test/test.js @@ -166,6 +166,25 @@ /*--------------------------------------------------------------------------*/ + QUnit.module('lodash.difference'); + + (function() { + test('should work correctly when using `cachedContains`', function() { + var array1 = _.range(27), + array2 = array1.slice(), + a = {}, + b = {}, + c = {}; + + array1.push(a, b, c); + array2.push(b, c, a); + + deepEqual(_.difference(array1, array2), []); + }); + }()); + + /*--------------------------------------------------------------------------*/ + QUnit.module('lodash.escape'); (function() {