【问题标题】:Double.IsNaN test 100 times faster?Double.IsNaN 测试快 100 倍?
【发布时间】:2014-08-11 11:07:42
【问题描述】:

我在.NET Source Code 中发现了这个:它声称比System.Double.IsNaN 快100 倍。是否有理由不使用此功能而不是System.Double.IsNaN

[StructLayout(LayoutKind.Explicit)]
private struct NanUnion
{
    [FieldOffset(0)] internal double DoubleValue;
    [FieldOffset(0)] internal UInt64 UintValue;
}

// The standard CLR double.IsNaN() function is approximately 100 times slower than our own wrapper,
// so please make sure to use DoubleUtil.IsNaN() in performance sensitive code.
// PS item that tracks the CLR improvement is DevDiv Schedule : 26916.
// IEEE 754 : If the argument is any value in the range 0x7ff0000000000001L through 0x7fffffffffffffffL 
// or in the range 0xfff0000000000001L through 0xffffffffffffffffL, the result will be NaN.         
public static bool IsNaN(double value)
{
    NanUnion t = new NanUnion();
    t.DoubleValue = value;

    UInt64 exp = t.UintValue & 0xfff0000000000000;
    UInt64 man = t.UintValue & 0x000fffffffffffff;

    return (exp == 0x7ff0000000000000 || exp == 0xfff0000000000000) && (man != 0);
}

编辑:还是according to the .NET Source CodeSystem.Double.IsNaN 的代码如下:

public unsafe static bool IsNaN(double d)
{
    return (*(UInt64*)(&d) & 0x7FFFFFFFFFFFFFFFL) > 0x7FF0000000000000L;
}

【问题讨论】:

  • 嗯,评论说它更快,所以它可能更快......你有基准测试吗?这是一个有趣的问题(尤其是“使用较慢的任何理由”部分。
  • 在不知道任何关于测试 NaN 或此方法的正确性的情况下,我建议坚持使用 .NET 实现,假设存在任何性能差异的充分理由。 .NET 开发人员知道他们在做什么,并且必须考虑所有边缘情况等,以生成一个强大的库。
  • 好吧,那个类是internal,所以你最终会复制那个源代码并且必须维护它以备将来使用。
  • @paqogomez 不是真的。这是一个有趣的问题。
  • @paqogomez 我读过这篇引文的次数,我开始讨厌它了……这不是这里的主题。

标签: c# floating-point nan


【解决方案1】:

这是一个简单的基准:

public static void Main()
{
    int iterations = 500 * 1000 * 1000;

    double nan = double.NaN;
    double notNan = 42;

    Stopwatch sw = Stopwatch.StartNew();

    bool isNan;
    for (int i = 0; i < iterations; i++)
    {
        isNan = IsNaN(nan);     // true
        isNan = IsNaN(notNan);  // false
    }

    sw.Stop();
    Console.WriteLine("IsNaN: {0}", sw.ElapsedMilliseconds);

    sw = Stopwatch.StartNew();

    for (int i = 0; i < iterations; i++)
    {
        isNan = double.IsNaN(nan);     // true
        isNan = double.IsNaN(notNan);  // false
    }

    sw.Stop();
    Console.WriteLine("double.IsNaN: {0}", sw.ElapsedMilliseconds);

    Console.Read();
}

显然他们错了:

IsNaN: 15012

double.IsNaN: 6243


编辑 + 注意:我确信时间会根据输入值、许多其他因素等而改变,但声称 一般而言这个包装器比默认实现快 100 倍似乎是错误的.

【讨论】:

  • 这也是我注意到的。即使经过 100 万次迭代,double.IsNaN 的速度实际上也是两倍。我想知道该评论是否来自 .NET 1.1 或其他版本。
  • @TyCobb 我用更多的迭代更新了结果,以反映你提到的“两倍快”。另外 +1 对 .NET 1.1 的评论,double.IsNan 在过去可能没有那么快。
  • +1 我也做了同样的事情。我的测试(发布模式,无调试器)显示 double.IsNan 的速度接近 10 倍。
  • 评论未编译。
【解决方案2】:

我称之为恶作剧。 “快速”版本具有相当多的操作数,甚至执行更多的内存读取(堆栈,所以在 L1 中,但仍然比寄存器慢)。

00007FFAC53D3D01  movups      xmmword ptr [rsp+8],xmm0  
00007FFAC53D3D06  sub         rsp,48h  
00007FFAC53D3D0A  mov         qword ptr [rsp+20h],0  
00007FFAC53D3D13  mov         qword ptr [rsp+28h],0  
00007FFAC53D3D1C  mov         qword ptr [rsp+30h],0  
00007FFAC53D3D25  mov         rax,7FFAC5423D40h  
00007FFAC53D3D2F  mov         eax,dword ptr [rax]  
00007FFAC53D3D31  test        eax,eax  
00007FFAC53D3D33  je          00007FFAC53D3D3A  
00007FFAC53D3D35  call        00007FFB24EE39F0  
00007FFAC53D3D3A  mov         r8d,8  
00007FFAC53D3D40  xor         edx,edx  
00007FFAC53D3D42  lea         rcx,[rsp+20h]  
00007FFAC53D3D47  call        00007FFB24A21680  
            t.DoubleValue = value;
00007FFAC53D3D4C  movsd       xmm5,mmword ptr [rsp+50h]  
00007FFAC53D3D52  movsd       mmword ptr [rsp+20h],xmm5  

            UInt64 exp = t.UintValue & 0xfff0000000000000;
00007FFAC53D3D58  mov         rax,qword ptr [rsp+20h]  
00007FFAC53D3D5D  mov         rcx,0FFF0000000000000h  
00007FFAC53D3D67  and         rax,rcx  
00007FFAC53D3D6A  mov         qword ptr [rsp+28h],rax  
            UInt64 man = t.UintValue & 0x000fffffffffffff;
00007FFAC53D3D6F  mov         rax,qword ptr [rsp+20h]  
00007FFAC53D3D74  mov         rcx,0FFFFFFFFFFFFFh  
00007FFAC53D3D7E  and         rax,rcx  
00007FFAC53D3D81  mov         qword ptr [rsp+30h],rax  

            return (exp == 0x7ff0000000000000 || exp == 0xfff0000000000000) && (man != 0);
00007FFAC53D3D86  mov         rax,7FF0000000000000h  
00007FFAC53D3D90  cmp         qword ptr [rsp+28h],rax  
00007FFAC53D3D95  je          00007FFAC53D3DA8  
00007FFAC53D3D97  mov         rax,0FFF0000000000000h  
00007FFAC53D3DA1  cmp         qword ptr [rsp+28h],rax  
00007FFAC53D3DA6  jne         00007FFAC53D3DBD  
00007FFAC53D3DA8  xor         eax,eax  
00007FFAC53D3DAA  cmp         qword ptr [rsp+30h],0  
00007FFAC53D3DB0  setne       al  
00007FFAC53D3DB3  mov         dword ptr [rsp+38h],eax  
00007FFAC53D3DB7  mov         al,byte ptr [rsp+38h]  
00007FFAC53D3DBB  jmp         00007FFAC53D3DC1  
00007FFAC53D3DBD  xor         eax,eax  
00007FFAC53D3DBF  jmp         00007FFAC53D3DC1  
00007FFAC53D3DC1  nop  
00007FFAC53D3DC2  add         rsp,48h  
00007FFAC53D3DC6  ret  

与 .NET 版本相比:

            return (*(UInt64*)(&d) & 0x7FFFFFFFFFFFFFFFL) > 0x7FF0000000000000L;
00007FFAC53D3DE0  movsd       mmword ptr [rsp+8],xmm0  
00007FFAC53D3DE6  sub         rsp,38h  
00007FFAC53D3DEA  mov         rax,7FFAC5423D40h  
00007FFAC53D3DF4  mov         eax,dword ptr [rax]  
00007FFAC53D3DF6  test        eax,eax  
00007FFAC53D3DF8  je          00007FFAC53D3DFF  
00007FFAC53D3DFA  call        00007FFB24EE39F0  
00007FFAC53D3DFF  mov         rdx,qword ptr [rsp+40h]  
00007FFAC53D3E04  mov         rax,7FFFFFFFFFFFFFFFh  
00007FFAC53D3E0E  and         rdx,rax  
00007FFAC53D3E11  xor         ecx,ecx  
00007FFAC53D3E13  mov         rax,7FF0000000000000h  
00007FFAC53D3E1D  cmp         rdx,rax  
00007FFAC53D3E20  seta        cl  
00007FFAC53D3E23  mov         dword ptr [rsp+20h],ecx  
00007FFAC53D3E27  movzx       eax,byte ptr [rsp+20h]  
00007FFAC53D3E2C  jmp         00007FFAC53D3E2E  
00007FFAC53D3E2E  nop  
00007FFAC53D3E2F  add         rsp,38h  
00007FFAC53D3E33  ret  

【讨论】:

    【解决方案3】:

    它声称比 System.Double.IsNaN 快 100 倍

    是的,曾经是真的。你错过了时间机器来知道这个决定是什么时候做出的。 Double.IsNaN() 以前不是那样的。来自SSCLI10源代码:

       public static bool IsNaN(double d)
       {
           // Comparisions of a NaN with another number is always false and hence both conditions will be false.
           if (d < 0d || d >= 0d) {
              return false;
           }
           return true;
       }
    

    如果d 是 NaN,那么在 32 位代码中,它在 FPU 上的性能非常很差。只是芯片设计的一个方面,它在微码中被视为例外。英特尔处理器手册对此只字未提,只是记录了一个跟踪“浮点辅助”数量的处理器性能计数器,并指出微码定序器对非正规数和 NaN 起作用,“可能会增加成本 数百个周期”。在 64 位代码中不是问题,它使用没有此性能命中的 SSE2 指令。

    一些代码可以自己查看:

    using System;
    using System.Diagnostics;
    
    class Program {
        static void Main(string[] args) {
            double d = double.NaN;
            for (int test = 0; test < 10; ++test) {
                var sw1 = Stopwatch.StartNew();
                bool result1 = false;
                for (int ix = 0; ix < 1000 * 1000; ++ix) {
                    result1 |= double.IsNaN(d);
                }
                sw1.Stop();
                var sw2 = Stopwatch.StartNew();
                bool result2 = false;
                for (int ix = 0; ix < 1000 * 1000; ++ix) {
                    result2 |= IsNaN(d);
                }
                sw2.Stop();
                Console.WriteLine("{0} - {1} x {2}%", sw1.Elapsed, sw2.Elapsed, 100 * sw2.ElapsedTicks / sw1.ElapsedTicks, result1, result2);
    
            }
            Console.ReadLine();
        }
        public static bool IsNaN(double d) {
            // Comparisions of a NaN with another number is always false and hence both conditions will be false.
            if (d < 0d || d >= 0d) {
                return false;
            }
            return true;
        }
    }
    

    使用经过微优化的 Double.IsNaN() 版本。顺便说一句,这样的微优化在框架中并不是邪恶的,Microsoft .NET 程序员的巨大负担是他们很少能猜到他们的代码何时处于应用程序的关键路径中。

    针对 32 位代码(Haswell 移动内核)时在我的机器上的结果:

    00:00:00.0027095 - 00:00:00.2427242 x 8957%
    00:00:00.0025248 - 00:00:00.2191291 x 8678%
    00:00:00.0024344 - 00:00:00.2209950 x 9077%
    00:00:00.0024144 - 00:00:00.2321169 x 9613%
    00:00:00.0024126 - 00:00:00.2173313 x 9008%
    00:00:00.0025488 - 00:00:00.2237517 x 8778%
    00:00:00.0026940 - 00:00:00.2231146 x 8281%
    00:00:00.0025052 - 00:00:00.2145660 x 8564%
    00:00:00.0025533 - 00:00:00.2200943 x 8619%
    00:00:00.0024406 - 00:00:00.2135839 x 8751%
    

    【讨论】:

    • 在阅读这个答案时,我在想,谁知道这么多东西,然后我看到了海报的名字。 +1 并感谢您为我们提供如此精彩的答案
    • 刚刚在 C、64 位、2.4 GHz 的 Core 2 Duo 上测量:比较 (d != d) 的最简单方法在 2.7 纳秒内给出正确结果。比较! (d = 0) 在 3.5 纳秒内给出正确的结果。建议的位模式测试需要 4.7 纳秒(全部测量超过 100 亿次迭代)。关闭优化的所有测量,否则所有比较都将被优化。
    • 嗯,“优化关闭”只是不会产生有意义的数字。请注意我发布的 sn-p 中 result1 和 result2 的奇怪用法,包括在 Console.WriteLine() 调用中使用它们而不实际显示它们。基准测试是一门棘手的艺术:)
    • 我们讨论的是每个循环需要 6 到 12 个时钟周期的代码。加上涉及到 NaN 的浮点比较慢得离谱的假设。所以这个测试是绝对有意义的。当您比较 NaN 时,Core 2 Duo 处理器不会减慢速度,Clang 不会优化 x == x 的比较,并且测试 (x != x) 是检查数字是否为 NaN 的最快方法。无需任何技巧。
    • 叹息。不要将 SSE2 与 FPU 代码进行比较。我已经注意到 .NET 中的 x64 抖动没有这个问题。
    猜你喜欢
    • 1970-01-01
    • 2020-09-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多