C# 悖论:为什么收益率回报率比这里列出的要快
人们已经无数次证明,C# 悖论:为什么收益率回报率比这里列出的要快,c#,performance,benchmarking,C#,Performance,Benchmarking,人们已经无数次证明,收益率比列表慢 例如: 然而,当我尝试基准测试时,我得到了相反的结果: Results: TestYield: Time =1.19 sec TestList : Time =4.22 sec 在这里,列表速度慢了400%。无论大小,这种情况都会发生。这毫无意义 IEnumerable<int> CreateNumbers() //for yield { for (int i = 0; i < Size; i++) yield return i;
收益率
比列表
慢
例如:
然而,当我尝试基准测试时,我得到了相反的结果:
Results:
TestYield: Time =1.19 sec
TestList : Time =4.22 sec
在这里,列表速度慢了400%。无论大小,这种情况都会发生。这毫无意义
IEnumerable<int> CreateNumbers() //for yield
{
for (int i = 0; i < Size; i++) yield return i;
}
IEnumerable<int> CreateNumbers() //for list
{
var list = new List<int>();
for (int i = 0; i < Size; i++) list.Add(i);
return list;
}
我使用所有正确的基准规则来避免结果冲突,所以这不是问题所在
如果您看到底层代码,yield-return
是一个令人厌恶的状态机,但它速度更快。为什么?
编辑:所有确实产生的答案都比列表快。
New Results With Size set on constructor:
TestYield: Time =1.001
TestList: Time =1.403
From a 400% slower difference, down to 40% slower difference.
然而,这些见解令人心碎。这意味着1960年及以后所有使用列表作为默认集合的程序员都是错误的,应该被解雇,因为他们没有使用最适合这种情况的工具(yield)
答案是收益率应该更快,因为它没有实现
1) 我不接受这种逻辑。Yield有其内在逻辑,它不是一个“理论模型”,而是一个编译器结构。因此,它在消费时自动成为现实。我不接受“没有实现”的说法,因为使用时已经支付了成本
2) 如果一艘船可以在海上航行,而一位老妇人不能,你就不能要求这艘船“在陆地上移动”。就像你在这里做的那样。如果一个列表需要具体化,而收益率不需要,那么这不是一个“收益率问题”,而是一个“特性”。在测试中,产量不应该因为有更多的用途而受到惩罚
3) 我在这里争论的是,如果您知道整个集合将被消费,那么测试的目的是找到“最快的集合”来消费/返回方法返回的结果
yield是否成为从方法返回列表参数的新“事实标准”
Edit2:如果我使用纯内联数组,它将获得与成品相同的性能
Test 3:
TestYield: Time =0.987
TestArray: Time =0.962
TestList: Time =1.516
int[] CreateNumbers()
{
var list = new int[Size];
for (int i = 0; i < Size; i++) list[i] = i;
return list;
}
测试3:
测试收益率:时间=0.987
测试阵列:时间=0.962
测试列表:时间=1.516
int[]CreateNumbers()
{
变量列表=新的整数[大小];
对于(inti=0;i
因此,yield会自动内联到数组中。列表并非如此。如果您使用yield来度量版本,而不具体化列表,那么它将比其他版本具有优势,因为它不必分配和调整大型列表的大小(以及触发GC) 根据您的编辑,我想添加以下内容: 但是,请记住,从语义上讲,您看到的是两个 不同的方法。一个人产生一个集合。它的大小是有限的, 您可以存储对集合的引用、更改其元素以及 分享它 另一个生成一个序列。它可能是无限的,你会发现 每次迭代一个新副本,可能有,也可能没有 它背后的藏品 它们不是一回事。编译器不创建集合 实现一个序列。如果你实施了 通过在您将看到的场景背后具体化集合来进行排序 与使用列表的版本的性能类似 BenchmarkDotNet默认情况下不允许延迟执行,因此您必须构建一个使用方法的测试,这就是我在下面所做的。我通过BenchmarkDotNet运行了这个,得到了以下结果
Method | Mean | Error | StdDev | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op |
------------- |---------:|---------:|---------:|------------:|------------:|------------:|--------------------:|
ConsumeYield | 475.5 us | 7.010 us | 6.214 us | - | - | - | 40 B |
ConsumeList | 958.9 us | 7.271 us | 6.801 us | 285.1563 | 285.1563 | 285.1563 | 1049024 B |
注意分配。在某些情况下,这可能会有所不同
我们可以通过分配正确大小的列表来抵消一些分配,但最终这不是一个苹果对苹果的比较。下面的数字
Method | Mean | Error | StdDev | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op |
------------- |---------:|----------:|----------:|------------:|------------:|------------:|--------------------:|
ConsumeYield | 470.8 us | 2.508 us | 2.346 us | - | - | - | 40 B |
ConsumeList | 836.2 us | 13.456 us | 12.587 us | 124.0234 | 124.0234 | 124.0234 | 400104 B |
代码如下
[MemoryDiagnoser]
public class Test
{
static void Main(string[] args)
{
var summary = BenchmarkRunner.Run<Test>();
}
public int Size = 100000;
[Benchmark]
public int ConsumeYield()
{
var sum = 0;
foreach (var x in CreateNumbersYield()) sum += x;
return sum;
}
[Benchmark]
public int ConsumeList()
{
var sum = 0;
foreach (var x in CreateNumbersList()) sum += x;
return sum;
}
public IEnumerable<int> CreateNumbersYield() //for yield
{
for (int i = 0; i < Size; i++) yield return i;
}
public IEnumerable<int> CreateNumbersList() //for list
{
var list = new List<int>();
for (int i = 0; i < Size; i++) list.Add(i);
return list;
}
}
[MemoryDiagnoser]
公开课考试
{
静态void Main(字符串[]参数)
{
var summary=BenchmarkRunner.Run();
}
公共整数大小=100000;
[基准]
公共收益率()
{
var总和=0;
foreach(CreateNumbersYield()中的变量x)和+=x;
回报金额;
}
[基准]
public int consumerlist()
{
var总和=0;
foreach(CreateNumbersList()中的变量x)和+=x;
回报金额;
}
public IEnumerable CreateNumbersYield()//用于产量
{
对于(int i=0;i
您必须考虑以下几点:
会消耗内存,但您可以在无需任何额外资源的情况下反复使用它。要使用List
实现同样的效果,您需要通过yield
具体化序列ToList()
- 在生成
时,最好设置容量。这将避免调整内部阵列的大小列表
class Program
{
static void Main(string[] args)
{
// warming up
CreateNumbersYield(1);
CreateNumbersList(1, true);
Measure(null, () => { });
// testing
var size = 1000000;
Measure("Yield", () => CreateNumbersYield(size));
Measure("Yield + ToList", () => CreateNumbersYield(size).ToList());
Measure("List", () => CreateNumbersList(size, false));
Measure("List + Set initial capacity", () => CreateNumbersList(size, true));
Console.ReadLine();
}
static void Measure(string testName, Action action)
{
var sw = new Stopwatch();
sw.Start();
action();
sw.Stop();
Console.WriteLine($"{testName} completed in {sw.Elapsed}");
}
static IEnumerable<int> CreateNumbersYield(int size) //for yield
{
for (int i = 0; i < size; i++)
{
yield return i;
}
}
static IEnumerable<int> CreateNumbersList(int size, bool setInitialCapacity) //for list
{
var list = setInitialCapacity ? new List<int>(size) : new List<int>();
for (int i = 0; i < size; i++)
{
list.Add(i);
}
return list;
}
}
如果我们比较可比较的案例(
Yield+ToList
&List+Set initial capacity
),则Yield
要慢得多。显示基准代码是的,您是如何对其进行基准测试的?List
已经具体化了,而Yield
方法没有。对于可比较的结果,添加var result=CreateNumbers().ToList()
在收益率接近my时,我发现(对于10万次迭代的大小),收益率方法花费了14815毫秒,列表方法花费了8433毫秒。我想看看下面的代码:DWhat if you initialized the list with the size?@Magnus这将改善这种情况,因为您将拥有更少的分配(我已经更新了答案)。然而,我认为GC是这里的区别,因为我们正在比较一种具体化列表的方法和一种不具体化列表的方法,所以这并不是真正的苹果对苹果的比较。
class Program
{
static void Main(string[] args)
{
// warming up
CreateNumbersYield(1);
CreateNumbersList(1, true);
Measure(null, () => { });
// testing
var size = 1000000;
Measure("Yield", () => CreateNumbersYield(size));
Measure("Yield + ToList", () => CreateNumbersYield(size).ToList());
Measure("List", () => CreateNumbersList(size, false));
Measure("List + Set initial capacity", () => CreateNumbersList(size, true));
Console.ReadLine();
}
static void Measure(string testName, Action action)
{
var sw = new Stopwatch();
sw.Start();
action();
sw.Stop();
Console.WriteLine($"{testName} completed in {sw.Elapsed}");
}
static IEnumerable<int> CreateNumbersYield(int size) //for yield
{
for (int i = 0; i < size; i++)
{
yield return i;
}
}
static IEnumerable<int> CreateNumbersList(int size, bool setInitialCapacity) //for list
{
var list = setInitialCapacity ? new List<int>(size) : new List<int>();
for (int i = 0; i < size; i++)
{
list.Add(i);
}
return list;
}
}
Yield completed in 00:00:00.0001683
Yield + ToList completed in 00:00:00.0121015
List completed in 00:00:00.0060071
List + Set initial capacity completed in 00:00:00.0033668