[NoBrainer] C# and IEnumerable.ForEach

By default we cannot use ForEach() on IEnumerable but only on List. However, with extension methods to the rescue, it is easy to roll your own:

public static void ForEach<TSource>
(
  this IEnumerable<TSource> source, 
  Action<TSource> predicate
)
{
  // ...
}

Regarding the implementation I saw a few options: I could implement ForEach() with a traditional for loop, parallelise the operation, use ToList() to convert the list or other LINQ extensions such as Any() and All(). But which one would be the fastest? This would certainly depend on several factors such as the size of the list and the operation I was performing on the items in general.

But I was curious if this would differ significantly at all. So I created some simple benchmarks with BenchmarkDoNet and gave it a try. For this I created a static list with some pseudo-randomised strings which I would then test for IsNullOrEmpty() (which I consider a relatively fast operation).

Then I wrote a few different extensions …

public static class EnumerableExtensions
{
  public static void ForEachViaToList<TSource>
  (
    this IEnumerable<TSource> source, 
    Action<TSource> predicate
  )
  {
    Contract.Requires(null != source);
    Contract.Requires(null != predicate);

    source.ToList().ForEach(predicate);
  }

  public static void ForEachViaAny<TSource>
  (
    this IEnumerable<TSource> source, 
    Action<TSource> predicate
  )
  {
    Contract.Requires(null != source);
    Contract.Requires(null != predicate);

    source.Any(e =>
    {
      predicate(e);
      return false;
    });
  }

  public static void ForEachViaAll<TSource>
  (
    this IEnumerable<TSource> source, 
    Action<TSource> predicate
  )
  {
    Contract.Requires(null != source);
    Contract.Requires(null != predicate);

    source.All(e =>
    {
      predicate(e);
      return false;
    });
  }

  public static void ForEachViaAsParallel<TSource>
  (
    this IEnumerable<TSource> source, 
    Action<TSource> predicate
  )
  {
    Contract.Requires(null != source);
    Contract.Requires(null != predicate);

    source.AsParallel().ForEach(predicate);
  }
}

… and the benchmarks:

[Benchmark]
public static void ForEachViaToList()
{
  Data._enumerable.ForEachViaToList(e => string.IsNullOrEmpty(e));
}

[Benchmark]
public static void ForEachViaAny()
{
  Data._enumerable.ForEachViaAny(e => string.IsNullOrEmpty(e));
}

[Benchmark]
public static void ForEachViaAll()
{
  Data._enumerable.ForEachViaAll(e => string.IsNullOrEmpty(e));
}

[Benchmark]
public static void ForEachViaParallel()
{
  Data._enumerable.ForEachViaParallel(e => string.IsNullOrEmpty(e));
}

[Benchmark]
public static void ForEachManual()
{
  Data._enumerable.ToList().ForEach(e => string.IsNullOrEmpty(e));
}

[Benchmark]
public static void ForEachViaFor()
{
  foreach (var item in Data._enumerable)
  {
    string.IsNullOrEmpty(item);
  }
}

The results shown below were tested in DEBUG configuration (which we should normally not use for benchmarking), so we can expect the RELEASE results to be a bit faster. I actually do not care about the absolute numbers, but its relative speeds. As it seems the implementation via manual ToList() was the fastest. And I found it surprising that the manual for loop was slower. But of course, your actual results may vary.

IEnumerable with 1000 items

             Method |       Mean |    StdDev |  Gen 0 | Allocated |
------------------- |----------- |---------- |------- |---------- |
   ForEachViaToList | 20.2590 us | 0.3298 us | 0.6307 |   4.04 kB |
      ForEachViaAny | 64.5220 us | 1.1361 us |      - |      82 B |
      ForEachViaAll | 61.9468 us | 1.6833 us |      - |      82 B |
 ForEachViaParallel | 61.1964 us | 1.7248 us |      - |     154 B |
      ForEachManual | 20.6026 us | 0.4428 us | 0.6144 |   4.04 kB |
      ForEachViaFor | 36.9611 us | 0.8986 us |      - |      25 B |

IEnumerable with 16 items

             Method |          Mean |     StdErr |     StdDev |  Gen 0 | Allocated |
------------------- |-------------- |----------- |----------- |------- |---------- |
   ForEachViaToList |   651.5331 ns |  3.1449 ns | 12.1803 ns | 0.0066 |     100 B |
      ForEachViaAny | 1,394.3225 ns |  4.8660 ns | 18.8461 ns |      - |      80 B |
      ForEachViaAll | 1,360.9192 ns |  6.8639 ns | 26.5837 ns |      - |      80 B |
 ForEachViaParallel | 1,685.4376 ns | 13.0217 ns | 46.9504 ns |      - |     152 B |
      ForEachManual |   642.3019 ns |  7.1587 ns | 32.0148 ns | 0.0269 |     100 B |
      ForEachViaFor |   782.2964 ns |  7.5939 ns | 38.7214 ns |      - |      24 B |

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: