9

以下简短但完整的示例程序

const long iterations = 1000000000;

T[] array = new T[1 << 20];
for (int i = 0; i < array.Length; i++)
{
    array[i] = new T();
}

Stopwatch sw = Stopwatch.StartNew();
for (int i = 0; i < iterations; i++)
{
    array[i % array.Length].Value0 = i;
}

Console.WriteLine("{0,-15}  {1}   {2:n0} iterations/s",
    typeof(T).Name, sw.Elapsed, iterations * 1000d / sw.ElapsedMilliseconds);

替换T为以下类型

class SimpleClass                   struct SimpleStruct
{                                   {
    public int Value0;                  public int Value0;
}                                   }

class ComplexClass                  struct ComplexStruct
{                                   {
    public int Value0;                  public int Value0;
    public int Value1;                  public int Value1;
    public int Value2;                  public int Value2;
    public int Value3;                  public int Value3;
    public int Value4;                  public int Value4;
    public int Value5;                  public int Value5;
    public int Value6;                  public int Value6;
    public int Value7;                  public int Value7;
    public int Value8;                  public int Value8;
    public int Value9;                  public int Value9;
    public int Value10;                 public int Value10;
    public int Value11;                 public int Value11;
}                                   }

在我的机器上产生以下有趣的结果(Windows 7 .NET 4.5 32 位)

SimpleClass 00:00:10.4471717 95,721,260 次迭代/秒
ComplexClass 00:00:37.8199150 26,441,736 次迭代/秒
SimpleStruct 00:00:12.3075100 81,254,571 次迭代/秒
ComplexStruct 00:00:32.6140182 30,661,679 次迭代/秒

问题1:为什么ComplexClass比 慢这么多SimpleClass?经过的时间似乎随着类中字段的数量线性增加。写入具有很多字段的类的第一个字段与写入只有一个字段的类的第一个字段应该没有太大区别,不是吗?

问题2:为什么ComplexStruct比 慢SimpleStruct?看一下 IL 代码,i它直接写入数组,而不是写入本地实例ComplexStruct,然后复制到数组中。所以应该不会因为复制更多字段而产生开销。

额外的问题:为什么ComplexStruct比 快ComplexClass


编辑:用更小的数组更新了测试结果,T[] array = new T[1 << 8];

SimpleClass 00:00:13.5091446 74,024,724 次迭代/秒
ComplexClass 00:00:13.2505217 75,471,698 次迭代/秒
SimpleStruct 00:00:14.8397693 67,389,986 次迭代/秒
ComplexStruct 00:00:13.4821834 74,172,971 次迭代/秒

所以和之间几乎没有区别,SimpleClass和之间ComplexClass只有很小的区别。但是,和的性能显着下降。SimpleStructComplexStructSimpleClassSimpleStruct


编辑:现在有了T[] array = new T[1 << 16];

SimpleClass 00:00:09.7477715 102,595,670 次迭代/秒
ComplexClass 00:00:10.1279081 98,745,927 次迭代/秒
SimpleStruct 00:00:12.1539631 82,284,210 次迭代/秒
ComplexStruct 00:00:10.5914174 94,419,790 次迭代/秒

for 的结果1<<15是 like 1<<8,而 for 的结果1<<17是 like 1<<20

4

4 回答 4

7

问题1的可能答案:

您的 CPU 一次将一页内存读入其缓存。

使用较大的数据类型,您可以在每个缓存页面上放置更少的对象。即使您只写入一个 32 位值,您仍然需要 CPU 缓存中的页面。使用较小的对象,您可以在下一次需要从主内存中读取之前通过更多循环。

于 2012-12-14T23:32:12.570 回答
2

我没有文件可以证明这一点,但我想这可能是地方问题。作为在内存方面更广泛的复杂类,内核访问堆或堆栈上的遥远内存区域需要更长的时间。不过,客观地说,我不得不说你的措施之间的差异听起来真的很高,因为问题是系统的错误。

关于类和结构之间的区别,我也无法记录,但这可能是因为,出于与之前相同的原理,堆栈比堆区域更频繁地被缓存,导致缓存未命中率更低。

您是否运行过积极优化的程序?

编辑:我做了一个小测试ComplexStruct并使用StructLayoutAttributewithLayoutKind.Explicit作为参数,然后FieldOffsetAttribute在结构的每个字段中添加了一个 with 0 作为参数。SimpleStruct时间大大缩短了,我认为它们与's 的时间大致相同。我在调试模式下运行它,调试器打开,没有优化。虽然结构保留了它的字段,但它在内存中的大小被削减了,时间也是如此。

于 2012-12-14T23:35:16.540 回答
2

答案 1:因为 CPU 的缓存是固定大小的,所以ComplexClass速度较慢,因此一次可以放入缓存中的对象较少。基本上,由于从内存中获取所需的时间,您会看到增加。如果您进入缓存并降低 RAM 的速度,这可能会更清楚(极端)。SimpleClassComplexClass

答案 2:与答案 1 相同。

奖励:结构数组是结构的连续块,仅由数组指针引用。类数组是对类实例的连续引用块,由数组指针引用。由于类是在堆上创建的(基本上是有空间的地方),它们不在一个连续且有序的块中。虽然这对于优化空间非常有用,但不利于 CPU 缓存。因此,当迭代一个数组(按顺序)时,会有更多的 CPU 缓存未命中,其中包含指向大型类的指针的大数组,然后会有一个结构数组的按顺序迭代。

为什么SimpleStruct要慢SimpleClass:据我了解,结构有一定的开销(我被告知大约有 76 口)。我不确定它是什么或为什么存在,但我希望如果您使用本机代码(C++ 编译)运行相同的测试,您会看到该SimpleStruct数组的性能更好。这只是一个猜测。


不管怎样,这看起来很有趣。我今晚要试试。我会发布我的结果。是否有可能获得您的完整代码?

于 2012-12-15T00:05:42.697 回答
1

我稍微修改了您的基准以删除模数,这可能是消耗大部分时间的原因,而且您似乎在比较字段访问时间,而不是 int 模数算法。

    const long iterations = 1000;
    GC.Collect();
    GC.WaitForPendingFinalizers();
    //long sMem = GC.GetTotalMemory(true);
    ComplexStruct[] array = new ComplexStruct[1 << 20];
    for (int i = 0; i < array.Length; i++) {
        array[i] = new ComplexStruct();
    }
    //long eMem = GC.GetTotalMemory(true);
    //Console.WriteLine("memDiff=" + (eMem - sMem));
    //Console.WriteLine("mem/elem=" + ((eMem - sMem) / array.Length));
    Stopwatch sw = Stopwatch.StartNew();
    for (int k = 0; k < iterations; k++) {
        for (int i = 0; i < array.Length; i++) {
            array[i].Value0 = i;
        }
    }
    Console.WriteLine("{0,-15}  {1}   {2:n0} iterations/s",
        typeof(ComplexStruct).Name, sw.Elapsed, (iterations * array.Length) * 1000d / sw.ElapsedMilliseconds);

(替换每个测试的类型)。我得到了这些结果(数百万次内循环分配/秒):

SimpleClass 357.1
SimpleStruct 411.5
ComplexClass 132.9
ComplexStruct 159.1

就 Class vs Struct 版本而言,这些数字更接近我的预期。我认为复杂版本的较慢时间可以通过较大对象/结构的 CPU 缓存效应来解释。使用注释掉的内存测量代码表明结构版本消耗的总内存更少。在我注意到内存测量代码会影响结构与类版本的相对时间后,我添加了 GC.Collect。

于 2012-12-15T02:06:45.893 回答