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

Dav*_*e S 8 c# clr struct memory-management prefetch

出于好奇,我试图测试List<T>使用两者valuereference类型的性能.

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

这是我的实验:

  • 创建一个class只包含两个成员的基本,a int和abool

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

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

  • 时间迭代需要多长时间List1(执行一些任意工作,例如递增计数器然后访问元素)

然后我重复了一个struct代替class

我的假设是,当使用a时class,其中的引用List<T>将是连续的,但由于我如何创建它们(在添加到List1和之间切换List2),它们指向的对象可能不会.

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

因此,我期望struct表现更好(由于预取器等...)

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

这里发生了什么?

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

测试类(或结构)

public class/struct TestClass
{
    public int TestInt;
    public bool TestBool;
}
Run Code Online (Sandbox Code Playgroud)

创建随机列表:

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;    
}
Run Code Online (Sandbox Code Playgroud)

测试:

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();
Run Code Online (Sandbox Code Playgroud)

同时重复TestObjecta class和a struct.

我意识到没有太大的区别,但我希望struct表现得更好class

Han*_*ant 10

// Access the element
testBool = item.TestBool;
Run Code Online (Sandbox Code Playgroud)

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

counter++;
Run Code Online (Sandbox Code Playgroud)

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

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

基准指南是:

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


Sam*_*ell 7

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

  1. List<IntPtr>
  2. List<object>

即使引用类型实例没有填充连续的内存部分,引用本身也是如此.

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

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