出于好奇,我试图使用List<T>
和value
类型来测试reference
的效果。
结果并不像我预期的那样,让我相信我对这些物体在内存中如何布局的理解可能不正确。
这是我的实验:
创建一个只包含两个成员的基本class
,int
和bool
创建2个List<T>
个对象来保存我的测试类(List1
和List2
)
随机生成测试对象并将其添加到List1
和List2
迭代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();
作为TestObject
和class
重复。
我意识到没有太大区别,但我希望struct
的效果明显优于struct
答案 0 :(得分:10)
// Access the element
testBool = item.TestBool;
这没有效果,优化器将删除该语句,因为它没有有用的副作用。实际上,您并没有测量结构和类之间的差异,因为您实际上从未实际访问该元素。
counter++;
同样的故事,很可能会被优化掉。除非您实际使用计数器值,否则在循环完成后。让优化器删除太多代码并使测试毫无意义是一个常见的微基准危险。解决方法是:
foreach(var item in list1)
{
// Access the element
counter += item.TestInt;
}
Console.WriteLine(counter);
基准指南是:
答案 1 :(得分:7)
如果您实际上没有访问列表中存储的类对象的成员,那么以下两种类型应该为迭代提供相同的性能。
List<IntPtr>
List<object>
即使引用类型实例没有填充连续的内存部分,引用本身也是。
上述情况的例外情况是,如果CLR在执行内存小于32GiB的64位应用程序时压缩指针。此策略在JVM中记录为Compressed OOPS。但是,x86-64指令集包含的指令允许极其高效执行此压缩/解压缩,因此即使在这种情况下,您也应该看到与List<int>
类似的性能。
当您的值类型超过指针的大小(IntPtr.Size
)时,事情变得有趣。在此之后,包含引用的List<T>
的性能应该快速超过List<T>
值类型的性能。这是因为无论您的引用类型 instance 有多大,该实例的引用最多为IntPtr.Size
。