C
C#•2y ago
Esa

yield???

public IEnumerable<int> A()
{
for (int i = 0; i <= 10; i += 2)
yield return i;
}



public IEnumerable<int> B()
{
var ints = new List<int>();
for (int i = 0; i <= 10; i += 2)
ints.Add(i);

return ints;
}
public IEnumerable<int> A()
{
for (int i = 0; i <= 10; i += 2)
yield return i;
}



public IEnumerable<int> B()
{
var ints = new List<int>();
for (int i = 0; i <= 10; i += 2)
ints.Add(i);

return ints;
}
What is the difference here?
10 Replies
Esa
EsaOP•2y ago
Is this only about the performance gain of not having to declare a new List, or is there something else im missing?
Thinker
Thinker•2y ago
The yield version doesn't actually allocate a list, it only yields its items when the caller actually requests it. The list has to allocate itself and a backing array. If you're actually concerned about performance here, benchmark it.
Esa
EsaOP•2y ago
I'm just thinking pragmatically from a beginners point of view, as in "is this knowledge that's useful, or academic fluff"
Thinker
Thinker•2y ago
yield is definitely useful for instance, it can be used to construct infinite sequences
Esa
EsaOP•2y ago
The usecase is flying well above my head sadly. Can you give some real world examples?
Thinker
Thinker•2y ago
sure, one sec For a simple example, this code generates an infinite sequence of factorial numbers.
static IEnumerable<BigInteger> Factorials()
{
BigInteger x = 1;
BigInteger i = 1;

while (true)
{
yield return x;

i++;
x *= i;
}
}
static IEnumerable<BigInteger> Factorials()
{
BigInteger x = 1;
BigInteger i = 1;

while (true)
{
yield return x;

i++;
x *= i;
}
}
This would be rather impractical to do with lists, if you just want a sequence of numbers You can do almost everything can do with yield with lists, but the benefit is that yield is lazy and only evaluates when the caller wants it. In the example above, the 10! isn't computed unless the caller actually wants that. (and yes the example above isn't actually very practical unless you really want a sequence of factorial numbers for whatever reason)
Esa
EsaOP•2y ago
benchmark gone wrong lmao
Thinker
Thinker•2y ago
Another example would be if you have a tree structure
record Node<T>(T Value, Node<T>? Child);

static IEnumerable<Node<T>> GetDescendantNodes<T>(this Node<T> node)
{
var current = node;
while (current.Child is not null)
{
yield current.Child;
current = current.Child;
}
}
record Node<T>(T Value, Node<T>? Child);

static IEnumerable<Node<T>> GetDescendantNodes<T>(this Node<T> node)
{
var current = node;
while (current.Child is not null)
{
yield current.Child;
current = current.Child;
}
}
This descends through a node's descendants, but only to the depth the caller wants. The tree might be recursive, having a node which links back to a previous node, which would for a list be impossible to handle, but for an enumerable using yield is perfectly fine. Again the benefit is not having to compute the entire sequence unless it's actually needed. Also here's some benchmarks
| Method | Mean | Error | StdDev | Median | Gen0 | Allocated |
|------- |---------:|---------:|---------:|---------:|--------:|----------:|
| List | 26.37 us | 1.275 us | 3.660 us | 25.27 us | 83.3130 | 131400 B |
| Yield | 13.08 us | 0.068 us | 0.053 us | 13.07 us | 0.0153 | 32 B |
| Method | Mean | Error | StdDev | Median | Gen0 | Allocated |
|------- |---------:|---------:|---------:|---------:|--------:|----------:|
| List | 26.37 us | 1.275 us | 3.660 us | 25.27 us | 83.3130 | 131400 B |
| Yield | 13.08 us | 0.068 us | 0.053 us | 13.07 us | 0.0153 | 32 B |
[MemoryDiagnoser]
public class Benchmarks
{
[Benchmark]
public void List()
{
static List<int> List()
{
List<int> list = new();
for (int i = 0; i < 10_000; i++)
list.Add(i);
return list;
}

var list = List();

_ = list[5000];
}

[Benchmark]
public void Yield()
{
static IEnumerable<int> Enumerable()
{
for (int i = 0; i < 10_000; i++)
yield return i;
}

var enumerable = Enumerable();

_ = enumerable.ElementAt(5000);
}
}
[MemoryDiagnoser]
public class Benchmarks
{
[Benchmark]
public void List()
{
static List<int> List()
{
List<int> list = new();
for (int i = 0; i < 10_000; i++)
list.Add(i);
return list;
}

var list = List();

_ = list[5000];
}

[Benchmark]
public void Yield()
{
static IEnumerable<int> Enumerable()
{
for (int i = 0; i < 10_000; i++)
yield return i;
}

var enumerable = Enumerable();

_ = enumerable.ElementAt(5000);
}
}
So you can see that the list allocates significantly more memory, because it has to allocate the entire list, while the enumerable only has to compute as much as it actually needs. However the list is significantly faster (O(1)) in terms of access, while the enumerable is linear (O(n)).
Esa
EsaOP•2y ago
Yup I actually did some tests. Thoroughly shocked at how big the difference was!
Pobiega
Pobiega•2y ago
If you allocate the list to fit 10K items from the start, you'll get a lot better memory numbers.. but it will still be much higher than the yield 🙂
Want results from more Discord servers?
Add your server