C# x64平台上Math.Round的性能显著下降

C# x64平台上Math.Round的性能显著下降,c#,.net,performance,floating-point,C#,.net,Performance,Floating Point,我注意到,与x86相比,针对x64使用Math.Round将double转换为int时,性能显著下降(约15倍)。我在64位Windows Core i7 3770K上测试了它。有人能复制它吗?这种情况有什么好的理由吗?也许是一些奇怪的边界条件 仅供参考,我将Math.Round(Test1)与两种近似值进行了比较:条件转换(Test2)和6755399441055744技巧(Test3) 运行时间为: --------------------------- | | x86 |

我注意到,与x86相比,针对x64使用Math.Round将double转换为int时,性能显著下降(约15倍)。我在64位Windows Core i7 3770K上测试了它。有人能复制它吗?这种情况有什么好的理由吗?也许是一些奇怪的边界条件

仅供参考,我将
Math.Round
(Test1)与两种近似值进行了比较:条件转换(Test2)和6755399441055744技巧(Test3)

运行时间为:

---------------------------
|       |   x86  |  x64   |
|-------+--------+--------|
| Test1 | 0,0662 | 0,9975 |
| Test2 | 0,1517 | 0,1513 |
| Test3 | 0,1966 | 0,0978 |
---------------------------
以下是基准代码:

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace MathRoundTester
{
    class Program
    {
        private const int IterationCount = 1000000;

        private static int dummy;
        static void Main(string[] args)
        {
            var data = new double[100];
            var rand = new Random(0);
            for (int i = 0; i < data.Length; ++i)
            {
                data[i] = rand.NextDouble() * int.MaxValue * 2 +
                    int.MinValue + rand.NextDouble();
            }

            dummy ^= Test1(data);
            dummy ^= Test2(data);
            dummy ^= Test3(data);
            RecordTime(data, Test1);
            RecordTime(data, Test2);
            RecordTime(data, Test3);
            Console.WriteLine(dummy);
            Console.Read();
        }
        private static void RecordTime(double[] data, Func<double[], int> action)
        {
            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.Collect();

            var sw = Stopwatch.StartNew();
            dummy ^= action(data);
            sw.Stop();
            Console.WriteLine((sw.ElapsedTicks / (double)Stopwatch.Frequency).ToString("F4"));
        }
        private static int Test1(double[] data)
        {
            int d = 0;
            for (int i = 0; i < IterationCount; ++i)
            {
                for (int j = 0; j < data.Length; ++j)
                {
                    var x = data[j];
                    d ^= (int)Math.Round(x);
                }
            }
            return d;
        }
        private static int Test2(double[] data)
        {
            int d = 0;
            for (int i = 0; i < IterationCount; ++i)
            {
                for (int j = 0; j < data.Length; ++j)
                {
                    var x = data[j];
                    d ^= x > 0 ? (int)(x + 0.5) : (int)(x - 0.5);
                }
            }
            return d;
        }
        [StructLayout(LayoutKind.Explicit)]
        private struct DoubleIntUnion
        {
            public DoubleIntUnion(double a)
            {
                Int = 0;
                Double = a;
            }
            [FieldOffset(0)]
            public double Double;
            [FieldOffset(0)]
            public int Int;
        }
        private static int Test3(double[] data)
        {
            int d = 0;
            for (int i = 0; i < IterationCount; ++i)
            {
                for (int j = 0; j < data.Length; ++j)
                {
                    var x = data[j];
                    d ^= new DoubleIntUnion(x + 6755399441055744.0).Int;
                }
            }
            return d;
        }
    }
}
使用系统;
使用系统诊断;
使用System.Runtime.InteropServices;
名称空间MathRoundTester
{
班级计划
{
私有常量int迭代计数=1000000;
私有静态int-dummy;
静态void Main(字符串[]参数)
{
var数据=新的双精度[100];
var rand=新随机变量(0);
对于(int i=0;i0?(int)(x+0.5):(int)(x-0.5);
}
}
返回d;
}
[StructLayout(LayoutKind.Explicit)]
私有结构双直觉
{
公共双重直觉(双重a)
{
Int=0;
双=a;
}
[字段偏移量(0)]
公双双;
[字段偏移量(0)]
公共int;
}
私有静态int Test3(双[]数据)
{
int d=0;
for(int i=0;i
更新2016-11-23:


在AndreyAkinshin善意地在dotnet/coreclr回购协议上发布了一条消息后不久,该消息被添加到1.2.0里程碑中。因此,这个问题似乎只是一个疏忽,将得到解决。

让我们看看
(int)Math.Round(data[j])
的asm

LegacyJIT-x86:

01172EB0  fld         qword ptr [eax+edi*8+8]  
01172EB4  fistp       dword ptr [ebp-14h]
RyuJIT-x64:

`d7350617 c4e17b1044d010  vmovsd  xmm0,qword ptr [rax+rdx*8+10h]
`d735061e e83dce605f      call    clr!COMDouble::Round (`3695d460)
`d7350623 c4e17b2ce8      vcvttsd2si ebp,xmm0
clr的源代码!COMDouble::Round

clr!COMDouble::Round:
`3695d460 4883ec58        sub     rsp,58h
`3695d464 0f29742440      movaps  xmmword ptr [rsp+40h],xmm6
`3695d469 0f57c9          xorps   xmm1,xmm1
`3695d46c f2480f2cc0      cvttsd2si rax,xmm0
`3695d471 0f297c2430      movaps  xmmword ptr [rsp+30h],xmm7
`3695d476 0f28f0          movaps  xmm6,xmm0
`3695d479 440f29442420    movaps  xmmword ptr [rsp+20h],xmm8
`3695d47f f2480f2ac8      cvtsi2sd xmm1,rax
`3695d484 660f2ec1        ucomisd xmm0,xmm1
`3695d488 7a17            jp      clr!COMDouble::Round+0x41 (`3695d4a1)
`3695d48a 7515            jne     clr!COMDouble::Round+0x41 (`3695d4a1)
`3695d48c 0f28742440      movaps  xmm6,xmmword ptr [rsp+40h]
`3695d491 0f287c2430      movaps  xmm7,xmmword ptr [rsp+30h]
`3695d496 440f28442420    movaps  xmm8,xmmword ptr [rsp+20h]
`3695d49c 4883c458        add     rsp,58h
`3695d4a0 c3              ret
`3695d4a1 440f28c0        movaps  xmm8,xmm0
`3695d4a5 f2440f5805c23a7100 
            addsd xmm8,mmword ptr [clr!_real (`37070f70)] ds:`37070f70=3fe0000000000000
`3695d4ae 410f28c0        movaps  xmm0,xmm8
`3695d4b2 e821000000      call    clr!floor (`3695d4d8)
`3695d4b7 66410f2ec0      ucomisd xmm0,xmm8
`3695d4bc 0f28f8          movaps  xmm7,xmm0
`3695d4bf 7a06            jp      clr!COMDouble::Round+0x67 (`3695d4c7)
`3695d4c1 0f8465af3c00    je      clr! ?? ::FNODOBFM::`string'+0xdd8c4 (`36d2842c)
`3695d4c7 0f28ce          movaps  xmm1,xmm6
`3695d4ca 0f28c7          movaps  xmm0,xmm7
`3695d4cd ff1505067000    call    qword ptr [clr!_imp__copysign (`3705dad8)]
`3695d4d3 ebb7            jmp     clr!COMDouble::Round+0x2c (`3695d48c)
如您所见,LegacyJIT-x86使用了一个非常快的对;根据报告,Haswell的数据如下:

Instruction | Latency | Reciprocal throughput
------------|---------|----------------------
FLD m32/64  | 3       | 0.5
FIST(P) m   | 7       | 1
RyuJIT-x64直接调用
clr!COMDouble::Round
(LegacyJIT-x64也这样做)。您可以在repo中找到此方法的源代码。如果您使用的是
版本1.0.0
,则需要:

如果您使用的是主分支,则可以在中找到类似的代码

整个.NET框架似乎使用了相同的逻辑

因此,
(int)Math.Round
x86
上的运行速度确实比在
x64
上快得多,因为不同JIT编译器的内部实现不同。请注意,此行为在将来可能会更改

顺便说一下,您可以在BenchmarkDotNet的帮助下编写一个小而可靠的基准:

[LegacyJitX86Job, LegacyJitX64Job, RyuJitX64Job]
public class MathRoundBenchmarks
{
    private const int N = 100;
    private double[] data;

    [Setup]
    public void Setup()
    {
        var rand = new Random(0);
        data = new double[N];
        for (int i = 0; i < data.Length; ++i)
        {
            data[i] = rand.NextDouble() * int.MaxValue * 2 +
                      int.MinValue + rand.NextDouble();
        }
    }

    [Benchmark(OperationsPerInvoke = N)]
    public int MathRound()
    {
        int d = 0;
        for (int i = 0; i < data.Length; ++i)
            d ^= (int) Math.Round(data[i]);
        return d;
    }
}

不是这样的答案,而是根据精确的舍入要求,其他人可能会在x64系统的性能关键区域发现一些有用的代码

100000000次操作的性能时间(毫秒)为:

Round(x):            1112
Round(x,y):          2183
FastMath.Round(x):   155
FastMath.Round(x,y): 519
代码:

公共静态类FastMath
{
私有静态只读双[]RoundLookup=CreateRoundLookup();
私有静态双[]CreateRoundLookup()
{
双精度[]结果=新双精度[15];
for(int i=0;i
当您以x64为目标时,浮点运算的处理方式非常不同。在32位模式下,抖动使用传统FPU。在64位模式下,可以确保处理器支持SSE2。这不是FPU的完全替代品。x86抖动可以依赖于FISTP指令,从而使Math.Round()成为一个内在函数。换句话说,不是方法调用,而是单处理器指令。x64抖动没有这样的运气,您看到了必须调用CLR到辅助函数的开销。您可以使用middpointRounding.AwayFromZero使32位版本变慢。现在
[LegacyJitX86Job, LegacyJitX64Job, RyuJitX64Job]
public class MathRoundBenchmarks
{
    private const int N = 100;
    private double[] data;

    [Setup]
    public void Setup()
    {
        var rand = new Random(0);
        data = new double[N];
        for (int i = 0; i < data.Length; ++i)
        {
            data[i] = rand.NextDouble() * int.MaxValue * 2 +
                      int.MinValue + rand.NextDouble();
        }
    }

    [Benchmark(OperationsPerInvoke = N)]
    public int MathRound()
    {
        int d = 0;
        for (int i = 0; i < data.Length; ++i)
            d ^= (int) Math.Round(data[i]);
        return d;
    }
}
BenchmarkDotNet.Core=v0.9.9.0
OS=Microsoft Windows NT 6.2.9200.0
Processor=Intel(R) Core(TM) i7-4702MQ CPU 2.20GHz, ProcessorCount=8
Frequency=2143475 ticks, Resolution=466.5321 ns, Timer=TSC
CLR=MS.NET 4.0.30319.42000, Arch=64-bit RELEASE [RyuJIT]
GC=Concurrent Workstation
JitModules=clrjit-v4.6.1586.0

Type=MathRoundBenchmarks  Mode=Throughput

    Method | Platform |       Jit |     Median |    StdDev |
---------- |--------- |---------- |----------- |---------- |
 MathRound |      X64 | LegacyJit | 12.8640 ns | 0.2796 ns |
 MathRound |      X64 |    RyuJit | 13.4390 ns | 0.4365 ns |
 MathRound |      X86 | LegacyJit |  1.0278 ns | 0.0373 ns |
Round(x):            1112
Round(x,y):          2183
FastMath.Round(x):   155
FastMath.Round(x,y): 519
public static class FastMath
{
    private static readonly double[] RoundLookup = CreateRoundLookup();

    private static double[] CreateRoundLookup()
    {
        double[] result = new double[15];
        for (int i = 0; i < result.Length; i++)
        {
            result[i] = Math.Pow(10, i);
        }

        return result;
    }

    public static double Round(double value)
    {
        return Math.Floor(value + 0.5);
    }

    public static double Round(double value, int decimalPlaces)
    {
        double adjustment = RoundLookup[decimalPlaces];
        return Math.Floor(value * adjustment + 0.5) / adjustment;
    }
}