Hôm qua, tôi đã tìm thấy một bài báo của Christoph Nahr có tiêu đề "Hiệu suất cấu trúc .NET" đã đánh giá một số ngôn ngữ (C ++, C #, Java, JavaScript) cho một phương pháp thêm hai cấu trúc điểm ( double
bộ giá trị).
Hóa ra, phiên bản C ++ mất khoảng 1000ms để thực thi (1e9 lần lặp), trong khi C # không thể dưới ~ 3000ms trên cùng một máy (và hoạt động thậm chí còn tệ hơn trong x64).
Để tự kiểm tra, tôi đã lấy mã C # (và đơn giản hóa một chút để chỉ gọi phương thức trong đó các tham số được truyền theo giá trị) và chạy nó trên máy i7-3610QM (tăng 3,1Ghz cho lõi đơn), RAM 8GB, Win8. 1, sử dụng .NET 4.5.2, bản dựng RELEASE 32-bit (x86 WoW64 vì hệ điều hành của tôi là 64-bit). Đây là phiên bản đơn giản hóa:
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);
}
}
Với Point
định nghĩa đơn giản là:
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; } }
}
Chạy nó cho kết quả tương tự như trong bài viết:
Result: x=1000000001 y=1000000001, Time elapsed: 3159 ms
Quan sát kỳ lạ đầu tiên
Vì phương thức phải được nội tuyến, tôi tự hỏi mã sẽ hoạt động như thế nào nếu tôi xóa hoàn toàn các cấu trúc và chỉ đơn giản là liên kết toàn bộ mọi thứ lại với nhau:
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);
}
}
Và thực tế nhận được kết quả tương tự (thực sự chậm hơn 1% sau vài lần thử lại), có nghĩa là JIT-ter dường như đang làm tốt công việc tối ưu hóa tất cả các lệnh gọi hàm:
Result: x=1000000001 y=1000000001, Time elapsed: 3200 ms
Điều đó cũng có nghĩa là điểm chuẩn dường như không đo lường bất kỳ struct
hiệu suất nào và thực sự dường như chỉ đo lường double
số học cơ bản (sau khi mọi thứ khác được tối ưu hóa).
Những thứ kỳ lạ
Bây giờ đến phần kỳ lạ. Nếu tôi chỉ thêm một đồng hồ bấm giờ khác bên ngoài vòng lặp (vâng, tôi đã thu hẹp nó xuống bước điên rồ này sau nhiều lần thử lại), mã chạy nhanh hơn ba lần :
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
Thật là nực cười! Và nó không giống như Stopwatch
việc đưa ra kết quả sai cho tôi bởi vì tôi có thể thấy rõ rằng nó kết thúc sau một giây.
Bất cứ ai có thể cho tôi biết những gì có thể xảy ra ở đây?
(Cập nhật)
Đây là hai phương pháp trong cùng một chương trình, điều này cho thấy lý do không phải là JITting:
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();
}
}
Đầu ra:
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
Đây là một pastebin. Bạn cần chạy nó dưới dạng bản phát hành 32 bit trên .NET 4.x (có một số kiểm tra trong mã để đảm bảo điều này).
(Cập nhật 4)
Sau nhận xét của @ usr về câu trả lời của @Hans, tôi đã kiểm tra cách tháo gỡ được tối ưu hóa cho cả hai phương pháp và chúng khá khác nhau:
Điều này dường như cho thấy sự khác biệt có thể là do trình biên dịch hoạt động hài hước trong trường hợp đầu tiên, chứ không phải là căn chỉnh trường kép?
Ngoài ra, nếu tôi thêm hai biến (tổng độ lệch 8 byte), tôi vẫn nhận được tốc độ tăng tương tự - và có vẻ như nó không còn liên quan đến việc căn chỉnh trường được Hans Passant đề cập:
// 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);
}
double
biến cục bộ, không có struct
s, vì vậy tôi đã loại trừ sự thiếu hiệu quả của bố cục / phương thức cấu trúc.