我正在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>();
}
}
我测量的结果:
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 |
注意:“ IteratedVectorAvoidNew”之所以存在,是因为讨论可能是问题出在Vector3I的new
运算符上-最初,我使用了自定义迭代循环并使用秒表进行了测量。
此外,这是我在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 |
我的环境:
注释:
我当前的任务是将现有代码重写为a)支持更多功能,b)更快。另外,我正在处理大量数据-这是整个应用程序的当前瓶颈,因此,这不是过早的优化。
将嵌套循环重写为一个-我并不想在那里进行优化。我只需要多次编写此类迭代,因此只想简化代码,仅此而已。但是,由于它是代码的性能关键部分,因此我正在评估设计中的此类更改。现在,当我看到将三个变量存储到一个结构中时,我的处理时间就增加了一倍……我很害怕使用这样的结构……
答案 0 :(得分:2)
这与内存访问和寄存器访问之间的差异有关。
TL; DR:
使用原始变量,所有内容都可以放入寄存器中,而使用struct,则必须从堆栈中访问所有内容,这就是内存访问。访问寄存器比访问内存快得多。
现在,进入完整说明:
C#是在启动时通过JIT编译的(这与JVM稍有不同,但是现在不重要),因此,我们可以看到实际生成的程序集(请查看here以查看它) )。
为此,我仅比较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
我已经完成了一些清理代码的工作,所有注释(标有分号(;
的行)均来自实际的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
在这里您会清楚地注意到很多原始内存访问,它们由方括号([]
标识,它们也带有标签dword ptr
,不必太担心这意味着什么,但只需将其视为Memory Access
。您会发现这里的代码充满了他们。它们无处不在,可以从结构进行值访问。
这就是为什么结构代码要慢得多的原因,寄存器位于CPU的旁边(字面上是它的内部),但是内存距离很远,即使它位于CPU高速缓存中,它也仍然要慢得多。访问然后注册。