List <struct> vs List <class> </class> </struct>的性能

时间:2014-04-12 15:39:15

标签: c# memory-management struct clr prefetch

出于好奇,我试图使用List<T>value类型来测试reference的效果。

结果并不像我预期的那样,让我相信我对这些物体在内存中如何布局的理解可能不正确。

这是我的实验:

  • 创建一个只包含两个成员的基本classintbool

  • 创建2个List<T>个对象来保存我的测试类(List1List2

  • 随机生成测试对象并将其添加到List1List2

  • 迭代List1所花费的时间(做一些任意工作,例如递增计数器然后访问元素)

然后我用struct代替class

重复了一次

我的假设是,当使用class时,List<T>中保存的引用将是连续的,但是由于我创建它们的方式(在添加List1和{{之间切换) 1}}),他们指向的对象可能不会。

我认为当使用List2时,因为它是一个值类型,所以对象本身将连续保存在内存中(因为struct包含实际的项而不是引用的集合)

因此,我希望List<T>表现得更好(由于预取等等)。

实际上,两者都非常相似。

这里发生了什么?

编辑 - 添加了实际访问迭代器中元素的代码,包括代码示例

测试类(或结构)

struct

创建随机列表:

public class/struct TestClass
{
    public int TestInt;
    public bool TestBool;
}

测试:

var list1 = new List<TestClass>();
var list2 = new List<TestClass>();

var toggle = false;
for (var i=0; i < 4000000; i++)
{
    // Random object generation removed for simplicity

    if (toggle)
        list1.Add(randomObject);
    else
        list2.Add(randomObject);

    toggle = !toggle;    
}

使用var stopWatch = new Stopwatch(); var counter = 0; var testBool = false; stopwatch.Start(); foreach(var item in list1) { // Access the element testBool = item.TestBool; counter++; } stopwatch.Stop(); 作为TestObjectclass重复。

我意识到没有太大区别,但我希望struct的效果明显优于struct

2 个答案:

答案 0 :(得分:10)

// Access the element
testBool = item.TestBool;

这没有效果,优化器将删除该语句,因为它没有有用的副作用。实际上,您并没有测量结构和类之间的差异,因为您实际上从未实际访问该元素。

counter++;

同样的故事,很可能会被优化掉。除非您实际使用计数器值,否则在循环完成后。让优化器删除太多代码并使测试毫无意义是一个常见的微基准危险。解决方法是:

foreach(var item in list1)
{
    // Access the element
    counter += item.TestInt;
}
Console.WriteLine(counter);

基准指南是:

  • 仅发布配置生成的配置文件代码。 Debug构建会产生太多额外代码并抑制优化
  • 工具+选项,调试,常规,取消勾选“在模块加载时抑制JIT优化”。即使您使用调试器
  • 运行,这也可确保您获得优化的代码
  • Debug + Windows + Disassembly是一个非常重要的调试器窗口,可以显示真正运行的代码。熟悉机器代码需要正确理解该窗口
  • 在测试代码周围放置一个外部循环非常重要,以确保您至少运行10次测试。这消除了冷启动效应,例如处理器必须填充L1指令高速缓存,抖动必须从程序集加载IL并在第一次执行时编译它。并删除你必须与在机器上运行的其他进程竞争并从而竞争处理器的随机异常值。
  • 15%的差异无统计学意义。

答案 1 :(得分:7)

如果您实际上没有访问列表中存储的类对象的成员,那么以下两种类型应该为迭代提供相同的性能。

  1. List<IntPtr>
  2. List<object>
  3. 即使引用类型实例没有填充连续的内存部分,引用本身也是。

    上述情况的例外情况是,如果CLR在执行内存小于32GiB的64位应用程序时压缩指针。此策略在JVM中记录为Compressed OOPS。但是,x86-64指令集包含的指令允许极其高效执行此压缩/解压缩,因此即使在这种情况下,您也应该看到与List<int>类似的性能。

    当您的值类型超过指针的大小(IntPtr.Size)时,事情变得有趣。在此之后,包含引用的List<T>的性能应该快速超过List<T>值类型的性能。这是因为无论您的引用类型 instance 有多大,该实例的引用最多为IntPtr.Size