Skip to content

Short-circuit when possible during Enumerable methods. #14842

Description

@JonHanna

As mentioned at dotnet/corefx#2237 and #14792, there are methods of Enumerable that can benefit from short-circuiting.

Now, logically a great many could such as e.g. Max(this IEnumerable<int>) testing for int.MaxValue, but that would require an additional check that would penalise all sequences that didn't have that value.

However:

  1. The logical behaviour of Single and SingleOrDefault is "look for a match, then check there isn't another" (just like Where(pred).Single()). Following that rather than keeping count removes work from non-error paths as well as shortening error paths.
  2. Min/MinOrDefault already have to check for NaN explicitly, so they don't suffer from any further work being done.
  3. Adding a check for IList<T> sources to Last/LastOrDefault adds only a single check to the entire operation. The search can then proceed in reverse order and break on the first match.

These changes all affect existing behaviour, in that if either the enumerable expected to be consumed, or they were used with a predicate that had a side-effect, that behaviour would now change.

On the one hand, any change to observed behaviour bar pure performance increase is bad.

On the other hand:

  1. If you have side-effects in a Func you are going to have problems elsewhere, particularly elsewhere in Linq which tends to short-circuit where it can.
  2. If you depend on an enumerable being consumed you are going to have problems elsewhere, , particularly elsewhere in Linq which tends to short-circuit where it can.
  3. The idea of Linq being side-effect free is supported not just in the documentation, but as a justification for the lack of a ForEach outside of cases (parallel, asynchronous) where some other feature is added (eh, being parallel or asynchronous).
  4. The current behaviour would be confusing to someone depending on side-effects anyway: Why does Single(pred) behave differently to Where(pred).Single()?
  5. The behaviour of Single can exasperate bugs. (I had a case where certain values would throw a sequence into an incorrect repetition. Rather than Single() throwing at this bug, it hung).

Side-benefit: I noticed when working on this that not only do the current Min(IEnumerable<double>) etc. fail to take the short-circuit opportunity if it encounters a NaN, but they can actually perform much slower on a sequence with a NaN in the middle than one without, presumably due to branch mis-prediction. It's reasonable to believe that there may be other branch mis-prediction cases that such short-circuiting would help remove.

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions