为什么更新结构数组比使用类数组更快?

时间:2015-07-05 15:11:35

标签: c# .net performance optimization jit

为了在现有软件框架中准备优化,我进行了独立的性能测试,因此我可以在花费大量时间之前评估潜在的收益。

情况

N种不同类型的组件,其中一些组件实现了IUpdatable接口 - 这些是有趣的组件。它们分组在M个对象中,每个对象都维护一个组件列表。更新它们的方式如下:

foreach (GroupObject obj in objects)
{
    foreach (Component comp in obj.Components)
    {
        IUpdatable updatable = comp as IUpdatable;
        if (updatable != null)
            updatable.Update();
    }
}

优化

我的目标是针对大量分组对象和组件优化这些更新。首先,确保连续更新一种类型的所有组件,方法是将它们缓存在每种类型的一个数组中。基本上,这个:

foreach (IUpdatable[] compOfType in typeSortedComponents)
{
    foreach (IUpdatable updatable in compOfType)
    {
        updatable.Update();
    }
}

它背后的想法是,JIT或CPU可能比在洗牌版本中一次又一次地在相同的对象类型上操作更容易。

在下一步中,我希望通过确保一个Component类型的所有数据在内存中对齐 - 通过将其存储在struct数组中来进一步改善这种情况,如下所示:

foreach (ComponentDataStruct[] compDataOfType in typeSortedComponentData)
{
    for (int i = 0; i < compDataOfType.Length; i++)
    {
        compDataOfType[i].Update();
    }
}

问题

在我的独立性能测试中,这些更改都没有显着的性能提升。我不知道为什么。 没有显着的性能提升意味着,对于10000个组件,每个批处理运行100个更新周期,所有主要测试大约需要85毫秒+/- 2毫秒。

(唯一的区别在于引入了as强制转换和if检查,但这并不是我正在测试的内容。)

  • 所有测试均在发布模式下执行,没有附加调试器。
  • 使用此代码减少了外部干扰:

        currentProc.ProcessorAffinity = new IntPtr(2);
        currentProc.PriorityClass = ProcessPriorityClass.High;
        currentThread.Priority = ThreadPriority.Highest;
    
  • 每个测试实际上都做了一些原始的数学工作,所以它不仅仅是测量可能被优化掉的空方法调用。

  • 垃圾收集在每次测试前明确执行,以排除干扰。
  • 完整的源代码(VS解决方案,构建和运行)可用here

我原本期望由于内存对齐和更新模式中的重复而发生重大变化。所以,我的核心问题是:为什么我无法衡量显着改善?我是否忽略了重要的事情?我在考试中错过了哪些内容?

1 个答案:

答案 0 :(得分:6)

传统上您更喜欢后一种实现的主要原因是Locality of Reference。如果数组的内容适合CPU缓存,那么您的代码运行速度要快得多。相反,如果你有很多缓存未命中,那么你的代码运行得慢得多。

我怀疑,你的错误是第一次测试中的对象可能已经具有良好的参考位置If you allocate a lot of small objects all at once, those objects are likely to be contiguous in memory even though they're on the heap.(我正在寻找更好的资源,但我在自己的工作中经历了同样的事情)即使它们已经不连续,GC也可能会将它们移动到这样的状态是。由于现代CPU具有大型缓存,因此可能存在整个数据结构适合L2缓存的情况,因为没有太多可以与之竞争。即使缓存不大,现代CPU也非常擅长预测使用模式和预取。

也可能是您的代码必须对您的结构进行装箱/取消装箱。然而,如果表现非常相似,这似乎不太可能。

在C#中使用像这样的低级别东西这件事的重要之处在于你真的需要a)信任框架来完成它的工作,或者b)在现实条件下的配置文件在你之后确定了一个低级别的性能问题。我很欣赏这可能是一个玩具项目,或者你可能只是在玩giggles进行内存优化,但是你在OP中进行的先验优化不太可能在项目规模上产生明显的性能提升。 / p>

我还没有详细介绍你的代码,但我怀疑你的问题是不切实际的条件。有了更大的内存压力,尤其是更加动态的组件分配,您可能会看到预期的性能差异。然后,你可能不会,这就是为什么配置文件非常重要的原因。

值得注意的是,如果您事先知道严格手动优化内存位置对于应用程序的正常功能至关重要,则可能需要考虑托管语言是否是正确的工具。

编辑:是的,问题几乎肯定在这里: -

public static void PrepareTest()
{
  data = new Base[Program.ObjCount]; // 10000
  for (int i = 0; i < data.Length; i++)
    data[i] = new Data(); // Data consists of four floats
}

Data的10,000个实例可能在内存中是连续的。此外,它们可能都适合你的缓存,所以我怀疑你会看到这个测试中缓存未命中对性能的影响。