เมื่อวานฉันพบบทความของ Christoph Nahr ชื่อ ".NET Struct Performance" ซึ่งเปรียบเทียบหลายภาษา (C ++, C #, Java, JavaScript) สำหรับวิธีการที่เพิ่มโครงสร้างสองจุด ( double
tuples)
ตามที่ปรากฏเวอร์ชัน C ++ ใช้เวลาประมาณ 1,000ms ในการดำเนินการ (การทำซ้ำ 1e9) ในขณะที่ C # ไม่สามารถทำงานได้ต่ำกว่า ~ 3000ms ในเครื่องเดียวกัน (และทำงานได้แย่กว่าใน x64)
เพื่อทดสอบด้วยตัวเองฉันใช้รหัส C # (และทำให้ง่ายขึ้นเล็กน้อยเพื่อเรียกเฉพาะเมธอดที่มีการส่งผ่านพารามิเตอร์ด้วยค่า) และรันบนเครื่อง i7-3610QM (บูสต์ 3.1Ghz สำหรับคอร์เดียว), RAM 8GB, Win8 1 โดยใช้. NET 4.5.2, RELEASE build 32-bit (x86 WoW64 เนื่องจากระบบปฏิบัติการของฉันเป็น 64 บิต) นี่คือเวอร์ชันที่เรียบง่าย:
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);
}
}
โดยPoint
กำหนดเป็นเพียง:
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; } }
}
การรันจะให้ผลลัพธ์ที่คล้ายกับในบทความ:
Result: x=1000000001 y=1000000001, Time elapsed: 3159 ms
ข้อสังเกตแปลก ๆ ครั้งแรก
เนื่องจากวิธีการนี้ควรเป็นแบบอินไลน์ฉันจึงสงสัยว่าโค้ดจะทำงานได้อย่างไรถ้าฉันลบโครงสร้างทั้งหมดออกไปและรวมเข้าด้วยกัน:
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);
}
}
และได้ผลลัพธ์ที่เหมือนกัน (จริงช้ากว่า 1% หลังจากการลองซ้ำหลายครั้ง) ซึ่งหมายความว่า JIT-ter ดูเหมือนจะทำงานได้ดีโดยเพิ่มประสิทธิภาพการเรียกใช้ฟังก์ชันทั้งหมด:
Result: x=1000000001 y=1000000001, Time elapsed: 3200 ms
นอกจากนี้ยังหมายความว่าเกณฑ์มาตรฐานดูเหมือนจะไม่ได้วัดstruct
ประสิทธิภาพใด ๆและดูเหมือนว่าจะวัดdouble
เลขคณิตพื้นฐานเท่านั้น(หลังจากที่ทุกอย่างได้รับการปรับให้เหมาะสมแล้ว)
สิ่งแปลก ๆ
ตอนนี้มาถึงส่วนที่แปลก ถ้าฉันแค่เพิ่มนาฬิกาจับเวลาอีกอันนอกลูป (ใช่ฉัน จำกัด ให้แคบลงเป็นขั้นตอนที่บ้าคลั่งนี้หลังจากลองหลายครั้ง) รหัสจะทำงานเร็วขึ้นสามเท่า :
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
มันไร้สาระ! และมันไม่เหมือนกับการStopwatch
ให้ผลลัพธ์ที่ผิดกับฉันเพราะฉันเห็นได้อย่างชัดเจนว่ามันจบลงหลังจากวินาทีเดียว
ใครช่วยบอกทีว่าอาจเกิดอะไรขึ้นที่นี่
(อัพเดท)
นี่คือสองวิธีในโปรแกรมเดียวกันซึ่งแสดงให้เห็นว่าเหตุผลไม่ได้เป็น 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();
}
}
เอาท์พุต:
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
นี่คือ Pastebinคุณต้องเรียกใช้เป็นรุ่น 32 บิตบน. NET 4.x (มีการตรวจสอบสองสามรหัสเพื่อให้แน่ใจว่าสิ่งนี้)
(อัพเดท 4)
ตามความคิดเห็นของ @ usr เกี่ยวกับคำตอบของ @Hans ฉันตรวจสอบการถอดแยกชิ้นส่วนที่เหมาะสมที่สุดสำหรับทั้งสองวิธีและค่อนข้างแตกต่างกัน:
สิ่งนี้ดูเหมือนจะแสดงให้เห็นว่าความแตกต่างอาจเกิดจากคอมไพเลอร์แสดงตลกในกรณีแรกมากกว่าการจัดแนวฟิลด์สองครั้ง?
นอกจากนี้หากฉันเพิ่มตัวแปรสองตัว (ออฟเซ็ตรวม 8 ไบต์) ฉันยังคงได้รับการเร่งความเร็วเท่าเดิมและดูเหมือนว่ามันจะไม่เกี่ยวข้องกับการกล่าวถึงการจัดตำแหน่งฟิลด์โดย 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);
}