为什么在C#中使用结构Vector3I而不是三个整数要慢得多?

时间:2018-09-20 12:05:47

标签: c# performance x86 compiler-optimization micro-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>();
    }
}

我测量的结果:

                 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 |

我的环境:

  • Intel(R)Core(TM)2四核CPU Q6600 @ 2.40GHz
  • Windows 10(64位)
  • Visual Studio 2017
  • 语言:C#
  • 是的,我选择了发布配置

注释:

我当前的任务是将现有代码重写为a)支持更多功能,b)更快。另外,我正在处理大量数据-这是整个应用程序的当前瓶颈,因此,这不是过早的优化。

将嵌套循环重写为一个-我并不想在那里进行优化。我只需要多次编写此类迭代,因此只想简化代码,仅此而已。但是,由于它是代码的性能关键部分,因此我正在评估设计中的此类更改。现在,当我看到将三个变量存储到一个结构中时,我的处理时间就增加了一倍……我很害怕使用这样的结构……

1 个答案:

答案 0 :(得分:2)

这与内存访问和寄存器访问之间的差异有关。

TL; DR:
使用原始变量,所有内容都可以放入寄存器中,而使用struct,则必须从堆栈中访问所有内容,这就是内存访问。访问寄存器比访问内存快得多。

现在,进入完整说明:

C#是在启动时通过JIT编译的(这与JVM稍有不同,但是现在不重要),因此,我们可以看到实际生成的程序集(请查看here以查看它) )。

为此,我仅比较IteratedVariablesIteratedVector,因为您将仅凭这些就可以了解总要点。首先,我们有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高速缓存中,它也仍然要慢得多。访问然后注册。