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

Ada*_*dam 3 .net c# optimization performance 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();
    }
}
Run Code Online (Sandbox Code Playgroud)

优化

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

foreach (IUpdatable[] compOfType in typeSortedComponents)
{
    foreach (IUpdatable updatable in compOfType)
    {
        updatable.Update();
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

问题

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

(唯一的区别在于引入as演员和if支票,但那不是我正在测试的.)

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

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

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

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

Iai*_*way 6

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

我怀疑你的错误是你第一次测试中的对象可能已经具有良好的参考局部性.如果你同时分配了很多小对象,那么这些对象很可能在内存中是连续的,即使它们在堆上.(我正在寻找一个更好的来源,但我在自己的工作中经历了同样的事情)即使它们不是已经连续,GC也可能会将它们移动到它们的位置.由于现代CPU具有大型缓存,因此可能存在整个数据结构适合L2缓存的情况,因为没有太多可以与之竞争.即使缓存不大,现代CPU也非常擅长预测使用模式和预取.

也可能是您的代码必须打包/取消打包您的结构.然而,如果性能非常相似,这似乎不太可能.

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

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

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

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

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

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