【问题标题】:Why does adding an extra field to struct greatly improves its performance?为什么向 struct 添加额外的字段会大大提高其性能?
【发布时间】:2017-11-04 19:11:49
【问题描述】:

我注意到包装单个浮点数的结构比直接使用浮点数要慢得多,大约只有一半的性能。

using System;
using System.Diagnostics;

struct Vector1 {

    public float X;

    public Vector1(float x) {
        X = x;
    }

    public static Vector1 operator +(Vector1 a, Vector1 b) {
        a.X = a.X + b.X;
        return a;
    }
}

但是,在添加额外的“额外”字段后,似乎会发生一些神奇的事情,并且性能再次变得更加合理:

struct Vector1Magic {

    public float X;
    private bool magic;

    public Vector1Magic(float x) {
        X = x;
        magic = true;
    }

    public static Vector1Magic operator +(Vector1Magic a, Vector1Magic b) {
        a.X = a.X + b.X;
        return a;
    }
}

我用来对这些进行基准测试的代码如下:

class Program {
    static void Main(string[] args) {
        int iterationCount = 1000000000;
        var sw = new Stopwatch();
        sw.Start();
        var total = 0.0f;
        for (int i = 0; i < iterationCount; i++) {
            var v = (float) i;
            total = total + v;
        }
        sw.Stop();
        Console.WriteLine("Float time was {0} for {1} iterations.", sw.Elapsed, iterationCount);
        Console.WriteLine("total = {0}", total);
        sw.Reset();
        sw.Start();
        var totalV = new Vector1(0.0f);
        for (int i = 0; i < iterationCount; i++) {
            var v = new Vector1(i);
            totalV += v;
        }
        sw.Stop();
        Console.WriteLine("Vector1 time was {0} for {1} iterations.", sw.Elapsed, iterationCount);
        Console.WriteLine("totalV = {0}", totalV);
        sw.Reset();
        sw.Start();
        var totalVm = new Vector1Magic(0.0f);
        for (int i = 0; i < iterationCount; i++) {
            var vm = new Vector1Magic(i);
            totalVm += vm;
        }
        sw.Stop();
        Console.WriteLine("Vector1Magic time was {0} for {1} iterations.", sw.Elapsed, iterationCount);
        Console.WriteLine("totalVm = {0}", totalVm);
        Console.Read();
    }
}

与基准测试结果:

Float time was 00:00:02.2444910 for 1000000000 iterations.
Vector1 time was 00:00:04.4490656 for 1000000000 iterations.
Vector1Magic time was 00:00:02.2262701 for 1000000000 iterations.

编译器/环境设置: 操作系统:Windows 10 64 位 工具链:VS2017 框架:.Net 4.6.2 目标:任何 CPU 首选 32 位

如果将 64 位设置为目标,我们的结果更可预测,但明显比我们在 32 位目标上看到的 Vector1Magic 差:

Float time was 00:00:00.6800014 for 1000000000 iterations.
Vector1 time was 00:00:04.4572642 for 1000000000 iterations.
Vector1Magic time was 00:00:05.7806399 for 1000000000 iterations.

对于真正的巫师,我在此处包含了 IL 转储:https://pastebin.com/sz2QLGEx

进一步调查表明,这似乎是特定于 Windows 运行时的,因为单声道编译器产生相同的 IL。

在单声道运行时,与原始浮点数相比,两种结构变体的性能大约慢 2 倍。这与我们在 .Net 上看到的性能有很大不同。

这是怎么回事?

*请注意,这个问题最初包含一个有缺陷的基准测试过程(感谢 Max Payne 指出这一点),并且已经更新以更准确地反映时间安排。

【问题讨论】:

  • 我猜这是由于结构包装现在具有更好的内存对齐。
  • 您应该添加一个预热迭代以排除来自 JIT 或其他一次性处理的可能干扰。
  • 如果我切换到 64 位,我的“魔术”向量的性能会更差。
  • 我之前加入了一个热身期 - 这对这次测试没有显着影响。
  • 我希望有人能解决这个问题。 Vector1Magic 会更快,这太违反直觉了。

标签: c# .net mono clr cil


【解决方案1】:

这不应该发生。这显然是某种错位,迫使 JIT 无法正常工作。

struct Vector1 //Works fast in 32 Bit 
{
    public double X;
}

struct Vector1 //Works fast in 64 Bit and 32 Bit
{
    public double X;
    public double X2;
}

您还必须调用: Console.WriteLine(total); 将时间精确地增加到 Vector1Magic 时间,这是有道理的。问题仍然存在,为什么 Vector1 这么慢。

也许结构没有针对 sizeof(foo)

这似乎是 7 年前提出的: Why is 16 byte the recommended size for struct in C#?

【讨论】:

  • “这不应该发生。这显然是某种错位,迫使 JIT 无法正常工作。” - 这并不能真正回答问题。为什么会这样?这背后的原因是什么?
  • 然后让我们投票,直到有人知道 .net 在内部是如何工作的。生成的 IL 代码很好,因此阅读不会将我们指向解决方案。问题更深,在 JIT 优化器内部。这是一个非常有趣的发现,也许您可​​以将其发布在 MSDN .net 开发者团队论坛上?
  • 请插入一行,上面写着 Console.WriteLine(total);在你的第一个循环之后。 JIT 不会执行之后不使用结果的节点。
  • 哦,好的,我测试了它,它把 float 的循环速度降低到了 magic2 的循环速度
  • 双重检查 - 它确实改变了结果。用那个更新问题。
【解决方案2】:

CIL 代码是相同的(实际上)。但 x86 汇编代码不是。

我认为,这是 JIT 编译器优化的一些特性。

编译器为Vector1 生成以下汇编代码。

C#(在 cmets 中部分组装 x86):

var totalV = new Vector1(0.0f);
/*
01300576  fldz  
01300578  fstp        dword ptr [ebp-14h] 
*/
for (int i = 0; i < iterationCount; i++)
{
   var v = new Vector1(i);
   /*
   0130057D  mov         dword ptr [ebp-4Ch],ecx ; ecx - is index "i"
   01300580  fild        dword ptr [ebp-4Ch]
   01300583  fstp        dword ptr [ebp-4Ch]  
   01300586  fld         dword ptr [ebp-4Ch]
   */
   totalV += v;
   /*
   01300589  lea         eax,[ebp-14h]  
   0130058C  mov         eax,dword ptr [eax]  
   0130058E  lea         edx,[ebp-18h]  
   01300591  mov         dword ptr [edx],eax  
   01300593  fadd        dword ptr [ebp-18h]  
   01300596  fstp        dword ptr [ebp-18h]  
   01300599  mov         eax,dword ptr [ebp-18h]  
   0130059C  mov         dword ptr [ebp-14h],eax  
   */
}

编译器为Vector1Magic 生成以下汇编代码。

C#(在 cmets 中部分组装 x86):

var totalVm = new Vector1Magic(0.0f);
/*
01300657  mov         byte ptr [ebp-20h],1  ; here's assignment "magic=true"
0130065B  fldz  
0130065D  fstp        dword ptr [ebp-1Ch]
*/
for (int i = 0; i < iterationCount; i++)
{
    var vm = new Vector1Magic(i);
    /*
    01300662  mov         dword ptr [ebp-4Ch],edx ; edx - is index "i"
    01300665  fild        dword ptr [ebp-4Ch]  
    01300668  fstp        dword ptr [ebp-4Ch]  
    0130066B  fld         dword ptr [ebp-4Ch]  
    */
    totalVm += vm;
    /*
    0130066E  movzx       ecx,byte ptr [ebp-20h] ; here's some work with "unused" magic field
    01300672  fld         dword ptr [ebp-1Ch]  
    01300675  faddp       st(1),st  
    01300677  fstp        dword ptr [ebp-1Ch]  
    0130067A  mov         byte ptr [ebp-20h],cl  ; here's some work with "unused" magic field
    */
}

显然这个 asm 块会影响性能:

;Vector1
01300589  lea         eax,[ebp-14h]  
0130058C  mov         eax,dword ptr [eax]  
0130058E  lea         edx,[ebp-18h]  
01300591  mov         dword ptr [edx],eax  
01300593  fadd        dword ptr [ebp-18h]  
01300596  fstp        dword ptr [ebp-18h]  
01300599  mov         eax,dword ptr [ebp-18h]  
0130059C  mov         dword ptr [ebp-14h],eax  

;Vector1Magic
0130066E  movzx       ecx,byte ptr [ebp-20h] ; here's some work with "unused" magic field
01300672  fld         dword ptr [ebp-1Ch]  
01300675  faddp       st(1),st  
01300677  fstp        dword ptr [ebp-1Ch]  
0130067A  mov         byte ptr [ebp-20h],cl  ; here's some work with "unused" magic field

JIT 编译器对具有一个字段和多个字段的结构以不同的方式处理操作。可能它期望在 Vector1Magic 操作中使用所有字段(以及“未使用”)。

【讨论】:

    【解决方案3】:

    jit 有一种称为“结构提升”的优化,它可以有效地用多个局部变量替换结构局部或参数,一个用于结构的每个字段。

    然而,单个结构包装浮点的结构提升被禁用。原因有点晦涩,但大致如下:

    • 简单包装原始类型的结构在传递给调用或从调用返回时被视为结构大小的整数值
    • 在提升分析期间,jit 无法判断该结构是否曾经传递给调用或从调用返回。
    • 在调用时将 int 重新分类为浮点数(反之亦然)所需的代码序列在运行时被认为是昂贵的。
    • 因此不提升结构,因此对浮点字段的访问和操作要慢一些。

    所以粗略地说,jit 优先考虑降低呼叫站点的成本,而不是提高使用该字段的地方的成本。有时(如上述情况,运营成本占主导地位)这不是正确的选择。

    如您所见,如果您使结构体变大,则传递和返回结构体的规则会发生变化(现在通过引用返回传递),这会解除对提升的阻碍。

    CoreCLR sources 中,您可以在Compiler::lvaShouldPromoteStructVar 中看到这个逻辑。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-06-18
      • 2015-10-05
      • 2018-09-01
      • 2015-08-26
      • 2013-07-21
      • 2010-12-09
      相关资源
      最近更新 更多