为什么更新结构数组并不比 class 数组更快?
Why isn't it faster to update a struct array than a class array?
为了准备在现有软件框架中进行优化,我进行了独立的性能测试,这样我就可以在花费大量时间之前评估潜在的收益。
情况
有 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 解决方案、构建和 运行)可用 here
由于内存对齐和更新模式的重复,我原以为会发生重大变化。所以,我的核心问题实际上是:为什么我无法衡量显着的改进? 我是否忽略了一些重要的事情?我在测试中是否遗漏了什么?
传统上您可能更喜欢后一种实现的主要原因是 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 也非常擅长预测使用模式和预取。
也可能是您的代码必须 box/unbox 您的结构。然而,如果性能真的如此相似,这似乎不太可能。
在 C# 中使用像这样的低级内容的重要之处在于,您确实需要 a) 信任框架来完成它的工作,或者 b) 在现实条件下进行分析 after 您发现了一个低级别的性能问题。我很欣赏这可能是一个玩具项目,或者您可能只是在玩笑的内存优化,但是您在 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
}
Data
的 10,000 个实例在内存中可能是连续的。此外,无论如何它们都可能适合您的缓存,所以我怀疑您是否会在本次测试中看到缓存未命中对性能的影响。
为了准备在现有软件框架中进行优化,我进行了独立的性能测试,这样我就可以在花费大量时间之前评估潜在的收益。
情况
有 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 解决方案、构建和 运行)可用 here
由于内存对齐和更新模式的重复,我原以为会发生重大变化。所以,我的核心问题实际上是:为什么我无法衡量显着的改进? 我是否忽略了一些重要的事情?我在测试中是否遗漏了什么?
传统上您可能更喜欢后一种实现的主要原因是 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 也非常擅长预测使用模式和预取。
也可能是您的代码必须 box/unbox 您的结构。然而,如果性能真的如此相似,这似乎不太可能。
在 C# 中使用像这样的低级内容的重要之处在于,您确实需要 a) 信任框架来完成它的工作,或者 b) 在现实条件下进行分析 after 您发现了一个低级别的性能问题。我很欣赏这可能是一个玩具项目,或者您可能只是在玩笑的内存优化,但是您在 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
}
Data
的 10,000 个实例在内存中可能是连续的。此外,无论如何它们都可能适合您的缓存,所以我怀疑您是否会在本次测试中看到缓存未命中对性能的影响。