2016-11-07 11 views
13

x86に比べてx64をターゲットにしてMath.Roundを使用してintをdoubleに変換すると、パフォーマンスが非常に低下しています。私はCore i7 3770K上の64ビットWindowsでテストしました。誰でもそれを再現できますか?これがなぜこのような理由があるのでしょうか?多分奇妙な境界条件でしょうか?x64プラットフォームでMath.Roundのパフォーマンスが大幅に低下しました

私はMath.Round(Test1)と2つの近似を比較しました。条件付きキャスト(Test2)と6755399441055744トリック(Test3)です。回を実行

は以下のとおりです。AndreyAkinshinは親切DOTNETにquestionを掲示した後、いくつかの時間

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; 
     } 
    } 
} 

アップデート2016年11月23日:

--------------------------- 
|  | x86 | x64 | 
|-------+--------+--------| 
| Test1 | 0,0662 | 0,9975 | 
| Test2 | 0,1517 | 0,1513 | 
| Test3 | 0,1966 | 0,0978 | 
--------------------------- 

ここではベンチマークのコードです/ coreclr repoでは、1.2.0マイルストーンに追加されました。だから、この問題は単なる見落としであり修正されるようです。

+5

浮動小数点演算が行われている*非常に*異なっあなたはx64のを対象とする場合。 32ビットモードでは、ジッタは従来のFPUを使用します。 64ビットモードでは、プロセッサがSSE2をサポートしていることが確認できます。これはFPUの完全な置き換えではありません。 x86ジッタは、MIS.Round()をintrinsic *にするFISTP命令に依存することができます。言い換えれば、メソッド呼び出しではなく、単一のプロセッサ命令のみです。このようなx64ジッタの不運は、CLRをヘルパー関数に呼び出す必要がないというオーバーヘッドが見えています。 –

+1

MidpointRounding.AwayFromZeroを使用して、32ビット版を遅くすることもできます。今や、組み込み関数はもう動作しません。 SSE2の典型的な結果である64ビット版は高速です。なぜなら、フレームワークがデフォルトの丸めモードとしてウォンキー銀行の丸めを好んだ理由を教えてくれるからです。 –

+0

私のコアi7-4700HQで確認するとさらに悪くなります - 0.0723(32ビット)vs 1,1548(64ビット) –

答えて

9

(int) Math.Round(data[j])のasmを見てみましょう。

LegacyJIT-のx86:

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

RyuJIT-x64の:clr!COMDouble::Round

`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: 
`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のはfld非常に高速を使用しています - fistp対; Instruction tables by Agner Fogによると、私たちはハスウェルための次の番号を持っている:

Instruction | Latency | Reciprocal throughput 
------------|---------|---------------------- 
FLD m32/64 | 3  | 0.5 
FIST(P) m | 7  | 1 

RyuJIT-x64のは、直接(LegacyJIT-x64のは、同じことを行う)clr!COMDouble::Roundを呼び出します。このメソッドのソースコードはdotnet/coreclrリポジトリにあります。あなたはrelease-1.0.0で作業している場合は、floatnative.cppを必要とする:

#if defined(_TARGET_X86_) 
__declspec(naked) 
double __fastcall COMDouble::Round(double d) 
{ 
    LIMITED_METHOD_CONTRACT; 

    __asm { 
     fld QWORD PTR [ESP+4] 
     frndint 
     ret 8 
    } 
} 

#else // !defined(_TARGET_X86_) 
FCIMPL1_V(double, COMDouble::Round, double d) 
    FCALL_CONTRACT; 

    double tempVal; 
    double flrTempVal; 
    // If the number has no fractional part do nothing 
    // This shortcut is necessary to workaround precision loss in borderline cases on some platforms 
    if (d == (double)(__int64)d) 
     return d; 
    tempVal = (d+0.5); 
    //We had a number that was equally close to 2 integers. 
    //We need to return the even one. 
    flrTempVal = floor(tempVal); 
    if (flrTempVal==tempVal) { 
     if (0 != fmod(tempVal, 2.0)) { 
      flrTempVal -= 1.0; 
     } 
    } 
    flrTempVal = _copysign(flrTempVal, d); 
    return flrTempVal; 
FCIMPLEND 
#endif // defined(_TARGET_X86_) 

あなたはmasterブランチで作業している場合は、floatdouble.cppで同様のコードを見つけることができます。

FCIMPL1_V(double, COMDouble::Round, double x) 
    FCALL_CONTRACT; 

    // If the number has no fractional part do nothing 
    // This shortcut is necessary to workaround precision loss in borderline cases on some platforms 
    if (x == (double)((INT64)x)) { 
     return x; 
    } 

    // We had a number that was equally close to 2 integers. 
    // We need to return the even one. 

    double tempVal = (x + 0.5); 
    double flrTempVal = floor(tempVal); 

    if ((flrTempVal == tempVal) && (fmod(tempVal, 2.0) != 0)) { 
     flrTempVal -= 1.0; 
    } 

    return _copysign(flrTempVal, x); 
FCIMPLEND 

完全な.NET Frameworkでは同じロジックが使用されているようです。

したがって、では、x64よりも、(int)Math.Roundが実際にはるかに速く動作します。これは、異なる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; 
    } 
} 

結果:

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 | 
+0

私は本当にそのような徹底的な答えに感謝します。あなたは "この行動は将来変更される可能性がある"と言いますが、本当にそれはできますか?その全体の方法は基本的に切り捨て付き整数への変換(cvttsd2si)を使う前に銀行家の丸めをエミュレートします。 RNモードで単純にcvtsd2si(丸め付き変換)を使用しないで、最も近い(偶数に丸める)非常に良い理由があります。そして、それは理由を知らずに私を生きて食べ続けます!私は(まだ)cvtsd2siとfistpの間に矛盾した振る舞いで何かを見つけることができませんでした。 – user98418468459

+0

@ user98418468459、私はdotnet/coreclrリポジトリに質問を投稿しました:https://github.com/dotnet/coreclr/issues/8053 – AndreyAkinshin

+0

これまでのところ、私はfistp/cvtsd2siの問題でしか見つかっていません:[link](https ://software.intel.com/en-us/articles/fast-floating-point-to-integer-conversions)(精密考察を参照してください)。しかし、x87スタックの80ビットの浮動小数点数で始める必要があります。 64ビットのメモリから毎回ロードするだけでは、当てはまりません。 – user98418468459

0

ないような答えが、他の人が見つけることがいくつかのコードを正確な丸め要件に応じて、x64システムのパフォーマンス上重要な領域に役立ちます。 100000000の操作のためのミリ秒で

パフォーマンス時間は以下のとおりです。

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; 
    } 
} 
関連する問題