11

During my investigations of List<T> enumeration I noticed List<T>.Enumerator is struct. In opposite for instance to System.Array.SZArrayEnumerator or System.SZArrayHelper.SZGenericArrayEnumerator<T> that are classes.

It seems this not typical usage of struct since enumerator has no traits of value type. For instance it modifies its state, it is never passed "by value" anywhere, etc.

What is design intention here?

Yarl
  • 288
  • 2
  • 13
  • 6
    Fewer allocations means less memory pressure for the garbage collector. As Net Core and C# continue to evolve, more things are moving toward `struct` like `ValueTask` when you are running a task synchronously. – Berin Loritsch Jun 10 '20 at 14:01
  • 5
    Enumerators are intended to be used in a loop, and then discarded. They are not meant to be passed at all. That makes it an ideal opportunity to optimize the enumerator into a struct. – Berin Loritsch Jun 10 '20 at 14:03
  • 3
    Kudos for looking through the reference source. So few developers do this. – Robert Harvey Jun 10 '20 at 14:06
  • @BerinLoritsch I guess you’re right. I highly doubt there will be ever pressure on GC from enumerator instances as Task’s ones could involve but still there’s likely no other meaningful reason. – Yarl Jun 10 '20 at 14:09
  • Some of my info came from this video: https://www.youtube.com/watch?v=nK54s84xRRs&t=1330s Writing Allocation Free Code in C# – Berin Loritsch Jun 10 '20 at 16:25
  • 1
    In game development you might iterate over the same array each frame 120 frames per second. If GetEnumerator would allocate we would see alot of GC spikes – Anders Jun 10 '20 at 16:50
  • @Anders I’m little confused. My idea is when `Enumerator` is once allocated on heap there is no new memory allocation. There are value updates like _index_ but no new allocations or you’re telling me that enumerator could be created (allocated) that much times? – Yarl Jun 10 '20 at 22:01
  • 1
    @Ucho every time `GetEnumerator` gets called (which includes every time you do a `foreach` over the `List`) the `Enumerator` gets instantiated. And yes, you could very easily be doing that every frame in a video game. *Although, that is on a `List`, on an array as Andres says, the `foreach` would be optimized into indexed access.* – Theraot Jun 10 '20 at 23:19

1 Answers1

7

According to a video from a conference session called "Writing Allocation Free Code In C#", the Net Core team is taking efforts to reduce unnecessary object allocations. Since struct is a "Value" type, it exists on the stack and is passed by copy. That is in opposition to the class which is a "Reference" type which exists in heap memory. There are a couple reasons to move in this direction:

  • Performance as stack allocation and clean up of small objects is faster than allocating and deallocating heap objects.
  • Runtime stability, not in the sense of things breaking, but more in the sense of reducing garbage collection pauses.

Some of the areas where this can make a big difference include the following:

  • Mobile gaming
  • Internet of Things devices (constrained runtime)

That said, it is very difficult to get the right application behavior using value types when most everything you use is a reference type. These changes are targeted with new APIs and new language features. In select cases (like List<T>.Enumerator) the newer iterations of Net Core will be making those optimizations in areas that they have the best hope of getting right.

I'm hoping the YouTube video stays up indefinitely, since there is a lot of good information on what language features and API changes support value types better.

I will repeat the same caveat that the speaker in the video had: Don't switch to value types in your APIs unless you really need to (for performance, etc.). It is easy to do it wrong, particularly where the struct is maintaining state like an enumerator. That said, the features are there when you need to make use of them.

Berin Loritsch
  • 45,784
  • 7
  • 87
  • 160
  • 4
    There's another caveat here: value types are boxed onto the heap if accessed via an interface, so you'd still incur a heap allocation if enumerating via `IList` or `IEnumerable`. You'd have to be holding onto a concrete `List` instance to avoid the allocation. – casablanca Jun 11 '20 at 03:32
  • Correct. Bottom line: easy to get wrong, hard to get right. There be dragons. – Berin Loritsch Jun 11 '20 at 11:46