อันตรายเมื่อสร้างเธรดที่มีขนาดสแต็ก 50x เป็นค่าเริ่มต้นคืออะไร


228

ขณะนี้ฉันกำลังทำงานกับโปรแกรมที่มีประสิทธิภาพสูงและหนึ่งเส้นทางที่ฉันตัดสินใจสำรวจซึ่งอาจช่วยลดการใช้ทรัพยากรคือการเพิ่มขนาดสแต็กของเธรดของผู้ปฏิบัติงานเพื่อให้ฉันสามารถย้ายข้อมูลส่วนใหญ่float[]ที่ฉันจะเข้าสู่ สแต็ก (ใช้stackalloc)

ฉันได้อ่านแล้วว่าขนาดสแต็กเริ่มต้นสำหรับเธรดคือ 1 MB ดังนั้นเพื่อย้ายทั้งหมดของfloat[]ฉันฉันจะต้องขยายสแต็กประมาณ 50 ครั้ง (ถึง 50 MB ~)

ฉันเข้าใจว่าโดยทั่วไปถือว่าเป็น "ไม่ปลอดภัย" และไม่แนะนำ แต่หลังจากทำการเปรียบเทียบโค้ดปัจจุบันของฉันกับวิธีนี้ฉันพบว่าความเร็วในการประมวลผลเพิ่มขึ้น530% ! ดังนั้นฉันจึงไม่สามารถผ่านตัวเลือกนี้ได้โดยไม่ต้องตรวจสอบเพิ่มเติมซึ่งทำให้ฉันมีคำถาม สิ่งที่เป็นอันตรายที่เกี่ยวข้องกับการเพิ่มสแต็คให้มีขนาดใหญ่ (สิ่งที่อาจผิดไป) และฉันควรระมัดระวังอะไรบ้างเพื่อลดอันตรายดังกล่าว

รหัสทดสอบของฉัน

public static unsafe void TestMethod1()
{
    float* samples = stackalloc float[12500000];

    for (var ii = 0; ii < 12500000; ii++)
    {
        samples[ii] = 32768;
    }
}

public static void TestMethod2()
{
    var samples = new float[12500000];

    for (var i = 0; i < 12500000; i++)
    {
        samples[i] = 32768;
    }
}

98
+1 อย่างจริงจัง. คุณถามสิ่งที่ดูคล้ายกับคำถามงี่เง่าจากบรรทัดฐานและจากนั้นคุณก็เป็นกรณีที่ดีมากที่ในสถานการณ์เฉพาะของคุณมันเป็นเรื่องที่ควรพิจารณาเพราะคุณทำการบ้านและวัดผล นี่เป็นสิ่งที่ดีมาก - ฉันคิดถึงมันด้วยคำถามมากมาย ดีมาก - ดีคุณพิจารณาบางสิ่งเช่นนี้น่าเสียดายที่โปรแกรมเมอร์ C # หลายคนไม่ทราบถึงโอกาสในการเพิ่มประสิทธิภาพเหล่านั้น ใช่ไม่จำเป็นบ่อยครั้ง - แต่บางครั้งมันมีความสำคัญและสร้างความแตกต่างอย่างมาก
TomTom

5
ฉันสนใจที่จะดูสองรหัสที่มีความแตกต่างในการประมวลผลความเร็ว 530% เพียงเพราะการย้ายอาร์เรย์ไปยังสแต็ค แค่รู้สึกไม่ถูก
Dialecticus

13
ก่อนที่คุณจะกระโดดลงบนถนนนั่น: คุณเคยลองใช้Marshal.AllocHGlobal(อย่าลืมFreeHGlobalเช่นกัน) เพื่อจัดสรรข้อมูลนอกหน่วยความจำที่มีการจัดการหรือไม่? จากนั้นชี้ไปที่ตัวชี้float*และคุณควรจะเรียง
Marc Gravell

2
รู้สึกไม่ถูกต้องถ้าคุณจัดสรรจำนวนมาก Stackalloc ข้ามปัญหา GC ทั้งหมดซึ่งสามารถสร้าง / สร้างตำแหน่งที่แข็งแกร่งในระดับโปรเซสเซอร์ นี่คือหนึ่งในสิ่งที่หมวกมีลักษณะเหมือนการเพิ่มประสิทธิภาพขนาดเล็ก - เว้นแต่คุณจะเขียนโปรแกรมคณิตศาสตร์ที่มีประสิทธิภาพสูงและมีพฤติกรรมตรงนี้และทำให้เกิดความแตกต่าง;)
TomTom

6
ความสงสัยของฉัน: หนึ่งในวิธีการเหล่านี้ทำให้เกิดการตรวจสอบขอบเขตในการวนซ้ำทุกครั้งในขณะที่อีกวิธีหนึ่งไม่ได้ทำหรือปรับให้เหมาะสม
pjc50

คำตอบ:


45

เมื่อเปรียบเทียบโค้ดทดสอบกับ Sam ฉันคิดว่าเราทั้งคู่ถูกต้อง!
อย่างไรก็ตามเกี่ยวกับสิ่งต่าง ๆ :

  • การเข้าถึงหน่วยความจำ (การอ่านและการเขียน) เป็นไปอย่างรวดเร็วไม่ว่าจะอยู่ที่ใด - สแต็ค, โกลบอลหรือฮีป
  • อย่างไรก็ตามการจัดสรรจะเร็วที่สุดในสแต็กและช้าที่สุดในฮีป

stackมันจะไปเช่นนี้: global< heap< (เวลาการจัดสรร) ใน
ทางเทคนิคแล้วการจัดสรรสแต็คไม่ใช่การจัดสรรจริง ๆ รันไทม์เพียงทำให้แน่ใจว่าส่วนหนึ่งของสแต็ก (เฟรม?) ถูกสงวนไว้สำหรับอาร์เรย์

ฉันแนะนำอย่างยิ่งให้ระวังอย่างนี้แม้ว่า
ฉันขอแนะนำดังต่อไปนี้:

  1. เมื่อคุณต้องการสร้างอาร์เรย์บ่อยครั้งซึ่งไม่เคยออกจากฟังก์ชั่น (เช่นผ่านการอ้างอิง) การใช้สแต็กจะเป็นการปรับปรุงที่ยิ่งใหญ่
  2. หากคุณสามารถรีไซเคิลอาร์เรย์ได้ให้ทำทุกครั้งที่ทำได้! ฮีปเป็นสถานที่ที่ดีที่สุดสำหรับการจัดเก็บวัตถุระยะยาว (การสร้างมลภาวะหน่วยความจำระดับโลกไม่ดี; กรอบสแต็กจะหายไป)

( หมายเหตุ : 1. ใช้กับประเภทค่าเท่านั้นประเภทการอ้างอิงจะถูกจัดสรรในฮีปและผลประโยชน์จะลดลงเป็น 0)

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

ส่วนด้านล่างคือคำตอบเริ่มต้นของฉัน มันผิด - ish และการทดสอบไม่ถูกต้อง มันถูกเก็บไว้สำหรับการอ้างอิงเท่านั้น


การทดสอบของฉันระบุว่าหน่วยความจำที่จัดสรรแบบกองซ้อนและหน่วยความจำส่วนกลางช้ากว่าอย่างน้อย 15% (ใช้เวลา 120% ของเวลา) หน่วยความจำที่จัดสรรฮีปสำหรับการใช้งานในอาร์เรย์!

นี่คือรหัสทดสอบของฉันและนี่คือผลลัพธ์ตัวอย่าง:

Stack-allocated array time: 00:00:00.2224429
Globally-allocated array time: 00:00:00.2206767
Heap-allocated array time: 00:00:00.1842670
------------------------------------------
Fastest: Heap.

  |    S    |    G    |    H    |
--+---------+---------+---------+
S |    -    | 100.80 %| 120.72 %|
--+---------+---------+---------+
G |  99.21 %|    -    | 119.76 %|
--+---------+---------+---------+
H |  82.84 %|  83.50 %|    -    |
--+---------+---------+---------+
Rates are calculated by dividing the row's value to the column's.

ฉันทดสอบบน Windows 8.1 Pro (พร้อมอัปเดต 1) โดยใช้ i7 4700 MQ ภายใต้. NET 4.5.1
ฉันทดสอบทั้งด้วย x86 และ x64 และผลลัพธ์นั้นเหมือนกัน

แก้ไข : ฉันเพิ่มขนาดสแต็กของเธรดทั้งหมด 201 MB ขนาดตัวอย่างเป็น 50 ล้านและลดการวนซ้ำเป็น 5
ผลลัพธ์จะเหมือนกับด้านบน :

Stack-allocated array time: 00:00:00.4504903
Globally-allocated array time: 00:00:00.4020328
Heap-allocated array time: 00:00:00.3439016
------------------------------------------
Fastest: Heap.

  |    S    |    G    |    H    |
--+---------+---------+---------+
S |    -    | 112.05 %| 130.99 %|
--+---------+---------+---------+
G |  89.24 %|    -    | 116.90 %|
--+---------+---------+---------+
H |  76.34 %|  85.54 %|    -    |
--+---------+---------+---------+
Rates are calculated by dividing the row's value to the column's.

แต่ก็ดูเหมือนว่าสแต็คที่เป็นจริงได้รับช้า


ฉันต้องไม่เห็นด้วยตามผลลัพธ์ของมาตรฐาน (ดูความคิดเห็นที่ด้านล่างของหน้าสำหรับผลลัพธ์) แสดงว่าสแต็กนั้นเร็วกว่าทั่วโลกเล็กน้อยและเร็วกว่ากองมาก และเพื่อให้แน่ใจว่าผลลัพธ์ของฉันถูกต้องแน่นอนการทดสอบ 20 ครั้งและแต่ละวิธีเรียกว่า 100 ครั้งต่อการทดสอบซ้ำ คุณใช้เกณฑ์มาตรฐานอย่างถูกต้องหรือไม่?
Sam

ฉันได้รับผลลัพธ์ที่ไม่สอดคล้องกันมาก ด้วยความไว้วางใจอย่างเต็มรูปแบบ x64 ปล่อยการตั้งค่าไม่มีการดีบั๊กพวกเขาทั้งหมดนั้นเท่าเทียมกันอย่างรวดเร็ว (ความแตกต่างน้อยกว่า 1% และมีความผันผวน) ในขณะที่ของคุณจะเร็วกว่ามากด้วยสแต็ก ฉันต้องทดสอบเพิ่มเติม! แก้ไข : คุณควรโยนข้อยกเว้นสแต็กล้น คุณจัดสรรให้เพียงพอ O_o
Vercas

ใช่ฉันรู้ว่ามันใกล้ คุณต้องทำซ้ำสองสามครั้งเหมือนอย่างที่เคยทำบางทีลองเฉลี่ย 5 หรือมากกว่านั้น
Sam

1
@ Voo การแข่งครั้งที่ 1 นั้นใช้เวลามากพอ ๆ กับการทดสอบครั้งที่ 100 สำหรับฉัน จากประสบการณ์ของฉันสิ่ง Java JIT นี้ไม่ได้ใช้กับ. NET เลย "อุ่นเครื่อง" เท่านั้นที่. NET ทำการโหลดคลาสและแอสเซมบลีเมื่อใช้เป็นครั้งแรก
Vercas

2
@Voo ทดสอบเกณฑ์มาตรฐานของฉันและอีกส่วนจากส่วนสำคัญที่เขาเพิ่มในความคิดเห็นในคำตอบนี้ รวบรวมรหัสเข้าด้วยกันและทำการทดสอบสองสามร้อยครั้ง จากนั้นกลับมารายงานข้อสรุปของคุณ ฉันได้ทำการทดสอบอย่างละเอียดถี่ถ้วนและฉันรู้ดีว่าฉันกำลังพูดถึงเรื่องอะไรเมื่อพูดว่า. NET ไม่ได้แปลความหมายของ bytecode อย่างที่ Java ทำเลย JITs มันทันที
Vercas

28

ฉันค้นพบความเร็วในการประมวลผลเพิ่มขึ้น 530%!

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

เป็นการยากมากที่จะใช้พื้นที่สแต็กจำนวนมากในโปรแกรม. NET นอกเหนือจากการเรียกซ้ำที่มากเกินไป ขนาดของเฟรมสแต็คของวิธีการที่จัดการถูกตั้งค่าเป็นศิลา เพียงผลรวมของข้อโต้แย้งของวิธีการและตัวแปรท้องถิ่นในวิธีการ ลบด้วยสิ่งที่สามารถเก็บไว้ใน CPU register คุณสามารถเพิกเฉยได้เนื่องจากมีจำนวนน้อย

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

ซึ่งแตกต่างจากโปรแกรมเนทีฟโดยเฉพาะที่เขียนใน C มันสามารถสำรองพื้นที่สำหรับอาร์เรย์ในสแต็กเฟรม มัลแวร์โจมตีเวกเตอร์พื้นฐานหลังสแต็คบัฟเฟอร์ล้น เป็นไปได้ใน C # เช่นกันคุณต้องใช้stackallocคำหลัก หากคุณกำลังทำสิ่งนั้นอันตรายที่ชัดเจนคือต้องเขียนโค้ดที่ไม่ปลอดภัยซึ่งอาจมีการโจมตีดังกล่าวรวมถึงความเสียหายของเฟรมสแต็กแบบสุ่ม ยากมากที่จะวินิจฉัยข้อบกพร่อง มีมาตรการต่อต้านต่อนี้ในกระวนกระวายใจในภายหลังฉันคิดว่าเริ่มต้นที่. NET 4.0 ที่กระวนกระวายใจสร้างรหัสเพื่อวาง "คุกกี้" ในกรอบสแต็คและตรวจสอบว่ามันยังคงเหมือนเดิมเมื่อวิธีการส่งกลับ ความผิดพลาดทันทีไปยังเดสก์ท็อปโดยไม่มีวิธีการสกัดกั้นหรือรายงานอุบัติเหตุหากเกิดขึ้น นั่นเป็น ... อันตรายต่อสภาพจิตใจของผู้ใช้

เธรดหลักของโปรแกรมของคุณซึ่งเริ่มโดยระบบปฏิบัติการจะมีสแต็ก 1 MB โดยค่าเริ่มต้น 4 MB เมื่อคุณคอมไพล์โปรแกรมของคุณกำหนดเป้าหมาย x64 การเพิ่มที่ต้องใช้ Editbin.exe ด้วยตัวเลือก / STACK ในเหตุการณ์การสร้างโพสต์ โดยทั่วไปคุณสามารถขอได้มากถึง 500 MB ก่อนที่โปรแกรมของคุณจะมีปัญหาในการเริ่มต้นเมื่อทำงานในโหมด 32 บิต แน่นอนว่าเธรดสามารถทำได้ง่ายกว่ามากโดยทั่วไปโซนอันตรายจะวนเวียนอยู่รอบ ๆ 90 MB สำหรับโปรแกรม 32 บิต ทริกเกอร์เมื่อโปรแกรมของคุณทำงานมาเป็นเวลานานและพื้นที่ที่อยู่ได้แยกส่วนจากการจัดสรรครั้งก่อน การใช้พื้นที่ที่อยู่ทั้งหมดต้องสูงเกินกว่ากิ๊กเพื่อรับโหมดความล้มเหลวนี้

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


21
speedup 5x รายงานมาจากการย้ายจากไปfloat[] float*กองขนาดใหญ่เป็นเพียงวิธีการที่ประสบความสำเร็จ การเร่งความเร็ว x5 ในบางสถานการณ์นั้นสมเหตุสมผลสำหรับการเปลี่ยนแปลงนั้น
Marc Gravell

3
ตกลงฉันยังไม่มีข้อมูลโค้ดเลยเมื่อฉันเริ่มตอบคำถาม ยังคงอยู่ใกล้พอ
Hans Passant

22

ฉันจะจองไว้ที่นั่นฉันไม่รู้ว่าจะคาดเดาได้อย่างไร - สิทธิ์ GC (ซึ่งต้องสแกนสแต็ค) และอื่น ๆ - ทั้งหมดอาจได้รับผลกระทบ ฉันจะถูกล่อลวงให้ใช้หน่วยความจำที่ไม่มีการจัดการแทน:

var ptr = Marshal.AllocHGlobal(sizeBytes);
try
{
    float* x = (float*)ptr;
    DoWork(x);
}
finally
{
    Marshal.FreeHGlobal(ptr);
}

1
คำถามด้านข้าง: เหตุใด GC ต้องการสแกนสแต็ก หน่วยความจำที่จัดสรรโดยstackallocไม่อยู่ภายใต้การรวบรวมขยะ
dcastro

6
@dcastro จะต้องสแกนสแต็กเพื่อตรวจสอบการอ้างอิงที่มีอยู่ในสแต็กเท่านั้น ฉันไม่รู้ว่ามันจะต้องทำอะไรเมื่อมันถึงขนาดใหญ่stackalloc- มันต้องกระโดดมันและคุณหวังว่ามันจะทำได้อย่างง่ายดาย - แต่ประเด็นที่ฉันพยายามทำก็คือมันแนะนำภาวะแทรกซ้อน / ความกังวลที่ไม่จำเป็น IMO stackallocนั้นยอดเยี่ยมในฐานะ scratch-buffer แต่สำหรับพื้นที่ทำงานโดยเฉพาะคาดว่าจะจัดสรรหน่วยความจำอันใดอันหนึ่งมากกว่าการใช้ / สร้างความสับสนให้กับกองซ้อน
Marc Gravell

8

สิ่งหนึ่งที่ผิดพลาดคือคุณอาจไม่ได้รับอนุญาตให้ทำเช่นนั้น หากไม่ได้ทำงานในโหมดเต็มความน่าเชื่อถือ Framework จะเพิกเฉยต่อคำร้องขอขนาดสแต็กที่ใหญ่ขึ้น (ดู MSDN บนThread Constructor (ParameterizedThreadStart, Int32))

แทนที่จะเพิ่มขนาดสแต็คระบบเป็นจำนวนมากเช่นนี้ฉันขอแนะนำให้เขียนรหัสของคุณใหม่เพื่อให้ใช้ Iteration และการใช้สแต็กด้วยตนเองบนฮีป


1
ความคิดที่ดีฉันจะทำซ้ำผ่านแทน นอกจากนั้นรหัสของฉันกำลังทำงานในโหมดเต็มความน่าเชื่อถือดังนั้นมีสิ่งอื่นใดอีกที่ฉันควรระวัง
Sam

6

อาร์เรย์ที่มีประสิทธิภาพสูงอาจเข้าถึงได้ในลักษณะเดียวกับ C # ปกติ แต่นั่นอาจเป็นจุดเริ่มต้นของปัญหา: พิจารณารหัสต่อไปนี้:

float[] someArray = new float[100]
someArray[200] = 10.0;

คุณคาดหวังว่ามีข้อยกเว้นที่ถูกผูกไว้และสิ่งนี้สมเหตุสมผลเนื่องจากคุณกำลังพยายามเข้าถึงองค์ประกอบ 200 แต่ค่าสูงสุดที่อนุญาตคือ 99 ถ้าคุณไปที่เส้นทาง stackalloc แล้วจะไม่มีวัตถุห่อรอบอาร์เรย์ของคุณเพื่อตรวจสอบที่ถูกผูกไว้และ การติดตามจะไม่แสดงข้อยกเว้นใด ๆ :

Float* pFloat =  stackalloc float[100];
fFloat[200]= 10.0;

ด้านบนคุณจะจัดสรรหน่วยความจำให้เพียงพอที่จะถือ 100 ลอยและคุณกำลังตั้งค่าขนาดหน่วยความจำ (ลอย) ซึ่งเริ่มต้นที่ตำแหน่งเริ่มต้นของหน่วยความจำนี้ + 200 * ขนาดของ (ลอย) สำหรับการถือค่าลอยของคุณ 10 แปลกใจว่าหน่วยความจำนี้ หน่วยความจำที่จัดสรรสำหรับโฟลตและไม่มีใครรู้ว่าจะเก็บอะไรไว้ในที่อยู่นั้น หากคุณโชคดีคุณอาจใช้หน่วยความจำที่ยังไม่ได้ใช้ในขณะนี้ แต่ในเวลาเดียวกันคุณอาจเขียนทับตำแหน่งที่ใช้สำหรับเก็บตัวแปรอื่น ๆ ในการสรุป: ลักษณะการทำงานของรันไทม์ที่คาดเดาไม่ได้


ผิดจริง การทดสอบรันไทม์และคอมไพเลอร์ยังคงอยู่ที่นั่น
TomTom

9
@TomTom erm ไม่ คำตอบมีข้อดี คำถามพูดถึงstackallocในกรณีที่เรากำลังพูดถึงfloat*ฯลฯ - ซึ่งไม่ได้มีการตรวจสอบเดียวกัน มันถูกเรียกunsafeด้วยเหตุผลที่ดีมาก โดยส่วนตัวฉันมีความสุขอย่างสมบูรณ์แบบที่จะใช้unsafeเมื่อมีเหตุผลที่ดี แต่โสกราตีสทำคะแนนที่สมเหตุสมผล
Marc Gravell

@Marc สำหรับโค้ดที่แสดง (หลังจากรัน JIT) จะไม่มีการตรวจสอบขอบเขตอีกต่อไปเพราะมันเป็นเรื่องเล็กน้อยสำหรับคอมไพเลอร์เพื่อให้เหตุผลว่าการเข้าถึงทั้งหมดเป็นแบบไม่ จำกัด โดยทั่วไปแล้วสิ่งนี้สามารถสร้างความแตกต่างได้อย่างแน่นอน
Voo

6

Microbenchmarking language กับ JIT และ GC เช่น Java หรือ C # อาจซับซ้อนเล็กน้อยดังนั้นโดยทั่วไปควรใช้เฟรมเวิร์กที่มีอยู่ - Java เสนอ mhf หรือ Caliper ที่ยอดเยี่ยมน่าเศร้าที่สุดเท่าที่ฉันจะรู้ C # อะไรก็ตามที่เข้าใกล้เหล่านั้น Jon Skeet เขียนสิ่งนี้ที่นี่ซึ่งฉันคิดว่าจะดูแลสิ่งที่สำคัญที่สุด (จอนรู้ว่าเขากำลังทำอะไรอยู่ในพื้นที่นั้น; ฉันปรับแต่งเวลาเล็กน้อยเพราะ 30 วินาทีต่อการทดสอบหลังจากวอร์มอัพมากเกินไปสำหรับความอดทนของฉัน (5 วินาทีควรทำ)

ดังนั้นก่อนอื่นผลลัพธ์. NET 4.5.1 ภายใต้ Windows 7 x64 ตัวเลขแสดงถึงการวนซ้ำที่มันสามารถทำงานได้ใน 5 วินาทีดังนั้นสูงกว่าจะดีกว่า

x64 JIT:

Standard       10,589.00  (1.00)
UnsafeStandard 10,612.00  (1.00)
Stackalloc     12,088.00  (1.14)
FixedStandard  10,715.00  (1.01)
GlobalAlloc    12,547.00  (1.18)

x86 JIT (ใช่แล้วยังเศร้าอยู่):

Standard       14,787.00   (1.02)
UnsafeStandard 14,549.00   (1.00)
Stackalloc     15,830.00   (1.09)
FixedStandard  14,824.00   (1.02)
GlobalAlloc    18,744.00   (1.29)

สิ่งนี้ให้การเร่งความเร็วที่สมเหตุสมผลมากที่สุดที่มากที่สุด 14% (และค่าใช้จ่ายส่วนใหญ่เกิดจาก GC ต้องทำงานให้พิจารณาว่าเป็นสถานการณ์กรณีที่เลวร้ายที่สุดตามความเป็นจริง) ผลลัพธ์ x86 นั้นน่าสนใจ - ไม่ชัดเจนเลยว่าเกิดอะไรขึ้น

และนี่คือรหัส:

public static float Standard(int size) {
    float[] samples = new float[size];
    for (var ii = 0; ii < size; ii++) {
        samples[ii] = 32768 + (ii != 0 ? samples[ii - 1] : 0);
    }
    return samples[size - 1];
}

public static unsafe float UnsafeStandard(int size) {
    float[] samples = new float[size];
    for (var ii = 0; ii < size; ii++) {
        samples[ii] = 32768 + (ii != 0 ? samples[ii - 1] : 0);
    }
    return samples[size - 1];
}

public static unsafe float Stackalloc(int size) {
    float* samples = stackalloc float[size];
    for (var ii = 0; ii < size; ii++) {
        samples[ii] = 32768 + (ii != 0 ? samples[ii - 1] : 0);
    }
    return samples[size - 1];
}

public static unsafe float FixedStandard(int size) {
    float[] prev = new float[size];
    fixed (float* samples = &prev[0]) {
        for (var ii = 0; ii < size; ii++) {
            samples[ii] = 32768 + (ii != 0 ? samples[ii - 1] : 0);
        }
        return samples[size - 1];
    }
}

public static unsafe float GlobalAlloc(int size) {
    var ptr = Marshal.AllocHGlobal(size * sizeof(float));
    try {
        float* samples = (float*)ptr;
        for (var ii = 0; ii < size; ii++) {
            samples[ii] = 32768 + (ii != 0 ? samples[ii - 1] : 0);
        }
        return samples[size - 1];
    } finally {
        Marshal.FreeHGlobal(ptr);
    }
}

static void Main(string[] args) {
    int inputSize = 100000;
    var results = TestSuite.Create("Tests", inputSize, Standard(inputSize)).
        Add(Standard).
        Add(UnsafeStandard).
        Add(Stackalloc).
        Add(FixedStandard).
        Add(GlobalAlloc).
        RunTests();
    results.Display(ResultColumns.NameAndIterations);
}

การสังเกตที่น่าสนใจฉันจะต้องตรวจสอบมาตรฐานของฉันอีกครั้ง แม้ว่าสิ่งนี้จะยังไม่ตอบคำถามของฉัน " ... อะไรคืออันตรายที่เกี่ยวข้องกับการเพิ่มสแต็คให้มีขนาดใหญ่เช่นนี้ ... " แม้ว่าผลลัพธ์ของฉันจะไม่ถูกต้องคำถามก็ยังใช้ได้อยู่ ฉันขอขอบคุณความพยายาม
Sam

1
@ Sam เมื่อใช้12500000เป็นขนาดฉันได้รับจริง ๆ ยกเว้น stackoverflow แต่ส่วนใหญ่เรื่องนี้เกี่ยวกับการปฏิเสธหลักฐานพื้นฐานที่ใช้รหัสการจัดสรรสแต็คเป็นคำสั่งหลายขนาดของเร็วกว่า เรากำลังทำผลงานน้อยที่สุดเท่าที่จะเป็นไปได้ที่นี่เป็นอย่างอื่นและความแตกต่างอยู่ที่ประมาณ 10-15% เท่านั้นในทางปฏิบัติมันจะยิ่งต่ำลง
Voo

5

เนื่องจากความแตกต่างด้านประสิทธิภาพมีขนาดใหญ่เกินไปปัญหาจึงแทบไม่เกี่ยวข้องกับการจัดสรร อาจเกิดจากการเข้าถึงอาร์เรย์

ฉันถอดชิ้นส่วนลูปของฟังก์ชั่น:

TestMethod1:

IL_0011:  ldloc.0 
IL_0012:  ldloc.1 
IL_0013:  ldc.i4.4 
IL_0014:  mul 
IL_0015:  add 
IL_0016:  ldc.r4 32768.
IL_001b:  stind.r4 // <----------- This one
IL_001c:  ldloc.1 
IL_001d:  ldc.i4.1 
IL_001e:  add 
IL_001f:  stloc.1 
IL_0020:  ldloc.1 
IL_0021:  ldc.i4 12500000
IL_0026:  blt IL_0011

TestMethod2:

IL_0012:  ldloc.0 
IL_0013:  ldloc.1 
IL_0014:  ldc.r4 32768.
IL_0019:  stelem.r4 // <----------- This one
IL_001a:  ldloc.1 
IL_001b:  ldc.i4.1 
IL_001c:  add 
IL_001d:  stloc.1 
IL_001e:  ldloc.1 
IL_001f:  ldc.i4 12500000
IL_0024:  blt IL_0012

เราสามารถตรวจสอบการใช้งานของคำสั่งและที่สำคัญกว่านั้นยกเว้นในข้อกำหนด ECMA :

stind.r4: Store value of type float32 into memory at address

ข้อยกเว้นมันจะพ่น:

System.NullReferenceException

และ

stelem.r4: Replace array element at index with the float32 value on the stack.

ข้อยกเว้นมันจะพ่น:

System.NullReferenceException
System.IndexOutOfRangeException
System.ArrayTypeMismatchException

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

และสิ่งนี้ยังตอบคำถามของคุณ: อันตรายคือไม่มีการตรวจสอบช่วงของอาร์เรย์และประเภท สิ่งนี้ไม่ปลอดภัย (ดังกล่าวในการประกาศฟังก์ชั่น; D)


4

แก้ไข: (การเปลี่ยนแปลงเล็กน้อยในรหัสและการวัดทำให้เกิดการเปลี่ยนแปลงครั้งใหญ่ในผลลัพธ์)

ก่อนอื่นฉันเรียกใช้โค้ดที่ได้รับการปรับให้เหมาะสมที่สุดในดีบักเกอร์ (F5) แต่นั่นผิด ควรรันโดยไม่มีการดีบักเกอร์ (Ctrl + F5) ประการที่สองรหัสอาจปรับให้เหมาะสมอย่างทั่วถึงดังนั้นเราต้องทำให้ซับซ้อนเพื่อให้เครื่องมือเพิ่มประสิทธิภาพไม่ยุ่งกับการวัดของเรา ฉันทำให้วิธีการทั้งหมดคืนรายการสุดท้ายในอาร์เรย์และอาร์เรย์มีประชากรแตกต่างกัน นอกจากนี้ยังมีศูนย์พิเศษใน OP TestMethod2ที่ทำให้มันช้าลงสิบเท่า

ฉันลองวิธีอื่นนอกเหนือจากวิธีที่คุณระบุไว้สองวิธี วิธีที่ 3 มีรหัสเดียวกับวิธีการของคุณ 2 unsafeแต่ฟังก์ชั่นที่มีการประกาศ วิธีที่ 4 ใช้การเข้าถึงตัวชี้ไปยังอาร์เรย์ที่สร้างขึ้นเป็นประจำ วิธีที่ 5 ใช้การเข้าถึงตัวชี้ไปยังหน่วยความจำที่ไม่มีการจัดการดังที่ Marc Gravell อธิบายไว้ ทั้งห้าวิธีการทำงานในเวลาที่คล้ายกันมาก M5 เป็นวิธีที่เร็วที่สุด (และ M1 ใกล้เคียงกับวินาที) ความแตกต่างระหว่างที่เร็วที่สุดและช้าที่สุดคือประมาณ 5% ซึ่งไม่ใช่สิ่งที่ฉันสนใจ

    public static unsafe float TestMethod3()
    {
        float[] samples = new float[5000000];

        for (var ii = 0; ii < 5000000; ii++)
        {
            samples[ii] = 32768 + (ii != 0 ? samples[ii - 1] : 0);
        }

        return samples[5000000 - 1];
    }

    public static unsafe float TestMethod4()
    {
        float[] prev = new float[5000000];
        fixed (float* samples = &prev[0])
        {
            for (var ii = 0; ii < 5000000; ii++)
            {
                samples[ii] = 32768 + (ii != 0 ? samples[ii - 1] : 0);
            }

            return samples[5000000 - 1];
        }
    }

    public static unsafe float TestMethod5()
    {
        var ptr = Marshal.AllocHGlobal(5000000 * sizeof(float));
        try
        {
            float* samples = (float*)ptr;

            for (var ii = 0; ii < 5000000; ii++)
            {
                samples[ii] = 32768 + (ii != 0 ? samples[ii - 1] : 0);
            }

            return samples[5000000 - 1];
        }
        finally
        {
            Marshal.FreeHGlobal(ptr);
        }
    }

ดังนั้น M3 จึงเหมือนกับ M2 ที่มีเครื่องหมายว่า "ไม่ปลอดภัย" เท่านั้น ค่อนข้างน่าสงสัยว่ามันจะเร็วกว่านี้ ... คุณแน่ใจหรือ
Roman Starkov

@romkyns ฉันเพิ่งวิ่งเบนช์มาร์กมาตรฐาน (M2 กับ M3) และ M3 น่าประหลาดใจจริงๆเร็วกว่า M2 2.14%
Sam

" ข้อสรุปคือการใช้สแต็กนั้นไม่จำเป็น " เมื่อทำการจัดสรรบล็อคขนาดใหญ่เช่นที่ฉันให้ไว้ในโพสต์ของฉันฉันเห็นด้วย แต่หลังจากทำเกณฑ์มาตรฐานM1 vs M2เสร็จสมบูรณ์แล้ว(โดยใช้ความคิดของ PFMสำหรับทั้งสองวิธี) ต้องไม่เห็นด้วยเนื่องจาก M1 ตอนนี้เร็วกว่า M2 ถึง 135%
Sam

1
@ แซม แต่คุณยังเปรียบเทียบการเข้าถึงตัวชี้กับการเข้าถึงอาร์เรย์! นั่นเป็นสิ่งแรกที่ทำให้เร็วขึ้น TestMethod4VS คือการเปรียบเทียบที่ดีมากสำหรับTestMethod1 stackalloc
Roman Starkov

@romkyns อาใช่จุดดีฉันลืมเรื่องนั้น ฉันได้ทำการวัดประสิทธิภาพมาแล้วตอนนี้มีความแตกต่างเพียง 8% (M1 นั้นเร็วกว่าของสองคนนี้)
Sam
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.