ประสิทธิภาพที่แปลกประหลาดเพิ่มขึ้นในเกณฑ์มาตรฐานอย่างง่าย


97

เมื่อวานฉันพบบทความของ Christoph Nahr ชื่อ ".NET Struct Performance" ซึ่งเปรียบเทียบหลายภาษา (C ++, C #, Java, JavaScript) สำหรับวิธีการที่เพิ่มโครงสร้างสองจุด ( doubletuples)

ตามที่ปรากฏเวอร์ชัน 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 ฉันตรวจสอบการถอดแยกชิ้นส่วนที่เหมาะสมที่สุดสำหรับทั้งสองวิธีและค่อนข้างแตกต่างกัน:

Test1 ทางซ้าย Test2 ทางขวา

สิ่งนี้ดูเหมือนจะแสดงให้เห็นว่าความแตกต่างอาจเกิดจากคอมไพเลอร์แสดงตลกในกรณีแรกมากกว่าการจัดแนวฟิลด์สองครั้ง?

นอกจากนี้หากฉันเพิ่มตัวแปรสองตัว (ออฟเซ็ตรวม 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);
}

1
นอกเหนือจากสิ่งที่ JIT ยังขึ้นอยู่กับการเพิ่มประสิทธิภาพของคอมไพเลอร์ Ryujit ใหม่ล่าสุดยังทำการปรับแต่งเพิ่มเติมและยังแนะนำการสนับสนุนคำแนะนำ SIMD แบบ จำกัด
Felix K.

3
จอน Skeet พบปัญหาประสิทธิภาพการทำงานที่มีเขตข้อมูลอ่านได้อย่างเดียวใน structs: Micro-Optimisation: การขาดประสิทธิภาพที่น่าแปลกใจของเขตอ่านได้อย่างเดียว ลองทำให้ฟิลด์ส่วนตัวไม่ใช่แบบอ่านอย่างเดียว
dbc

2
@dbc: ฉันทำการทดสอบกับdoubleตัวแปรเฉพาะที่เท่านั้นไม่มีstructs ดังนั้นฉันจึงตัดความไร้ประสิทธิภาพของการเรียกเค้าโครงโครงสร้าง / เมธอดออกไป
Groo

3
ดูเหมือนว่าจะเกิดขึ้นเฉพาะใน 32 บิตกับ RyuJIT ฉันได้รับ 1600ms ทั้งสองครั้ง
leppie

2
ฉันได้ดูการถอดชิ้นส่วนของทั้งสองวิธีแล้ว ไม่มีอะไรน่าสนใจให้ดู Test1 สร้างรหัสที่ไม่มีประสิทธิภาพโดยไม่มีเหตุผลชัดเจน JIT bug หรือตามการออกแบบ ใน Test1 JIT จะโหลดและเก็บค่าสองเท่าสำหรับการวนซ้ำแต่ละครั้งไปยังสแต็ก นี่อาจจะเพื่อให้แน่ใจว่ามีความแม่นยำแน่นอนเนื่องจากหน่วยลอย x86 ใช้ความแม่นยำภายใน 80 บิต ฉันพบว่าการเรียกใช้ฟังก์ชันที่ไม่อยู่ในบรรทัดใด ๆ ที่ด้านบนของฟังก์ชันทำให้การเรียกใช้งานเร็วขึ้นอีกครั้ง
usr

คำตอบ:


10

อัพเดต 4อธิบายปัญหา: ในกรณีแรก JIT จะเก็บค่าที่คำนวณได้ ( a, b) บนสแต็ก ในกรณีที่สอง JIT เก็บไว้ในรีจิสเตอร์

ในความเป็นจริง, Test1ทำงานได้ช้าเนื่องจากไฟล์Stopwatch. ฉันเขียนเกณฑ์มาตรฐานขั้นต่ำต่อไปนี้ตามBenchmarkDotNet :

[BenchmarkTask(platform: BenchmarkPlatform.X86)]
public class Jit_RegistersVsStack
{
    private const int IterationCount = 100001;

    [Benchmark]
    [OperationsPerInvoke(IterationCount)]
    public string WithoutStopwatch()
    {
        double a = 1, b = 1;
        for (int i = 0; i < IterationCount; i++)
        {
            // fld1  
            // faddp       st(1),st
            a = a + b;
        }
        return string.Format("{0}", a);
    }

    [Benchmark]
    [OperationsPerInvoke(IterationCount)]
    public string WithStopwatch()
    {
        double a = 1, b = 1;
        var sw = new Stopwatch();
        for (int i = 0; i < IterationCount; i++)
        {
            // fld1  
            // fadd        qword ptr [ebp-14h]
            // fstp        qword ptr [ebp-14h]
            a = a + b;
        }
        return string.Format("{0}{1}", a, sw.ElapsedMilliseconds);
    }

    [Benchmark]
    [OperationsPerInvoke(IterationCount)]
    public string WithTwoStopwatches()
    {
        var outerSw = new Stopwatch();
        double a = 1, b = 1;
        var sw = new Stopwatch();
        for (int i = 0; i < IterationCount; i++)
        {
            // fld1  
            // faddp       st(1),st
            a = a + b;
        }
        return string.Format("{0}{1}", a, sw.ElapsedMilliseconds);
    }
}

ผลลัพธ์บนคอมพิวเตอร์ของฉัน:

BenchmarkDotNet=v0.7.7.0
OS=Microsoft Windows NT 6.2.9200.0
Processor=Intel(R) Core(TM) i7-4702MQ CPU @ 2.20GHz, ProcessorCount=8
HostCLR=MS.NET 4.0.30319.42000, Arch=64-bit  [RyuJIT]
Type=Jit_RegistersVsStack  Mode=Throughput  Platform=X86  Jit=HostJit  .NET=HostFramework

             Method |   AvrTime |    StdDev |       op/s |
------------------- |---------- |---------- |----------- |
   WithoutStopwatch | 1.0333 ns | 0.0028 ns | 967,773.78 |
      WithStopwatch | 3.4453 ns | 0.0492 ns | 290,247.33 |
 WithTwoStopwatches | 1.0435 ns | 0.0341 ns | 958,302.81 |

อย่างที่เราเห็น:

  • WithoutStopwatchทำงานได้อย่างรวดเร็ว (เพราะa = a + bใช้รีจิสเตอร์)
  • WithStopwatchทำงานช้า (เพราะa = a + bใช้สแต็ก)
  • WithTwoStopwatches ทำงานได้อย่างรวดเร็วอีกครั้ง (เพราะ a = a + bใช้รีจิสเตอร์)

พฤติกรรมของ JIT-x86 ขึ้นอยู่กับเงื่อนไขจำนวนมากที่แตกต่างกัน ด้วยเหตุผลบางประการนาฬิกาจับเวลาเรือนแรกบังคับให้ JIT-x86 ใช้สแต็กและนาฬิกาจับเวลาตัวที่สองอนุญาตให้ใช้รีจิสเตอร์อีกครั้ง


นี่อธิบายสาเหตุไม่ได้จริงๆ หากคุณตรวจสอบการทดสอบของฉันดูเหมือนว่าการทดสอบที่มีเพิ่มเติมStopwatchจะทำงานได้เร็วขึ้นจริงๆ แต่ถ้าคุณสลับลำดับที่เรียกใช้ในMainเมธอดวิธีอื่นจะได้รับการปรับให้เหมาะสม
Groo

75

มีวิธีที่ง่ายมากในการรับโปรแกรมเวอร์ชัน "เร็ว" เสมอ โครงการ> คุณสมบัติ> แท็บสร้างยกเลิกการเลือกตัวเลือก "ต้องการ 32 บิต" ตรวจสอบให้แน่ใจว่าการเลือกเป้าหมายแพลตฟอร์มคือ AnyCPU

คุณไม่ชอบ 32 บิต แต่น่าเสียดายที่จะเปิดใช้งานโดยค่าเริ่มต้นสำหรับโครงการ C # เสมอ ในอดีตชุดเครื่องมือ Visual Studio ทำงานได้ดีขึ้นมากกับกระบวนการ 32 บิตซึ่งเป็นปัญหาเก่าที่ Microsoft ได้รับการบิ่นไป ถึงเวลาที่จะลบตัวเลือกนั้นออกไปแล้ว VS2015 ได้กล่าวถึงการบล็อกถนนจริงสองสามรายการสุดท้ายเป็นรหัส 64 บิตด้วยการกระวนกระวายใจ x64 ใหม่เอี่ยมและการสนับสนุนสากลสำหรับ Edit + Continue

พูดพล่อยพอสิ่งที่คุณค้นพบคือความสำคัญของการจัดตำแหน่งตัวแปร โปรเซสเซอร์ให้ความสำคัญกับมันเป็นอย่างมาก หากตัวแปรอยู่ในตำแหน่งที่ไม่ถูกต้องในหน่วยความจำโปรเซสเซอร์จะต้องทำงานพิเศษเพื่อสับไบต์เพื่อให้ได้มาในลำดับที่ถูกต้อง มีปัญหาการจัดแนวที่ไม่ตรงกันสองประการปัญหาหนึ่งคือการที่ไบต์ยังคงอยู่ในบรรทัดแคช L1 เดียวซึ่งจะต้องเสียค่าใช้จ่ายเพิ่มเติมเพื่อเปลี่ยนให้อยู่ในตำแหน่งที่ถูกต้อง และสิ่งที่ไม่ดีเป็นพิเศษคือสิ่งที่คุณพบโดยที่ส่วนหนึ่งของไบต์อยู่ในบรรทัดแคชหนึ่งและอีกส่วนหนึ่ง ซึ่งต้องใช้การเข้าถึงหน่วยความจำสองรายการแยกกันและติดกาวเข้าด้วยกัน ช้าเป็นสามเท่า

doubleและlongประเภทที่มีปัญหาผู้มีอำนาจในกระบวนการแบบ 32 บิต มีขนาด 64 บิต และสามารถทำให้ไม่ตรงแนวด้วย 4 ได้ CLR สามารถรับประกันการจัดตำแหน่ง 32 บิตเท่านั้น ไม่ได้เป็นปัญหาในกระบวนการแบบ 64 บิตตัวแปรทั้งหมดมีการรับประกันที่จะสอดคล้องกับ 8. นอกจากนี้ยังมีเหตุผลพื้นฐานทำไมภาษา C # ไม่สามารถสัญญาว่าพวกเขาจะเป็นอะตอม และเหตุใดอาร์เรย์ของคู่จึงถูกจัดสรรใน Large Object Heap เมื่อมีองค์ประกอบมากกว่า 1,000 รายการ LOH ให้การรับประกันการจัดตำแหน่งเป็น 8 และอธิบายว่าเหตุใดการเพิ่มตัวแปรโลคัลจึงแก้ปัญหาได้การอ้างอิงอ็อบเจ็กต์คือ 4 ไบต์ดังนั้นจึงย้ายตัวแปรคู่ไปด้วย 4 ตอนนี้ทำให้มันถูกจัดตำแหน่ง โดยบังเอิญ.

คอมไพเลอร์ C หรือ C ++ แบบ 32 บิตทำงานพิเศษเพื่อให้แน่ใจว่าคู่ไม่สามารถจัดแนวไม่ตรง ไม่ใช่ปัญหาง่ายๆในการแก้ปัญหาสแต็กสามารถจัดวางไม่ตรงแนวเมื่อป้อนฟังก์ชันได้เนื่องจากการรับประกันเพียงอย่างเดียวคือการจัดแนวเป็น 4 อารัมภบทของฟังก์ชันดังกล่าวจำเป็นต้องทำงานพิเศษเพื่อให้สอดคล้องกับ 8 เคล็ดลับเดียวกันนี้ใช้ไม่ได้ในโปรแกรมที่มีการจัดการตัวรวบรวมขยะสนใจเป็นอย่างมากว่าตัวแปรโลคัลอยู่ที่ใดในหน่วยความจำ จำเป็นเพื่อให้สามารถค้นพบว่าวัตถุในฮีป GC ยังคงอ้างอิงอยู่ ไม่สามารถจัดการได้อย่างเหมาะสมกับตัวแปรดังกล่าวที่ถูกย้ายด้วย 4 เนื่องจากสแต็กไม่ตรงแนวเมื่อป้อนเมธอด

นี่เป็นปัญหาพื้นฐานของการกระตุก. NET ที่ไม่รองรับคำแนะนำ SIMD อย่างง่ายดาย พวกเขามีข้อกำหนดในการจัดตำแหน่งที่แข็งแกร่งกว่ามากชนิดที่โปรเซสเซอร์ไม่สามารถแก้ไขได้ด้วยตัวเอง SSE2 ต้องการการจัดตำแหน่ง 16 AVX ต้องการการจัดตำแหน่ง 32 ไม่สามารถรับสิ่งนั้นในโค้ดที่มีการจัดการ

สุดท้าย แต่ไม่ท้ายสุดโปรดทราบว่าสิ่งนี้ทำให้ความสมบูรณ์แบบของโปรแกรม C # ที่ทำงานในโหมด 32 บิตไม่สามารถคาดเดาได้ เมื่อคุณเข้าถึงสองครั้งหรือยาวที่เก็บไว้เป็นฟิลด์ในออบเจ็กต์ดังนั้น perf สามารถเปลี่ยนแปลงได้อย่างมากเมื่อตัวรวบรวมขยะกระชับฮีป ซึ่งเคลื่อนย้ายวัตถุในหน่วยความจำช่องดังกล่าวสามารถผิด / จัดแนวได้ทันที แน่นอนสุ่มมากอาจเป็นรอยหัวได้ :)

ไม่มีการแก้ไขง่ายๆ แต่อย่างใดอย่างหนึ่งรหัส 64 บิตคืออนาคต ลบความกระวนกระวายใจที่บังคับตราบใดที่ Microsoft ไม่เปลี่ยนเทมเพลตโครงการ อาจจะเป็นเวอร์ชั่นต่อไปเมื่อพวกเขารู้สึกมั่นใจมากขึ้นเกี่ยวกับริวจิต


1
ไม่แน่ใจว่าการจัดตำแหน่งมีผลอย่างไรเมื่อตัวแปรคู่สามารถลงทะเบียน (และอยู่ใน Test2) ได้ Test1 ใช้สแต็ก Test2 ไม่
usr

2
คำถามนี้เปลี่ยนแปลงเร็วเกินไปสำหรับฉันที่จะติดตาม คุณต้องระวังการทดสอบที่ส่งผลต่อผลลัพธ์ของการทดสอบ คุณต้องใส่ [MethodImpl (MethodImplOptions.NoInlining)] ในวิธีการทดสอบเพื่อเปรียบเทียบแอปเปิ้ลกับส้ม ตอนนี้คุณจะเห็นว่าเครื่องมือเพิ่มประสิทธิภาพสามารถเก็บตัวแปรไว้ในสแต็ก FPU ได้ทั้งสองกรณี
Hans Passant

4
โอ้มันเป็นเรื่องจริง เหตุใดการจัดตำแหน่งวิธีจึงมีผลกระทบต่อคำแนะนำที่สร้างขึ้น! ไม่ควรมีความแตกต่างใด ๆ สำหรับตัวห่วง ทั้งหมดควรอยู่ในการลงทะเบียน อารัมภบทการจัดตำแหน่งควรไม่เกี่ยวข้อง ดูเหมือนว่าจะเป็นจุดบกพร่องของ JIT
usr

3
ฉันต้องแก้ไขคำตอบอย่างมีนัยสำคัญคนเกียจคร้าน ฉันจะไปให้ถึงในวันพรุ่งนี้
Hans Passant

2
@HansPassant คุณกำลังจะขุดค้นแหล่ง JIT หรือไม่? คงจะสนุกดี ณ จุดนี้สิ่งที่ฉันรู้คือมันเป็นบั๊ก JIT แบบสุ่ม
usr

5

จำกัด สิ่งที่แคบลง (ดูเหมือนว่าจะมีผลกับรันไทม์ 32 บิต CLR 4.0 เท่านั้น)

สังเกตตำแหน่งของตำแหน่งvar f = Stopwatch.Frequency;ทำให้เกิดความแตกต่าง

ช้า (2700ms):

static void Test1()
{
  Point a = new Point(1, 1), b = new Point(1, 1);
  var f = Stopwatch.Frequency;

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

เร็ว (800ms):

static void Test1()
{
  var f = Stopwatch.Frequency;
  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);
}

การแก้ไขโค้ดโดยไม่ต้องสัมผัสStopwatchยังทำให้ความเร็วเปลี่ยนแปลงไปอย่างมาก การเปลี่ยนลายเซ็นของวิธีการTest1(bool warmup)และการเพิ่มเงื่อนไขในConsoleผลลัพธ์: if (!warmup) { Console.WriteLine(...); }ยังมีผลเช่นเดียวกัน (สะดุดกับสิ่งนี้ในขณะที่สร้างการทดสอบของฉันเพื่อแก้ไขปัญหา)
ระหว่าง

@InBetween: ฉันเห็นมีบางอย่างที่คาว ยังเกิดขึ้นเฉพาะในโครงสร้าง
leppie

4

ดูเหมือนว่าจะมีข้อผิดพลาดบางอย่างใน Jitter เนื่องจากพฤติกรรมนั้นแย่ลง พิจารณารหัสต่อไปนี้:

public static void Main()
{
    Test1(true);
    Test1(false);
    Console.ReadLine();
}

public static void Test1(bool warmup)
{
    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();

    if (!warmup)
    {
        Console.WriteLine("Result: x={0} y={1}, Time elapsed: {2} ms",
            a.X, a.Y, sw.ElapsedMilliseconds);
    }
}

ซึ่งจะทำงานเป็น900มิลลิวินาทีเช่นเดียวกับตัวเรือนนาฬิกาจับเวลาด้านนอก อย่างไรก็ตามหากเราลบif (!warmup)เงื่อนไขเงื่อนไขนั้นจะทำงานเป็น3000มิลลิวินาที สิ่งที่แม้แต่คนแปลกหน้าก็คือรหัสต่อไปนี้จะทำงานเป็น900มิลลิวินาที:

public static void Test1()
{
    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",
        0, 0, sw.ElapsedMilliseconds);
}

หมายเหตุฉันได้ลบa.Xและa.Yอ้างอิงจากConsoleผลลัพธ์แล้ว

ฉันไม่รู้ว่าเกิดอะไรขึ้น แต่สิ่งนี้มีกลิ่นเหม็นสำหรับฉันและมันไม่เกี่ยวข้องกับการมีภายนอกStopwatchหรือไม่ปัญหาดูเหมือนจะเป็นเรื่องทั่วไปมากขึ้น


เมื่อคุณลบการโทรไปยังa.Xและa.Yคอมไพเลอร์อาจมีอิสระที่จะเพิ่มประสิทธิภาพทุกอย่างในลูปเนื่องจากไม่ได้ใช้ผลลัพธ์ของการดำเนินการ
กรู

@Groo: ใช่มันดูสมเหตุสมผล แต่ไม่ใช่เมื่อคุณคำนึงถึงพฤติกรรมแปลก ๆ อื่น ๆ ที่เราเห็น การลบa.Xและa.Yไม่ทำให้มันเร็วไปกว่าเมื่อคุณรวมif (!warmup)เงื่อนไขหรือ OP outerSwซึ่งหมายความว่ามันไม่ได้เพิ่มประสิทธิภาพอะไรเลยมันเป็นเพียงการกำจัดข้อผิดพลาดใด ๆ ที่ทำให้โค้ดทำงานด้วยความเร็วต่ำกว่าปกติ ( 3000ms แทน900ms)
ระหว่าง

2
โอ้โอเคผมคิดว่าการปรับปรุงความเร็วที่เกิดขึ้นเมื่อwarmupเป็นความจริง แต่ในกรณีที่สายไม่ถูกพิมพ์แม้ดังนั้นกรณีที่ไม่aได้รับการพิมพ์จริงอ้างอิง อย่างไรก็ตามฉันต้องการให้แน่ใจว่าฉันอ้างอิงผลการคำนวณอยู่ใกล้จุดสิ้นสุดของวิธีการเสมอเมื่อใดก็ตามที่ฉันกำลังเปรียบเทียบสิ่งต่างๆ
Groo
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.