Kemarin saya menemukan artikel oleh Christoph Nahr berjudul ".NET Struct Performance" yang membandingkan beberapa bahasa (C ++, C #, Java, JavaScript) untuk metode yang menambahkan dua titik struct (double
tupel).
Ternyata, versi C ++ membutuhkan waktu sekitar 1000ms untuk dieksekusi (1e9 iterations), sementara C # tidak bisa di bawah ~ 3000ms pada mesin yang sama (dan berkinerja lebih buruk di x64).
Untuk mengujinya sendiri, saya mengambil kode C # (dan sedikit disederhanakan untuk memanggil hanya metode di mana parameter diteruskan oleh nilai), dan menjalankannya pada mesin i7-3610QM (dorongan 3.1Ghz untuk inti tunggal), RAM 8GB, Win8. 1, menggunakan .NET 4.5.2, RELEASE build 32-bit (x86 WoW64 karena OS saya 64-bit). Ini adalah versi yang disederhanakan:
public static class CSharpTest
{
private const int ITERATIONS = 1000000000;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Point AddByVal(Point a, Point b)
{
return new Point(a.X + b.Y, a.Y + b.X);
}
public static void Main()
{
Point a = new Point(1, 1), b = new Point(1, 1);
Stopwatch sw = Stopwatch.StartNew();
for (int i = 0; i < ITERATIONS; i++)
a = AddByVal(a, b);
sw.Stop();
Console.WriteLine("Result: x={0} y={1}, Time elapsed: {2} ms",
a.X, a.Y, sw.ElapsedMilliseconds);
}
}
Dengan Point
didefinisikan secara sederhana:
public struct Point
{
private readonly double _x, _y;
public Point(double x, double y) { _x = x; _y = y; }
public double X { get { return _x; } }
public double Y { get { return _y; } }
}
Menjalankannya menghasilkan hasil yang mirip dengan yang ada di artikel:
Result: x=1000000001 y=1000000001, Time elapsed: 3159 ms
Pengamatan aneh pertama
Karena metode ini harus sebaris, saya bertanya-tanya bagaimana kode akan bekerja jika saya menghapus struct sama sekali dan hanya memasukkan semuanya bersama-sama:
public static class CSharpTest
{
private const int ITERATIONS = 1000000000;
public static void Main()
{
// not using structs at all here
double ax = 1, ay = 1, bx = 1, by = 1;
Stopwatch sw = Stopwatch.StartNew();
for (int i = 0; i < ITERATIONS; i++)
{
ax = ax + by;
ay = ay + bx;
}
sw.Stop();
Console.WriteLine("Result: x={0} y={1}, Time elapsed: {2} ms",
ax, ay, sw.ElapsedMilliseconds);
}
}
Dan mendapatkan hasil yang hampir sama (sebenarnya 1% lebih lambat setelah beberapa kali percobaan ulang), yang berarti bahwa JIT-ter tampaknya melakukan pekerjaan yang baik dengan mengoptimalkan semua pemanggilan fungsi:
Result: x=1000000001 y=1000000001, Time elapsed: 3200 ms
Ini juga berarti bahwa tolok ukur tampaknya tidak mengukur struct
kinerja apa pun dan sebenarnya hanya tampak mengukur dasardouble
aritmatika (setelah yang lainnya dioptimalkan).
Hal-hal aneh
Sekarang sampai pada bagian yang aneh. Jika saya hanya menambahkan stopwatch lain di luar loop (ya, saya mempersempitnya ke langkah gila ini setelah beberapa percobaan ulang), kode berjalan tiga kali lebih cepat :
public static void Main()
{
var outerSw = Stopwatch.StartNew(); // <-- added
{
Point a = new Point(1, 1), b = new Point(1, 1);
var sw = Stopwatch.StartNew();
for (int i = 0; i < ITERATIONS; i++)
a = AddByVal(a, b);
sw.Stop();
Console.WriteLine("Result: x={0} y={1}, Time elapsed: {2} ms",
a.X, a.Y, sw.ElapsedMilliseconds);
}
outerSw.Stop(); // <-- added
}
Result: x=1000000001 y=1000000001, Time elapsed: 961 ms
Konyol! Dan itu tidak seperti ituStopwatch
memberi saya hasil yang salah karena saya dapat dengan jelas melihat bahwa itu berakhir setelah satu detik.
Adakah yang bisa memberi tahu saya apa yang mungkin terjadi di sini?
(Memperbarui)
Berikut adalah dua metode dalam program yang sama, yang menunjukkan bahwa alasannya bukan JIT:
public static class CSharpTest
{
private const int ITERATIONS = 1000000000;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Point AddByVal(Point a, Point b)
{
return new Point(a.X + b.Y, a.Y + b.X);
}
public static void Main()
{
Test1();
Test2();
Console.WriteLine();
Test1();
Test2();
}
private static void Test1()
{
Point a = new Point(1, 1), b = new Point(1, 1);
var sw = Stopwatch.StartNew();
for (int i = 0; i < ITERATIONS; i++)
a = AddByVal(a, b);
sw.Stop();
Console.WriteLine("Test1: x={0} y={1}, Time elapsed: {2} ms",
a.X, a.Y, sw.ElapsedMilliseconds);
}
private static void Test2()
{
var swOuter = Stopwatch.StartNew();
Point a = new Point(1, 1), b = new Point(1, 1);
var sw = Stopwatch.StartNew();
for (int i = 0; i < ITERATIONS; i++)
a = AddByVal(a, b);
sw.Stop();
Console.WriteLine("Test2: x={0} y={1}, Time elapsed: {2} ms",
a.X, a.Y, sw.ElapsedMilliseconds);
swOuter.Stop();
}
}
Keluaran:
Test1: x=1000000001 y=1000000001, Time elapsed: 3242 ms
Test2: x=1000000001 y=1000000001, Time elapsed: 974 ms
Test1: x=1000000001 y=1000000001, Time elapsed: 3251 ms
Test2: x=1000000001 y=1000000001, Time elapsed: 972 ms
Ini pastebin. Anda perlu menjalankannya sebagai rilis 32-bit di .NET 4.x (ada beberapa pemeriksaan dalam kode untuk memastikan ini).
(Perbarui 4)
Mengikuti komentar @ usr tentang jawaban @Hans, saya memeriksa pembongkaran yang dioptimalkan untuk kedua metode, dan mereka agak berbeda:
Hal ini tampaknya menunjukkan bahwa perbedaan mungkin disebabkan oleh penyusun bertingkah lucu dalam kasus pertama, daripada perataan bidang ganda?
Juga, jika saya menambahkan dua variabel (offset total 8 byte), saya masih mendapatkan peningkatan kecepatan yang sama - dan sepertinya tidak lagi terkait dengan penyejajaran bidang yang disebutkan oleh Hans Passant:
// this is still fast?
private static void Test3()
{
var magical_speed_booster_1 = "whatever";
var magical_speed_booster_2 = "whatever";
{
Point a = new Point(1, 1), b = new Point(1, 1);
var sw = Stopwatch.StartNew();
for (int i = 0; i < ITERATIONS; i++)
a = AddByVal(a, b);
sw.Stop();
Console.WriteLine("Test2: x={0} y={1}, Time elapsed: {2} ms",
a.X, a.Y, sw.ElapsedMilliseconds);
}
GC.KeepAlive(magical_speed_booster_1);
GC.KeepAlive(magical_speed_booster_2);
}