Zauważyłem bardzo znaczący (~ 15x) spadek wydajności podczas używania Math.Round do konwersji podwójnej na int podczas kierowania na x64 w porównaniu do x86. Przetestowałem go na 64-bitowym systemie Windows na Core i7 3770K. Czy ktoś może go odtworzyć? Czy jest jakiś dobry powód, dlaczego tak się dzieje? Może jakieś dziwne warunki brzegowe?Znaczący spadek wydajności Math.Round na platformie x64
Dla porównania, porównałem Math.Round
(Test1) z 2 przybliżeniami: podpowiedź warunkowa (Test2) i sztuczka 6755399441055744 (Test3).
czasy przebiegu są:
---------------------------
| | x86 | x64 |
|-------+--------+--------|
| Test1 | 0,0662 | 0,9975 |
| Test2 | 0,1517 | 0,1513 |
| Test3 | 0,1966 | 0,0978 |
---------------------------
Oto kod odniesienia:
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;
}
}
}
Aktualizacja 23.11.2016:
Jakiś czas po AndreyAkinshin uprzejmie napisali question na dotnet/repozytorium coreclr zostało dodane do etapu 1.2.0. Wydaje się więc, że ten problem to tylko niedopatrzenie i zostanie naprawiony.
matematyki zmiennoprzecinkowej odbywa * bardzo * inaczej, gdy cel 64. W trybie 32-bitowym jitter wykorzystuje dotychczasową jednostkę FPU. W trybie 64-bitowym można mieć pewność, że procesor obsługuje SSE2. To nie jest kompletny zamiennik FPU. Jitter x86 może polegać na instrukcji FISTP, przez co Math.Round() jest * wewnętrzną *. Innymi słowy, nie jest to wywołanie metody, ale tylko jedna instrukcja procesora. Brak szczęścia dla jittera x64, widzisz narzut związany z koniecznością wywołania CLR do funkcji pomocnika. –
Możesz spowolnić 32-bitową wersję z MidpointRounding.AwayFromZero. Teraz samoistnie już nie działa. Wersja 64-bitowa jest szybsza, typowy wynik SSE2. Cóż, mówi, dlaczego korzystne ramy zaokrąglania wonky bankiera jako domyślny tryb :) –
zaokrąglania Wystarczy, aby potwierdzić - na mój rdzeń i7-4700HQ jest jeszcze gorzej - 0,0723 (32bit) vs 1,1548 (64bit) –