为什么更新结构数组不比更新类数组更快

本文关键字:数组 更新 结构 为什么 | 更新日期: 2023-09-27 18:01:02

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

A. 情况

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 可能比在随机版本中更容易一遍又一遍地操作相同的对象类型。

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

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 Solution,Build & Run(可在此处获得

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

为什么更新结构数组不比更新类数组更快

传统上,您可能更喜欢后一种实现的主要原因是因为引用位置。如果数组的内容适合 CPU 缓存,则代码运行速度会快得多。相反,如果有很多缓存未命中,则代码运行速度要慢得多。

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

也可能是代码必须对结构进行装箱/拆箱的情况。然而,如果性能真的如此相似,这似乎不太可能。

在 C# 中,像这样的低级东西最重要的事情是,你真的需要 a( 信任框架来完成它的工作,或者 b( 在确定低级性能问题后在现实条件下进行配置文件。我很欣赏这可能是一个玩具项目,或者你可能只是在玩内存优化来傻笑,但像你在 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
}

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