【问题标题】:Is it possible that doubles are x2 FASTER than float? [duplicate]双打有可能比浮点数快 2 倍吗? [复制]
【发布时间】:2013-12-09 21:29:39
【问题描述】:

我执行了一些基准测试来比较双精度和浮点性能。看到双打比浮点快得多,我感到非常惊讶。

我看到了一些关于这个的讨论,例如:

Is using double faster than float?

Are doubles faster than floats in c#?

他们中的大多数人表示,由于双精度优化等原因,可能 double 和 float 性能会相似。但是我看到使用双打时 x2 的性能提升!!这怎么可能?最糟糕的是,我使用的是 32 位机器,根据一些帖子,它确实有望在浮点数上表现更好......

我使用 C# 对其进行了精确检查,但我发现类似的 C++ 实现具有类似的行为。

我用来检查的代码:

static void Main(string[] args)
{
  double[,] doubles = new double[64, 64];
  float[,] floats = new float[64, 64];

  System.Diagnostics.Stopwatch s = new System.Diagnostics.Stopwatch();

  s.Restart();
  CalcDoubles(doubles);
  s.Stop();
  long doubleTime = s.ElapsedMilliseconds;

  s.Restart();
  CalcFloats(floats);
  s.Stop();
  long floatTime = s.ElapsedMilliseconds;

  Console.WriteLine("Doubles time: " + doubleTime + " ms");
  Console.WriteLine("Floats time: " + floatTime + " ms");
}

private static void CalcDoubles(double[,] arr)
{
  unsafe
  {
    fixed (double* p = arr)
    {
      for (int b = 0; b < 192 * 12; ++b)
      {
        for (int i = 0; i < 64; ++i)
        {
          for (int j = 0; j < 64; ++j)
          {
            double* addr = (p + i * 64 + j);
            double arrij = *addr;
            arrij = arrij == 0 ? 1.0f / (i * j) : arrij * (double)i / j;
            *addr = arrij;
          }
        }
      }
    }
  }
}

private static void CalcFloats(float[,] arr)
{
  unsafe
  {
    fixed (float* p = arr)
    {
      for (int b = 0; b < 192 * 12; ++b)
      {
        for (int i = 0; i < 64; ++i)
        {
          for (int j = 0; j < 64; ++j)
          {
            float* addr = (p + i * 64 + j);
            float arrij = *addr;
            arrij = arrij == 0 ? 1.0f / (i * j) : arrij * (float)i / j;
            *addr = arrij;
          }
        }
      }
    }
  }
}

我正在使用一个性能很差的笔记本:Intel Atom N455 处理器(双核,1.67GHz,32 位),2GB RAM。

【问题讨论】:

  • 感谢您的回复。请注意,在双精度和浮点情况下,计数器都会以相同的偏移量递增(它是在 for 循环中手动计算的)。
  • 你不需要不安全的代码来重现这个。如果将赋值移回数组,效果就会消失。
  • 你是对的,它也发生在我的机器上。那为什么呢?也许缓存回写/直写策略(如前所述)?
  • @ZongZhengLi 这支持这样的假设,即使用双精度执行计算,并且浮点的额外处理时间专门用于将它们转换为 32 位格式以存储在数组中。跨度>

标签: c# performance intel processor


【解决方案1】:

这看起来抖动优化器在这里丢球,它不会抑制浮点情况下的冗余存储。热门代码是 1.0f / (i * j) 计算,因为所有数组值都是 0。x86 抖动生成:

01062928  mov         eax,edx                     ; eax = i
0106292A  imul        eax,esi                     ; eax = i * j
0106292D  mov         dword ptr [ebp-10h],eax     ; store to mem
01062930  fild        dword ptr [ebp-10h]         ; convert to double 
01062933  fstp        dword ptr [ebp-10h]         ; redundant store, convert to float
01062936  fld         dword ptr [ebp-10h]         ; redundant load
01062939  fld1                                    ; 1.0f
0106293B  fdivrp      st(1),st                    ; 1.0f / (i * j)
0106293D  fstp        dword ptr [ecx]             ; arrij = result

x64 抖动:

00007FFCFD6440B0  cvtsi2ss    xmm0,r10d           ; (float)(i * j)
00007FFCFD6440B5  movss       xmm1,dword ptr [7FFCFD644118h]  ; 1.0f
00007FFCFD6440BD  divss       xmm1,xmm0           ; 1.0f / (i * j)
00007FFCFD6440C1  cvtss2sd    xmm0,xmm1           ; redundant store 
00007FFCFD6440C5  cvtsd2ss    xmm0,xmm0           ; redundant load
00007FFCFD6440C9  movss       dword ptr [rax+r11],xmm0  ; arrij = result

我用“冗余”标记了多余的说明。优化器确实设法在 double 版本中消除它们,以便代码运行得更快。

冗余存储实际上存在于 C# 编译器生成的 IL 中,优化器的工作是检测和删除它们。值得注意的是,x86 和 x64 抖动都有这个缺陷,因此它看起来像是优化器算法中的一般疏忽。

x64 代码在将浮点结果转换为双精度然后再次返回浮点时特别值得注意,这表明潜在的问题是它不知道如何抑制的数据类型转换。您还可以在 x86 代码中看到它,冗余存储实际上进行了 double 到 float 的转换。在 x86 情况下消除转换看起来很困难,因此这很可能已泄漏到 x64 抖动中。

请注意,x64 代码的运行速度明显快于 x86 代码,因此请务必将 Platform target 设置为 AnyCPU 以获得简单的胜利。至少部分加速是优化器在提升整数乘法方面的聪明才智。

并确保测试真实数据,由于未初始化的数组内容,您的测量从根本上是无效的。对于元素中的非零数据,差异不那么明显,这使得除法更加昂贵。

还要注意你在双重情况下的错误,你不应该在那里使用 1.0f。

【讨论】:

  • 有趣。 “冗余存储,转换为浮点数”是否会截断双精度值中的任何额外精度,以符合在显式转换时消除这种额外精度的要求?
  • 是的,我确实认为这是核心问题。 FPU 只是不太喜欢 float。为它生成有效的代码通常是一种邪恶的黑魔法。 SSE2 有了很大改进。
  • 非常有用,感谢您的详细回答。但是后来我不明白为什么消除回写(正如宗政李的评论所说)效果消失了?我尝试在 Debug 模式下运行以消除不需要 writeback 时的优化,其行为与 Release 模式非常相似。
  • 如果您删除分配,那么优化器会删除整个计算,因为它的结果是无用的。删除的代码并不比删除的代码慢或快。
  • 我考虑过,所以正如我所说,我也在调试模式下运行以避免此类优化,它没有任何区别。
【解决方案2】:

来自 C# 规范:

浮点运算的执行精度可能高于 操作的结果类型。例如,一些硬件 架构支持“扩展”或“长双精度”浮点 比双精度类型具有更大范围和精度的类型,并且 使用这个更高的隐式执行所有浮点运算 精密型。只有在性能上付出过高的代价才能这样 硬件架构可以执行浮点运算 精度较低,而不是需要实现 丧失性能和精度,C# 允许更高的精度 用于所有浮点运算的类型。以外 提供更精确的结果,这很少有任何可衡量的 效果。然而,在 x * y / z 形式的表达式中, 乘法产生的结果超出双精度范围,但 随后的除法将临时结果带回 双倍范围,表达式以更高的值计算的事实 范围格式可能会导致产生有限的结果,而不是 无穷大。

在将值存储到数组之前,可能需要额外的指令来将值转换为 32 位浮点数。

此外,正如您链接到的问题之一的accepted answer 中所述,CLI 规范要求在某些其他情况下截断 64 位(或 80 位)值。该答案还链接到此处的其他讨论:

http://weblog.ikvm.net/PermaLink.aspx?guid=f300c4e1-15b0-45ed-b6a6-b5dc8fb8089e

【讨论】:

    猜你喜欢
    • 2010-09-14
    • 2013-07-26
    • 1970-01-01
    • 1970-01-01
    • 2010-09-24
    • 2016-07-23
    • 1970-01-01
    • 2014-04-25
    • 2014-03-02
    相关资源
    最近更新 更多