Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Simplified _.max and _.min #1423

Merged
merged 2 commits into from

5 participants

@megawac
Collaborator

Bringing this back up - the last time I saw this being discussed is #578.

Curious on thoughts as given a comparitor case is the more common use case from my experience with the functions.

Removing the native apply also normalizes the output for some oddities mentioned in #728 when given a positive number as the first argument and make the functions more consistent in general (for absurdly large collections)

E.g.

_.max([1, NaN]) //1 instead of NaN

Updated jsperf

@jashkenas
Owner

We want to be able to use the native implementation as much as possible — that's most of the point of the function existing in the first place.

@jashkenas jashkenas closed this
@jdalton
Collaborator

Ping @jashkenas, not sure if you saw my comment as it was in the OPs branch.

@jashkenas jashkenas reopened this
underscore.js
((13 lines not shown))
each(obj, function(value, index, list) {
var computed = iterator ? iterator.call(context, value, index, list) : value;
- computed > result.computed && (result = {value : value, computed : computed});
+ if(computed > computedMax) {

You could add a space after if.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
underscore.js
((33 lines not shown))
each(obj, function(value, index, list) {
var computed = iterator ? iterator.call(context, value, index, list) : value;
- computed < result.computed && (result = {value : value, computed : computed});
+ if(computed < computedMin) {

Here, too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@jdalton
Collaborator

@megawac Some of the changes here overlap those already done by #1448, might be worth rebasing them in.

@megawac megawac Simplify _.max and _.min
Remove the native apply case
4074324
@megawac
Collaborator

Thanks for the ping - rebased to use the code that was changed in davidchambers' (or was it brandon's?) and mehdishojae's commits since the original PR. Thanks @jdalton

@jashkenas
Owner

@megawac — Would it be possible to see a quick jsperf for this patch vs. the current master?

underscore.js
((6 lines not shown))
_.max = function(obj, iterator, context) {
- if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) {
- return Math.max.apply(Math, obj);
- }
var result = -Infinity, lastComputed = -Infinity;
each(obj, function(value, index, list) {
@jdalton Collaborator
jdalton added a note

I have a feeling that each use alone will not be enough to show a perf win. We can confirm with a jsPerf, but I think the Math.max and Math.min branch will need to be replaced with a simple for-loop branch.

@megawac Collaborator
megawac added a note

I updated the jsperf test with your suggestion and it obviously breaks compat

_.max = function(obj, iterator, context) {
    var result = -Infinity,
      lastComputed = -Infinity;
  for(var index = 0, l = obj.length; index < l; index++) {
    value = obj[index];
    computed = iterator ? iterator.call(context, value, index, list) : value;
    if (computed > lastComputed) {
      result = value;
      lastComputed = computed;
    }
  }
  return result;
};
@jdalton Collaborator
jdalton added a note

In my own implementation I keyed the fast path off of:
if (callback == null && isArray(collection)) {

else it hits the slow path, each.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@megawac
Collaborator

http://jsperf.com/simplified-max-min/2

Slightly faster in chrome and slower in ff from my quick tests

@jdalton
Collaborator

RE the jsperf: Yay \o/, for-loops win!

@jdalton
Collaborator

@megawac
Could you modify your PR to use the for-loop in place of the Math.m** use.
Something like:

  _.max = function(obj, iterator, context) {
    var result = -Infinity, lastComputed = -Infinity;
    if (!iterator && _.isArray(obj)) {
      for (var i = 0, length = obj.length; i < length; i++) {
        value = obj[i];
        if (value > result) {
          result = value;
        }
      }
    } else {
      each(obj, function(value, index, list) {
        var computed = iterator ? iterator.call(context, value, index, list) : value;
        if (computed > lastComputed) {
          result = value;
          lastComputed = computed;
        }
      });
    }
    return result;
  };
@jashkenas
Owner

Branching out for a for-loop is certainly a nice micro-optimization, but probably not something that's quite appropriate for core Underscore...

@jashkenas jashkenas closed this
@jdalton
Collaborator

The point was it avoids the hacky arguments length guards that Underscore is using and wins perf to boot with little more than a for-loop.

@megawac
Collaborator

@jashkenas I agree that @jdalton's implementation does not fit underscores style1, but I also disagree with the appropriateness of this Math.m**.apply hack that has compatibility issues.

_.max([1,2,3,"test"]) !== _.max(["test",1,2,3]) currently in underscore whereas it will always be true with the hack removed. I think this case alone is unjustifiable for a perf win for the absolutely simplest inputs.

1: Aside it may be useful to have an optimized internal forIn and forArray that don't consider context

@jdalton
Collaborator

@jashkenas I agree that @jdalton's implementation does not fit underscores style1, but I also disagree with the appropriateness of this Math.m**.apply hack that has compatibility issues.

Other places in Underscore use for-loops. The benefit is clear; it avoids the hacks Underscore currently has without a negative perf impact (the reason Underscore is using the hacks in the first place).

@jashkenas
Owner

Alright then. @megawac Want to amend this PR?

@jashkenas jashkenas reopened this
@megawac
Collaborator

Sure I'll rebase did you want me to include @jdalton's suggestions (sorry about the last commit I'm a moron I'll cherry pick it out)

@jashkenas
Owner

Yes. For simple arrays, use the for loop, otherwise, fall back to the each.

@michaelficarra michaelficarra commented on the diff
underscore.js
((24 lines not shown))
}
- });
+ } else {
+ each(obj, function(value, index, list) {
@michaelficarra Collaborator

Is there a generic fold we can use instead? I'm okay with implementing map using fold, but the other way around is just ... weird, since fold is a more basic abstraction.

@megawac Collaborator
megawac added a note

Something like this @michaelficarra (goes against #1448)?

_.min = function(obj, iterator, context) {
  return _.reduce(obj, function(last, value, index, list) {
    var computed = iterator ? iterator.call(context, value, index, list) : value;
    return computed < last.computed ? {value: value, computed: computed} : last;
  }, {result: Infinity, computed: Infinity}).value;
};
@michaelficarra Collaborator

Yeah, though it's too bad it kills performance. Let's stick with what you have.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@megawac
Collaborator

@jashkenas I've updated the PR with jdalton's suggested implementation

@jashkenas jashkenas merged commit 512854c into jashkenas:master

1 check passed

Details default The Travis CI build passed
@megawac megawac deleted the megawac:_.max branch
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Feb 20, 2014
  1. @megawac

    Simplify _.max and _.min

    megawac authored
    Remove the native apply case
Commits on Mar 3, 2014
  1. @megawac
This page is out of date. Refresh to see the latest.
Showing with 40 additions and 22 deletions.
  1. +6 −0 test/collections.js
  2. +34 −22 underscore.js
View
6 test/collections.js
@@ -295,6 +295,9 @@
equal(_.max({'a': 'a'}), -Infinity, 'Maximum value of a non-numeric collection');
equal(299999, _.max(_.range(1,300000)), 'Maximum value of a too-big array');
+
+ equal(3, _.max([1, 2, 3, 'test']), 'Finds correct max in array starting with num and containing a NaN');
+ equal(3, _.max(['test', 1, 2, 3]), 'Finds correct max in array starting with NaN');
});
test('min', function() {
@@ -312,6 +315,9 @@
equal(_.min([now, then]), then);
equal(1, _.min(_.range(1,300000)), 'Minimum value of a too-big array');
+
+ equal(1, _.min([1, 2, 3, 'test']), 'Finds correct min in array starting with num and containing a NaN');
+ equal(1, _.min(['test', 1, 2, 3]), 'Finds correct min in array starting with NaN');
});
test('sortBy', function() {
View
56 underscore.js
@@ -249,36 +249,48 @@
};
// Return the maximum element or (element-based computation).
- // Can't optimize arrays of integers longer than 65,535 elements.
- // See [WebKit Bug 80797](https://bugs.webkit.org/show_bug.cgi?id=80797)
_.max = function(obj, iterator, context) {
- if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) {
- return Math.max.apply(Math, obj);
- }
- var result = -Infinity, lastComputed = -Infinity;
- each(obj, function(value, index, list) {
- var computed = iterator ? iterator.call(context, value, index, list) : value;
- if (computed > lastComputed) {
- result = value;
- lastComputed = computed;
+ var result = -Infinity, lastComputed = -Infinity,
+ value, computed;
+ if (!iterator && _.isArray(obj)) {
+ for (var i = 0, length = obj.length; i < length; i++) {
+ value = obj[i];
+ if (value > result) {
+ result = value;
+ }
}
- });
+ } else {
+ each(obj, function(value, index, list) {
@michaelficarra Collaborator

Is there a generic fold we can use instead? I'm okay with implementing map using fold, but the other way around is just ... weird, since fold is a more basic abstraction.

@megawac Collaborator
megawac added a note

Something like this @michaelficarra (goes against #1448)?

_.min = function(obj, iterator, context) {
  return _.reduce(obj, function(last, value, index, list) {
    var computed = iterator ? iterator.call(context, value, index, list) : value;
    return computed < last.computed ? {value: value, computed: computed} : last;
  }, {result: Infinity, computed: Infinity}).value;
};
@michaelficarra Collaborator

Yeah, though it's too bad it kills performance. Let's stick with what you have.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ computed = iterator ? iterator.call(context, value, index, list) : value;
+ if (computed > lastComputed) {
+ result = value;
+ lastComputed = computed;
+ }
+ });
+ }
return result;
};
// Return the minimum element (or element-based computation).
_.min = function(obj, iterator, context) {
- if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) {
- return Math.min.apply(Math, obj);
- }
- var result = Infinity, lastComputed = Infinity;
- each(obj, function(value, index, list) {
- var computed = iterator ? iterator.call(context, value, index, list) : value;
- if (computed < lastComputed) {
- result = value;
- lastComputed = computed;
+ var result = Infinity, lastComputed = Infinity,
+ value, computed;
+ if (!iterator && _.isArray(obj)) {
+ for (var i = 0, length = obj.length; i < length; i++) {
+ value = obj[i];
+ if (value < result) {
+ result = value;
+ }
}
- });
+ } else {
+ each(obj, function(value, index, list) {
+ computed = iterator ? iterator.call(context, value, index, list) : value;
+ if (computed < lastComputed) {
+ result = value;
+ lastComputed = computed;
+ }
+ });
+ }
return result;
};
Something went wrong with that request. Please try again.