为什么这个自定义 Vector2 结构的性能比这个自定义 Vector4 差这么多?

Wal*_*t D 9 .net c# performance

虽然标杆一些自定义矢量类型,我发现,我的意料,我Vector2类型是许多基本操作慢得多从阵列中读取时,比我的Vector4型(和的Vector3)尽管有代码本身更少的操作,字段和变量。这是一个大大简化的示例,演示了这一点:

using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

namespace VectorTest
{
    [StructLayout(LayoutKind.Sequential, Pack = 4)]
    public struct TestStruct4
    {
        public float X, Y, Z, W;

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public TestStruct4(float x, float y, float z, float w)
        {
            X = x;
            Y = y;
            Z = z;
            W = w;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static TestStruct4 operator +(in TestStruct4 a, in TestStruct4 b)
        {
            return new TestStruct4(
                a.X + b.X,
                a.Y + b.Y,
                a.Z + b.Z,
                a.W + b.W);
        }
    }

    [StructLayout(LayoutKind.Sequential, Pack = 4)]
    public struct TestStruct2
    {
        public float X, Y;

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public TestStruct2(float x, float y)
        {
            X = x;
            Y = y;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static TestStruct2 operator +(in TestStruct2 a, in TestStruct2 b)
        {
            return new TestStruct2(
                a.X + b.X,
                a.Y + b.Y);
        }
    }

    public class Program
    {
        private const int COUNT = 10000;
        private static readonly TestStruct4[] s_arr4 = new TestStruct4[COUNT];
        private static readonly TestStruct2[] s_arr2 = new TestStruct2[COUNT];

        static unsafe void Main()
        {
            for(int i = 0; i < s_arr4.Length; i++)
                s_arr4[i] = new TestStruct4(i, i * 2, i * 3, i * 4);
            for(int i = 0; i < s_arr2.Length; i++)
                s_arr2[i] = new TestStruct2(i, i * 2);

            BenchmarkRunner.Run<Program>();
        }

        [Benchmark]
        public TestStruct4 BenchmarkTestStruct4()
        {
            TestStruct4 ret = default;
            for (int i = 0; i < COUNT; i++)
                ret += s_arr4[i];
            return ret;
        }

        [Benchmark]
        public TestStruct2 BenchmarkTestStruct2()
        {
            TestStruct2 ret = default;
            for (int i = 0; i < COUNT; i++)
                ret += s_arr2[i];
            return ret;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

运行这个基准测试结果:

方法 意思 错误 标准差
基准测试结构4 9.863 美元 0.0706 美元 0.0626 美元
基准测试结构2 22.412我们 0.3100 美元 0.2899 美元

如您所见,TestStruct2 的速度是 TestStruct4 的两倍多(至少在我的电脑上)。鉴于 TestStruct2 基本上与 TestStruct4 相同,除了它具有较少的字段并且必须执行较少的添加操作,我原以为它在最坏的情况下与 TestStruct4 的速度相同,但实际上它更慢。谁能解释这是为什么?

进一步的实验表明,如果我向 MyStruct2 添加另一个或两个填充浮点数(并用于Unsafe.SkipInit避免初始化它们的成本),那么性能会提高以匹配 MyStruct4 的性能。所以我猜测MyStruct2 存在某种对齐问题,但我不明白具体可能是什么。将代码粘贴到 SharpLab 中并没有揭示 ASM 中任何明显的明显差异(尽管我可能对 ASM 的理解不够好,无法发现某些东西)。

编辑:这是在 Windows 10 64 位上的 .NET 5 上运行的。

(注意:我不想讨论在已经有很多现有类型的情况下编写自己的向量类型是否谨慎。我有这样做的理由,它们与这个问题的重点无关,这我出于学术上的好奇而询问为什么会有如此巨大的性能差异。)

编辑:根据要求,这里是两个结构的字节布局:

Type layout for 'TestStruct4'
Size: 16 bytes. Paddings: 0 bytes (%0 of empty space)
|===========================|
|   0-3: Single X (4 bytes) |
|---------------------------|
|   4-7: Single Y (4 bytes) |
|---------------------------|
|  8-11: Single Z (4 bytes) |
|---------------------------|
| 12-15: Single W (4 bytes) |
|===========================|


Type layout for 'TestStruct2'
Size: 8 bytes. Paddings: 0 bytes (%0 of empty space)
|===========================|
|   0-3: Single X (4 bytes) |
|---------------------------|
|   4-7: Single Y (4 bytes) |
|===========================|
Run Code Online (Sandbox Code Playgroud)

小智 4

对于结构体TestStruct4“+”运算符重载方法,生成的汇编指令使用 XMM 寄存器来存储和递增值,因此加法指令如下所示:

00007FFF72084077  vaddss      xmm0,xmm0,dword ptr [rdx]
00007FFF7208407B  vaddss      xmm1,xmm1,dword ptr [rdx+4]
00007FFF72084080  vaddss      xmm2,xmm2,dword ptr [rdx+8]
00007FFF72084085  vaddss      xmm3,xmm3,dword ptr [rdx+0Ch]
Run Code Online (Sandbox Code Playgroud)

漂亮又整洁。现在这是生成的内容TestStruct2

00007FFF6FE2B3EA  vmovss      xmm0,dword ptr [rsp+20h]
00007FFF6FE2B3F0  vaddss      xmm0,xmm0,dword ptr [rdx]
00007FFF6FE2B3F4  vmovss      xmm1,dword ptr [rsp+24h]
00007FFF6FE2B3FA  vaddss      xmm1,xmm1,dword ptr [rdx+4]
00007FFF6FE2B3FF  vmovss      dword ptr [rsp+20h],xmm0
00007FFF6FE2B405  vmovss      dword ptr [rsp+24h],xmm1
Run Code Online (Sandbox Code Playgroud)

这里“+”运算符重载方法汇编指令并不将值存储在 XMM 寄存器中,而是存储在内存中,因此存在额外的开销 - 在开始时它将初始值从内存移动到 XMM,并在end 它将修改后的值移回内存。

目前尚不清楚为什么会发生这种情况,但它看起来确实很像编译器未能正确优化代码。要解决这个特定问题,您可以将字段的类型从 更改floatdouble,然后它将得到优化,并且性能方面将基本相同。或者,如果无法更改类型,则解决方案是,正如您所提到的 - 添加虚拟字段。