Tom*_*Tom 9 c# performance x86 micro-optimization compiler-optimization
我正在处理3D网格中的大量数据,所以我想实现一个简单的迭代器而不是三个嵌套循环.但是,我遇到了性能问题:首先,我只使用int x,y和z变量实现了一个简单的循环.然后我实现了Vector3I结构并使用了 - 并且计算时间加倍.现在我正在努力解决这个问题 - 为什么会这样?我做错了什么?
复制示例:
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System.Runtime.CompilerServices;
public struct Vector2I
{
public int X;
public int Y;
public int Z;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vector2I(int x, int y, int z)
{
this.X = x;
this.Y = y;
this.Z = z;
}
}
public class IterationTests
{
private readonly int _countX;
private readonly int _countY;
private readonly int _countZ;
private Vector2I _Vector = new Vector2I(0, 0, 0);
public IterationTests()
{
_countX = 64;
_countY = 64;
_countZ = 64;
}
[Benchmark]
public void NestedLoops()
{
int countX = _countX;
int countY = _countY;
int countZ = _countZ;
int result = 0;
for (int x = 0; x < countX; ++x)
{
for (int y = 0; y < countY; ++y)
{
for (int z = 0; z < countZ; ++z)
{
result += ((x ^ y) ^ (~z));
}
}
}
}
[Benchmark]
public void IteratedVariables()
{
int countX = _countX;
int countY = _countY;
int countZ = _countZ;
int result = 0;
int x = 0, y = 0, z = 0;
while (true)
{
result += ((x ^ y) ^ (~z));
++z;
if (z >= countZ)
{
z = 0;
++y;
if (y >= countY)
{
y = 0;
++x;
if (x >= countX)
{
break;
}
}
}
}
}
[Benchmark]
public void IteratedVector()
{
int countX = _countX;
int countY = _countY;
int countZ = _countZ;
int result = 0;
Vector2I iter = new Vector2I(0, 0, 0);
while (true)
{
result += ((iter.X ^ iter.Y) ^ (~iter.Z));
++iter.Z;
if (iter.Z >= countZ)
{
iter.Z = 0;
++iter.Y;
if (iter.Y >= countY)
{
iter.Y = 0;
++iter.X;
if (iter.X >= countX)
{
break;
}
}
}
}
}
[Benchmark]
public void IteratedVectorAvoidNew()
{
int countX = _countX;
int countY = _countY;
int countZ = _countZ;
int result = 0;
Vector2I iter = _Vector;
iter.X = 0;
iter.Y = 0;
iter.Z = 0;
while (true)
{
result += ((iter.X ^ iter.Y) ^ (~iter.Z));
++iter.Z;
if (iter.Z >= countZ)
{
iter.Z = 0;
++iter.Y;
if (iter.Y >= countY)
{
iter.Y = 0;
++iter.X;
if (iter.X >= countX)
{
break;
}
}
}
}
}
}
public static class Program
{
public static void Main(string[] args)
{
BenchmarkRunner.Run<IterationTests>();
}
}
Run Code Online (Sandbox Code Playgroud)
我测量的是:
Method | Mean | Error | StdDev |
----------------------- |---------:|----------:|----------:|
NestedLoops | 333.9 us | 4.6837 us | 4.3811 us |
IteratedVariables | 291.0 us | 0.8792 us | 0.6864 us |
IteratedVector | 702.1 us | 4.8590 us | 4.3073 us |
IteratedVectorAvoidNew | 725.8 us | 6.4850 us | 6.0661 us |
Run Code Online (Sandbox Code Playgroud)
注意:'IteratedVectorAvoidNew'是由于讨论问题可能在于new
Vector3I 的操作符 - 最初,我使用自定义迭代循环并用秒表测量.
另外,我在迭代256×256×256区域时的基准:
Method | Mean | Error | StdDev |
----------------------- |---------:|----------:|----------:|
NestedLoops | 18.67 ms | 0.0504 ms | 0.0446 ms |
IteratedVariables | 18.80 ms | 0.2006 ms | 0.1877 ms |
IteratedVector | 43.66 ms | 0.4525 ms | 0.4232 ms |
IteratedVectorAvoidNew | 43.36 ms | 0.5316 ms | 0.4973 ms |
Run Code Online (Sandbox Code Playgroud)
我的环境:
笔记:
我目前的任务是重写现有代码a)支持更多功能,b)更快.此外,我正在处理大量数据 - 这是整个应用程序的当前瓶颈所以不,这不是一个过早的优化.
将嵌套循环重写为一个 - 我不是想在那里进行优化.我只需要多次编写这样的迭代,所以只是想简化代码,仅此而已.但由于它是代码中性能关键的一部分,我正在测量设计中的这些变化.现在,当我看到简单地将三个变量存储到结构中时,我将处理时间加倍...我非常害怕使用这样的结构......
这涉及到存储器访问和寄存器访问之间的区别。
TL;DR:
对于原始变量,所有内容都可以放入寄存器中,而对于结构体,所有内容都必须从堆栈访问,这是一种内存访问。访问寄存器比访问内存要快得多。
现在,完整的解释:
C# 是在启动时进行 JIT 编译的(这与 JVM 略有不同,但这现在并不重要),因此我们可以看到生成的实际程序集(在此处查看如何查看它)。
为此,我只是进行比较IteratedVariables
,IteratedVector
因为您只需通过这些就可以了解总体要点。首先我们有IteratedVariables
:
; int countX = 64;
in al, dx
push edi
push esi
push ebx
; int result = 0;
xor ebx, ebx
; int x = 0, y = 0, z = 0;
xor edi, edi
; int x = 0, y = 0, z = 0;
xor ecx, ecx
xor esi, esi
; while(true) {
; result += ((x ^ y) ^ (~z));
LOOP:
mov eax, edi
xor eax, ecx
mov edx, esi
not edx
xor eax, edx
add ebx, eax
; ++z;
inc esi
; if(z >= countZ)
cmp esi, 40h
jl LOOP
; {
; z = 0;
xor esi, esi
; ++y;
inc ecx
; if(y >= countY)
cmp ecx, 40h
jl LOOP
; {
; y = 0;
xor ecx, ecx
; ++x;
inc edi
; if(x >= countX)
cmp edi, 40h
jl LOOP
; {
; break;
; } } } }
; return result;
mov eax, ebx
pop ebx
pop esi
pop edi
pop ebp
ret
Run Code Online (Sandbox Code Playgroud)
我做了一些清理代码的工作,所有注释(用分号 ( ;
) 标记的行)都来自实际的 C# 代码(这些是为我生成的),为了简洁起见,我对它们进行了一些清理。您在这里应该注意的主要事情是,所有内容都在访问寄存器,没有原始内存访问(原始内存访问可以通过[]
寄存器名称来识别)。
在第二个示例 ( IteratedVector
) 中,我们将看到略有不同的代码片段:
; int countX = 64;
push ebp
mov ebp, esp
sub esp, 0Ch
xor eax, eax
mov dword ptr [ebp-0Ch], eax
mov dword ptr [ebp-8], eax
mov dword ptr [ebp-4], eax
; int result = 0;
xor ecx,ecx
; Vector3i iter = new Vector3i(0, 0, 0);
mov dword ptr [ebp-0Ch], ecx
mov dword ptr [ebp-8], ecx
mov dword ptr [ebp-4], ecx
; while(true) {
; result += ((iter.X ^ iter.Y) ^ (~iter.Z));
LOOP:
mov eax, dword ptr [ebp-0Ch]
xor eax, dword ptr [ebp-8]
mov edx, dword ptr [ebp-4]
not edx
xor eax, edx
add ecx, eax
; ++iter.Z;
lea eax, [ebp-4]
inc dword ptr [eax]
; if(iter.Z >= countZ)
cmp dword ptr [ebp-4], 40h
jl LOOP
; {
; iter.Z = 0;
xor edx, edx
mov dword ptr [ebp-4], edx
; ++iter.Y;
lea eax, [ebp-8]
inc dword ptr [eax]
; if(iter.Y >= countY)
cmp dword ptr [ebp-8], 40h
jl LOOP
; {
; iter.Y = 0;
xor edx, edx
mov dword ptr [ebp-8], edx
; ++iter.X;
lea eax, [ebp-0Ch]
inc dword ptr [eax]
; if(iter.X >= countX)
cmp dword ptr [ebp-0Ch], 40h
jl LOOP
; {
; break;
; } } } }
; return result;
mov eax, ecx
mov esp, ebp
; {
; break;
; } } } }
; return result;
pop ebp
ret
Run Code Online (Sandbox Code Playgroud)
在这里你会清楚地注意到很多原始内存访问,它们由方括号 ( []
) 标识,它们也有标签dword ptr
,不要太担心这意味着什么,只需将其视为Memory Access
。您会注意到这里的代码充满了它们。它们存在于从结构进行值访问的任何地方。
这就是为什么结构代码慢得多的原因,寄存器就在CPU旁边(实际上是在CPU内部),但内存却很远,即使它在CPU缓存中,访问寄存器仍然会慢得多。