การเปรียบเทียบตัวอย่างโค้ดขนาดเล็กใน C # การปรับใช้นี้สามารถปรับปรุงได้หรือไม่?


104

บ่อยครั้งที่ฉันพบว่าตัวเองกำลังเปรียบเทียบโค้ดส่วนเล็ก ๆ เพื่อดูว่า implemnetation ใดที่เร็วที่สุด

บ่อยครั้งที่ฉันเห็นความคิดเห็นว่ารหัสการเปรียบเทียบไม่ได้คำนึงถึงการกระตุกหรือตัวเก็บขยะ

ฉันมีฟังก์ชันการเปรียบเทียบง่ายๆดังต่อไปนี้ซึ่งฉันได้พัฒนาอย่างช้าๆ:

  static void Profile(string description, int iterations, Action func) {
        // warm up 
        func();
        // clean up
        GC.Collect();

        var watch = new Stopwatch();
        watch.Start();
        for (int i = 0; i < iterations; i++) {
            func();
        }
        watch.Stop();
        Console.Write(description);
        Console.WriteLine(" Time Elapsed {0} ms", watch.ElapsedMilliseconds);
    }

การใช้งาน:

Profile("a descriptions", how_many_iterations_to_run, () =>
{
   // ... code being profiled
});

การใช้งานนี้มีข้อบกพร่องหรือไม่? ดีพอหรือไม่ที่จะแสดงให้เห็นว่าการใช้งาน X เร็วกว่าการใช้งาน Y มากกว่าการวนซ้ำ Z คุณคิดวิธีใด ๆ ที่จะปรับปรุงสิ่งนี้ได้ไหม

แก้ไข ค่อนข้างชัดเจนว่าวิธีการตามเวลา (เมื่อเทียบกับการทำซ้ำ) เป็นที่ต้องการใครมีการใช้งานที่การตรวจสอบเวลาไม่ส่งผลกระทบต่อประสิทธิภาพหรือไม่?


ดูเพิ่มเติมBenchmarkDotNet
Ben Hutchison

คำตอบ:


95

นี่คือฟังก์ชั่นที่แก้ไข: ตามคำแนะนำของชุมชนอย่าลังเลที่จะแก้ไขสิ่งนี้เป็นวิกิชุมชน

static double Profile(string description, int iterations, Action func) {
    //Run at highest priority to minimize fluctuations caused by other processes/threads
    Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.High;
    Thread.CurrentThread.Priority = ThreadPriority.Highest;

    // warm up 
    func();

    var watch = new Stopwatch(); 

    // clean up
    GC.Collect();
    GC.WaitForPendingFinalizers();
    GC.Collect();

    watch.Start();
    for (int i = 0; i < iterations; i++) {
        func();
    }
    watch.Stop();
    Console.Write(description);
    Console.WriteLine(" Time Elapsed {0} ms", watch.Elapsed.TotalMilliseconds);
    return watch.Elapsed.TotalMilliseconds;
}

ให้แน่ใจว่าคุณรวบรวมในรุ่นด้วยการเพิ่มประสิทธิภาพการใช้งานและเรียกใช้การทดสอบนอก Visual Studio ส่วนสุดท้ายนี้มีความสำคัญเนื่องจาก JIT จำกัด การปรับให้เหมาะสมด้วยดีบักเกอร์ที่แนบมาแม้ในโหมดรีลีส


คุณอาจต้องการคลายการวนซ้ำตามจำนวนครั้งเช่น 10 เพื่อลดค่าใช้จ่ายของลูปให้เหลือน้อยที่สุด
Mike Dunlavey

2
ฉันเพิ่งอัปเดตเพื่อใช้นาฬิกาจับเวลา StartNew ไม่ใช่การเปลี่ยนแปลงที่ใช้งานได้ แต่บันทึกโค้ดบรรทัดเดียว
LukeH

1
@ ลุคการเปลี่ยนแปลงที่ยิ่งใหญ่ (ฉันหวังว่าฉันจะ +1 มันได้) @ ไมค์ฉันไม่แน่ใจฉันสงสัยว่าค่าโสหุ้ยการโทรเสมือนจะสูงกว่าการเปรียบเทียบและการมอบหมายงานดังนั้นประสิทธิภาพที่แตกต่างกันจะน้อยมาก
แซมแซฟฟรอน

ฉันขอเสนอให้คุณส่งผ่านการนับซ้ำไปยัง Action และสร้างลูปที่นั่น (อาจจะ - แม้จะไม่มีการควบคุม) ในกรณีที่คุณกำลังวัดการทำงานที่ค่อนข้างสั้นนี่เป็นทางเลือกเดียว และฉันต้องการเห็นเมตริกผกผันเช่นจำนวนรอบ / วินาที
Alex Yakunin

2
คุณคิดอย่างไรเกี่ยวกับการแสดงเวลาโดยเฉลี่ย สิ่งนี้: Console.WriteLine ("เวลาเฉลี่ยที่ผ่านไป {0} ms", watch.ElapsedMilliseconds / การวนซ้ำ);
rudimenter

22

การสรุปผลไม่จำเป็นต้องเสร็จสิ้นก่อนGC.Collectส่งคืน การสรุปเป็นคิวแล้วรันบนเธรดแยกต่างหาก เธรดนี้อาจยังคงใช้งานได้ในระหว่างการทดสอบของคุณซึ่งส่งผลต่อผลลัพธ์

หากคุณต้องการให้แน่ใจว่าการสรุปเสร็จสิ้นก่อนเริ่มการทดสอบของคุณคุณอาจต้องการโทรGC.WaitForPendingFinalizersซึ่งจะบล็อกจนกว่าคิวการสรุปจะถูกล้าง:

GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();

10
ทำไมGC.Collect()อีกครั้ง?
colinfang

7
@colinfang เนื่องจากอ็อบเจ็กต์ที่ถูก "สรุป" ไม่ได้รับการกำหนดโดย GC โดย Finalizer อย่างที่สองCollectคือเพื่อให้แน่ใจว่ามีการรวบรวมวัตถุที่ "สรุปแล้ว" ด้วย
MAV

15

หากคุณต้องการนำการโต้ตอบ GC ออกจากสมการคุณอาจต้องการเรียกใช้การเรียก 'อุ่นเครื่อง' ของคุณหลังจาก GC รวบรวมการโทรไม่ใช่ก่อนหน้านี้ ด้วยวิธีนี้คุณจะรู้ว่า. NET จะมีหน่วยความจำเพียงพอที่จัดสรรจากระบบปฏิบัติการสำหรับชุดการทำงานของฟังก์ชันของคุณอยู่แล้ว

โปรดทราบว่าคุณกำลังเรียกใช้วิธีการแบบไม่อินไลน์สำหรับการวนซ้ำแต่ละครั้งดังนั้นอย่าลืมเปรียบเทียบสิ่งที่คุณกำลังทดสอบกับร่างกายที่ว่างเปล่า คุณจะต้องยอมรับด้วยว่าคุณสามารถกำหนดเวลาที่น่าเชื่อถือได้เฉพาะสิ่งที่นานกว่าการเรียกใช้เมธอดหลายเท่าเท่านั้น

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


1
คุณควรคำนึงถึงการใช้งานตามเวลาหรือไม่
Sam Saffron

6

ฉันจะหลีกเลี่ยงการส่งผู้แทนเลย:

  1. การโทรของตัวแทนคือ ~ การเรียกวิธีเสมือน ไม่ถูก: ประมาณ 25% ของการจัดสรรหน่วยความจำที่เล็กที่สุดใน. NET หากคุณสนใจรายละเอียดโปรดดูเช่นลิงค์นี้
  2. ผู้รับมอบสิทธิ์ที่ไม่ระบุชื่ออาจนำไปสู่การใช้การปิดโดยที่คุณไม่ได้สังเกตเห็นด้วยซ้ำ อีกครั้งการเข้าถึงฟิลด์การปิดนั้นเห็นได้ชัดกว่าเช่นการเข้าถึงตัวแปรบนสแต็ก

โค้ดตัวอย่างที่นำไปสู่การปิดการใช้งาน:

public void Test()
{
  int someNumber = 1;
  Profiler.Profile("Closure access", 1000000, 
    () => someNumber + someNumber);
}

หากคุณไม่ทราบเกี่ยวกับการปิดให้ดูที่วิธีนี้ใน. NET Reflector


ประเด็นที่น่าสนใจ แต่คุณจะสร้างโปรไฟล์ที่ใช้ซ้ำได้อย่างไร () หากคุณไม่ผ่านตัวแทน มีวิธีอื่นในการส่งรหัสไปยังเมธอดโดยพลการหรือไม่?
เถ้า

1
เราใช้ "การใช้ (การวัดใหม่ (... )) {... รหัสที่วัดได้ ... }" ดังนั้นเราจึงได้รับวัตถุการวัดที่ใช้ IDisposable แทนการส่งผ่านตัวแทน ดูcode.google.com/p/dataobjectsdotnet/source/browse/Xtensive.Core/…
Alex Yakunin

สิ่งนี้จะไม่นำไปสู่ปัญหาใด ๆ ในการปิด
Alex Yakunin

3
@AlexYakunin: ลิงค์ของคุณดูเหมือนจะเสีย คุณสามารถใส่รหัสสำหรับคลาสการวัดผลในคำตอบของคุณได้ไหม ฉันสงสัยว่าไม่ว่าคุณจะนำไปใช้อย่างไรคุณจะไม่สามารถเรียกใช้โค้ดเพื่อสร้างโปรไฟล์หลาย ๆ ครั้งด้วยวิธีการที่สามารถระบุได้ อย่างไรก็ตามมันมีประโยชน์มากในสถานการณ์ที่คุณต้องการวัดว่าส่วนต่างๆของแอพพลิเคชั่นที่ซับซ้อน (เกี่ยวพันกัน) ทำงานอย่างไรตราบใดที่คุณจำไว้ว่าการวัดอาจไม่ถูกต้องและไม่สอดคล้องกันเมื่อทำงานในเวลาที่ต่างกัน ฉันใช้แนวทางเดียวกันในโครงการส่วนใหญ่ของฉัน
ShdNx

1
ข้อกำหนดในการเรียกใช้การทดสอบประสิทธิภาพหลาย ๆ ครั้งมีความสำคัญมาก (การอุ่นเครื่อง + การวัดหลายครั้ง) ดังนั้นฉันจึงเปลี่ยนไปใช้วิธีการกับผู้รับมอบสิทธิ์เช่นกัน IDisposableนอกจากนี้หากคุณไม่ได้ใช้การปิดตัวแทนภาวนาเป็นอินเตอร์เฟซได้เร็วขึ้นแล้วเรียกวิธีการในกรณีที่มี
Alex Yakunin

6

ฉันคิดว่าปัญหาที่ยากที่สุดในการเอาชนะด้วยวิธีการเปรียบเทียบเช่นนี้คือการพิจารณากรณีขอบและสิ่งที่ไม่คาดคิด ตัวอย่างเช่น - "ข้อมูลโค้ดทั้งสองทำงานอย่างไรภายใต้ภาระ CPU สูง / การใช้งานเครือข่าย / การทุบดิสก์ / ฯลฯ " เหมาะอย่างยิ่งสำหรับการตรวจสอบตรรกะพื้นฐานเพื่อดูว่าอัลกอริทึมเฉพาะทำงานได้เร็วกว่าที่อื่นอย่างมีนัยสำคัญหรือไม่ แต่ในการทดสอบประสิทธิภาพโค้ดส่วนใหญ่อย่างถูกต้องคุณจะต้องสร้างการทดสอบที่วัดปัญหาคอขวดเฉพาะของรหัสนั้น ๆ

ฉันยังคงบอกว่าการทดสอบโค้ดกลุ่มเล็ก ๆ มักมีผลตอบแทนจากการลงทุนเพียงเล็กน้อยและสามารถส่งเสริมให้ใช้โค้ดที่ซับซ้อนเกินไปแทนที่จะใช้โค้ดที่บำรุงรักษาง่าย การเขียนโค้ดที่ชัดเจนซึ่งนักพัฒนารายอื่นหรือตัวผมเองเมื่อ 6 เดือนที่ผ่านมาสามารถเข้าใจได้อย่างรวดเร็วจะมีประโยชน์ด้านประสิทธิภาพมากกว่าโค้ดที่ปรับให้เหมาะสม


1
สำคัญคือหนึ่งในคำศัพท์ที่โหลดจริงๆ บางครั้งการนำไปใช้งานที่เร็วขึ้น 20% ก็มีความสำคัญบางครั้งต้องเร็วขึ้น 100 เท่าจึงจะมีนัยสำคัญ เห็นด้วยกับคุณในเรื่องความชัดเจนโปรดดู: stackoverflow.com/questions/1018407/…
Sam Saffron

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

5

ฉันจะโทรfunc()หาการอุ่นเครื่องหลายครั้งไม่ใช่แค่ครั้งเดียว


1
ความตั้งใจคือเพื่อให้แน่ใจว่ามีการรวบรวม jit คุณได้ประโยชน์อะไรจากการเรียก func หลาย ๆ ครั้งก่อนการวัดผล?
Sam Saffron

3
เพื่อให้ JIT มีโอกาสปรับปรุงผลลัพธ์แรก
Alexey Romanov

1
.NET JIT ไม่ได้ปรับปรุงผลลัพธ์เมื่อเวลาผ่านไป (เช่นเดียวกับ Java) จะแปลงวิธีการจาก IL เป็น Assembly เพียงครั้งเดียวในการโทรครั้งแรก
Matt Warren

4

ข้อเสนอแนะในการปรับปรุง

  1. การตรวจจับว่าสภาพแวดล้อมการดำเนินการนั้นดีสำหรับการเปรียบเทียบ (เช่นการตรวจจับว่ามีการต่อดีบักเกอร์หรือถ้าปิดใช้งานการเพิ่มประสิทธิภาพ jit ซึ่งจะส่งผลให้การวัดไม่ถูกต้อง)

  2. การวัดส่วนต่างๆของโค้ดโดยอิสระ (เพื่อดูว่าคอขวดอยู่ตรงไหน)

  3. การเปรียบเทียบเวอร์ชัน / ส่วนประกอบ / ส่วนต่างๆของโค้ด (ในประโยคแรกของคุณคุณพูดว่า '... การเปรียบเทียบโค้ดส่วนเล็ก ๆ เพื่อดูว่าการนำไปใช้งานใดเร็วที่สุด')

เกี่ยวกับ # 1:

  • หากต้องการตรวจสอบว่ามีการแนบดีบักเกอร์หรือไม่ให้อ่านคุณสมบัติSystem.Diagnostics.Debugger.IsAttached(อย่าลืมจัดการกรณีที่ไม่ได้ต่อดีบักเกอร์ในตอนแรก แต่จะแนบหลังจากเวลาผ่านไปสักครู่)

  • หากต้องการตรวจสอบว่าการปรับให้เหมาะสม jit ถูกปิดใช้งานหรือไม่ให้อ่านคุณสมบัติDebuggableAttribute.IsJITOptimizerDisabledของชุดประกอบที่เกี่ยวข้อง:

    private bool IsJitOptimizerDisabled(Assembly assembly)
    {
        return assembly.GetCustomAttributes(typeof (DebuggableAttribute), false)
            .Select(customAttribute => (DebuggableAttribute) customAttribute)
            .Any(attribute => attribute.IsJITOptimizerDisabled);
    }

เกี่ยวกับ # 2:

นี้สามารถทำได้หลายวิธี วิธีหนึ่งคืออนุญาตให้มีการจัดหาผู้ร่วมประชุมหลายคนจากนั้นวัดผลผู้รับมอบสิทธิ์เหล่านั้นทีละคน

เกี่ยวกับ # 3:

นอกจากนี้ยังสามารถทำได้หลายวิธีและกรณีการใช้งานที่แตกต่างกันจะต้องการโซลูชันที่แตกต่างกันมาก หากมีการเรียกเกณฑ์มาตรฐานด้วยตนเองการเขียนลงในคอนโซลอาจไม่เป็นไร อย่างไรก็ตามหากระบบการสร้างดำเนินการโดยอัตโนมัติการเขียนลงคอนโซลอาจไม่ดีนัก

วิธีหนึ่งในการทำเช่นนี้คือการส่งคืนผลการเปรียบเทียบเป็นวัตถุที่พิมพ์อย่างรุนแรงซึ่งสามารถใช้งานได้ง่ายในบริบทต่างๆ


Etimo เกณฑ์มาตรฐาน

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

นี่คือเอาต์พุตตัวอย่างที่เปรียบเทียบสององค์ประกอบและผลลัพธ์จะถูกเขียนไปยังคอนโซล ในกรณีนี้ส่วนประกอบทั้งสองที่เปรียบเทียบกันเรียกว่า 'KeyedCollection' และ 'MultiplyIndexedKeyedCollection':

Etimo.Benchmarks - เอาต์พุตคอนโซลตัวอย่าง

มีเป็นแพคเกจ NuGetเป็นตัวอย่างแพคเกจ NuGetและรหัสที่มาสามารถใช้ได้ที่GitHub นอกจากนี้ยังมีการโพสต์บล็อก

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



1

ขึ้นอยู่กับรหัสที่คุณกำลังเปรียบเทียบและแพลตฟอร์มที่ทำงานอยู่คุณอาจต้องพิจารณาว่าการจัดตำแหน่งโค้ดมีผลต่อประสิทธิภาพอย่างไร ในการทำเช่นนั้นอาจต้องใช้กระดาษห่อหุ้มภายนอกที่รันการทดสอบหลายครั้ง (ในโดเมนของแอปหรือกระบวนการที่แยกจากกัน) บางครั้งเรียก "รหัสช่องว่างภายใน" ครั้งแรกเพื่อบังคับให้คอมไพล์ JIT เพื่อให้โค้ดถูก เทียบเคียงเพื่อจัดตำแหน่งให้แตกต่างกัน ผลการทดสอบที่สมบูรณ์จะให้การกำหนดเวลากรณีที่ดีที่สุดและกรณีที่เลวร้ายที่สุดสำหรับการจัดตำแหน่งโค้ดต่างๆ


1

หากคุณกำลังพยายามกำจัดผลกระทบจากการเก็บรวบรวมขยะจากเกณฑ์มาตรฐานให้เสร็จสมบูรณ์คุณควรตั้งค่าGCSettings.LatencyModeหรือไม่?

ถ้าไม่และคุณต้องการให้ผลกระทบของขยะที่สร้างขึ้นfuncเป็นส่วนหนึ่งของเกณฑ์มาตรฐานคุณไม่ควรบังคับให้รวบรวมเมื่อสิ้นสุดการทดสอบ (ภายในตัวจับเวลา) หรือไม่?


0

ปัญหาพื้นฐานสำหรับคำถามของคุณคือสมมติฐานที่ว่าการวัดเพียงครั้งเดียวสามารถตอบคำถามของคุณได้ทั้งหมด คุณต้องวัดหลาย ๆ ครั้งเพื่อให้ได้ภาพสถานการณ์ที่มีประสิทธิภาพและโดยเฉพาะอย่างยิ่งในภาษาที่รวบรวมขยะเช่น C #

อีกคำตอบหนึ่งให้วิธีการวัดประสิทธิภาพพื้นฐานที่ใช้ได้

static void Profile(string description, int iterations, Action func) {
    // warm up 
    func();

    var watch = new Stopwatch(); 

    // clean up
    GC.Collect();
    GC.WaitForPendingFinalizers();
    GC.Collect();

    watch.Start();
    for (int i = 0; i < iterations; i++) {
        func();
    }
    watch.Stop();
    Console.Write(description);
    Console.WriteLine(" Time Elapsed {0} ms", watch.Elapsed.TotalMilliseconds);
}

อย่างไรก็ตามการวัดครั้งเดียวนี้ไม่ได้คำนึงถึงการเก็บขยะ โปรไฟล์ที่เหมาะสมยังอธิบายถึงประสิทธิภาพในกรณีที่เลวร้ายที่สุดของการรวบรวมขยะที่กระจายออกไปในหลาย ๆ การโทร (หมายเลขนี้ไม่มีประโยชน์เนื่องจาก VM สามารถยุติได้โดยไม่ต้องเก็บขยะที่เหลือทิ้ง แต่ยังมีประโยชน์ในการเปรียบเทียบการใช้งานสองแบบที่แตกต่างกันfunc)

static void ProfileGarbageMany(string description, int iterations, Action func) {
    // warm up 
    func();

    var watch = new Stopwatch(); 

    // clean up
    GC.Collect();
    GC.WaitForPendingFinalizers();
    GC.Collect();

    watch.Start();
    for (int i = 0; i < iterations; i++) {
        func();
    }
    GC.Collect();
    GC.WaitForPendingFinalizers();
    GC.Collect();

    watch.Stop();
    Console.Write(description);
    Console.WriteLine(" Time Elapsed {0} ms", watch.Elapsed.TotalMilliseconds);
}

และอาจต้องการวัดประสิทธิภาพกรณีที่เลวร้ายที่สุดของการรวบรวมขยะสำหรับวิธีการที่เรียกเพียงครั้งเดียว

static void ProfileGarbage(string description, int iterations, Action func) {
    // warm up 
    func();

    var watch = new Stopwatch(); 

    // clean up
    GC.Collect();
    GC.WaitForPendingFinalizers();
    GC.Collect();

    watch.Start();
    for (int i = 0; i < iterations; i++) {
        func();

        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();
    }
    watch.Stop();
    Console.Write(description);
    Console.WriteLine(" Time Elapsed {0} ms", watch.Elapsed.TotalMilliseconds);
}

แต่สิ่งที่สำคัญกว่าการแนะนำการวัดเพิ่มเติมที่เป็นไปได้ใด ๆ ให้กับโปรไฟล์คือแนวคิดที่ว่าเราควรวัดสถิติที่แตกต่างกันหลาย ๆ ตัวไม่ใช่แค่สถิติประเภทเดียว

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.