การอ่านเบื้องหลังที่สำคัญ: microarch pdf ของ Agner Fogและอาจเป็น Ulrich Drepper ด้วยอะไรทุกโปรแกรมเมอร์ควรรู้เกี่ยวกับหน่วยความจำ ดูลิงก์อื่น ๆ ในx86วิกิพีเดียแท็กโดยเฉพาะอย่างยิ่งคู่มือการเพิ่มประสิทธิภาพของ Intel และเดวิด Kanter ของการวิเคราะห์ของสถาปัตยกรรม Haswell กับแผนภาพ
งานที่เด็ดมาก ๆ ดีกว่าที่ฉันเคยเห็นเมื่อนักเรียนถูกขอให้ปรับโค้ดให้เหมาะสมgcc -O0
เรียนรู้เทคนิคมากมายที่ไม่สำคัญในรหัสจริง ในกรณีนี้คุณจะถูกขอให้เรียนรู้เกี่ยวกับขั้นตอนการทำงานของ CPU และใช้สิ่งนั้นเพื่อเป็นแนวทางในการลดความเหมาะสมไม่ใช่เพียงแค่การคาดเดาแบบไม่รู้ตัว ส่วนที่สนุกที่สุดของอันนี้คือการแสดงให้เห็นถึงการมองโลกในแง่ร้ายด้วย "ความไร้ความสามารถอย่างโหดร้าย" ไม่ใช่ความมุ่งร้าย
ปัญหาเกี่ยวกับการใช้ถ้อยคำและรหัส :
ตัวเลือกเฉพาะ uarch สำหรับรหัสนี้มี จำกัด ไม่ใช้อาร์เรย์ใด ๆ และค่าใช้จ่ายส่วนใหญ่เรียกไปยังexp
/ log
ฟังก์ชั่นห้องสมุด ไม่มีวิธีที่ชัดเจนที่จะมีความเท่าเทียมกันในระดับคำสั่งมากหรือน้อยและห่วงโซ่การพึ่งพาแบบวนซ้ำที่ดำเนินการนั้นสั้นมาก
ฉันชอบที่จะเห็นคำตอบที่พยายามทำให้การชะลอตัวจากการจัดเรียงนิพจน์ใหม่เพื่อเปลี่ยนการอ้างอิงเพื่อลดILPจากการพึ่งพา (อันตราย) ฉันไม่ได้ลองเลย
ซีพียูอินเทลแซนดีบริดจ์ครอบครัวที่มีการออกแบบที่ออกจากคำสั่งซื้อที่ก้าวร้าวใช้จ่ายจำนวนมากของทรานซิสเตอร์และอำนาจในการค้นหาความเท่าเทียมและหลีกเลี่ยงอันตราย (อ้างอิง) ที่จะมีปัญหาRISC ท่อในการสั่งซื้อคลาสสิก โดยทั่วไปความอันตรายดั้งเดิมเท่านั้นที่ทำให้ช้าลงคือการอ้างอิง RAW "จริง" ที่ทำให้ปริมาณงานถูก จำกัด โดยความหน่วง
สงครามและอุแว๊อันตรายสำหรับการลงทะเบียนจะสวยมากไม่เป็นปัญหาขอบคุณลงทะเบียนเปลี่ยนชื่อ (ยกเว้นสำหรับpopcnt
/lzcnt
/tzcnt
ซึ่งมีการอ้างอิงที่ผิดพลาดในปลายทางของพวกเขาบน Intel CPUsแม้ว่าจะเป็นแบบเขียนอย่างเดียวเช่น WAW ที่ได้รับการจัดการเป็นอันตรายจาก RAW + การเขียน) สำหรับการสั่งซื้อหน่วยความจำซีพียูที่ทันสมัยใช้คิวร้านค้าในการกระทำความล่าช้าในแคชจนเกษียณยังหลีกเลี่ยงสงครามและอุแว๊อันตราย
ทำไม Mulge ใช้เวลาเพียง 3 รอบใน Haswell แตกต่างจากตารางคำแนะนำของ Agner มีข้อมูลเพิ่มเติมเกี่ยวกับการเปลี่ยนชื่อรีจิสเตอร์และการซ่อนเวลาแฝงของ FMA ในลูปผลิตภัณฑ์ดอท FP
ชื่อแบรนด์ "i7" ได้รับการแนะนำให้รู้จักกับ Nehalem (สืบต่อจาก Core2)และคู่มือ Intel บางคนยังพูดว่า "Core i7" เมื่อพวกเขาดูเหมือนจะหมายถึง Nehalem แต่พวกเขายังคงรักษาแบรนด์ "i7" ไว้สำหรับ Sandybridgeและ microarchitectures ภายหลัง SNB คือเมื่อ P6 ครอบครัวกลายเป็นสายพันธุ์ใหม่ที่ SNB-ครอบครัว ในหลาย ๆ ทาง Nehalem มีส่วนร่วมกับ Pentium III มากกว่ากับ Sandybridge (เช่น register read stalls และ ROB-read ไม่ได้เกิดขึ้นกับ SnB เพราะมันเปลี่ยนเป็นการใช้ไฟล์ลงทะเบียนทางกายภาพนอกจากนี้ uop cache และ Internal รูปแบบ uop) คำว่า "สถาปัตยกรรม i7" ไม่มีประโยชน์เพราะมันสมเหตุสมผลเล็กน้อยที่จะจัดกลุ่ม SnB-family กับ Nehalem แต่ไม่ใช่ Core2 (Nehalem ได้แนะนำสถาปัตยกรรมแคช L3 แบบรวมที่ใช้ร่วมกันสำหรับการเชื่อมต่อหลายคอร์ด้วยกันและรวมถึง GPU ด้วยดังนั้นระดับชิปจึงทำให้การตั้งชื่อเหมาะสมยิ่งขึ้น)
บทสรุปของความคิดที่ดีที่การไร้ความสามารถที่โหดร้ายสามารถพิสูจน์ได้
แม้แต่คนที่ไร้ความสามารถก็ไม่สามารถที่จะเพิ่มงานที่ไร้ประโยชน์อย่างเห็นได้ชัดหรือการวนซ้ำไม่สิ้นสุดและการยุ่งกับคลาส C ++ / Boost นั้นอยู่นอกเหนือขอบเขตของการมอบหมาย
- มัลติเธรดพร้อมตัวนับลูปที่แชร์ เดียว
std::atomic<uint64_t>
ดังนั้นจำนวนการวนซ้ำที่ถูกต้องจึงเกิดขึ้น uint64_t -m32 -march=i586
อะตอมเป็นที่ไม่ดีโดยเฉพาะอย่างยิ่งกับ สำหรับคะแนนโบนัสจัดเรียงให้ตรงแนวและข้ามขอบเขตของหน้าด้วยการแบ่งที่ไม่สม่ำเสมอ (ไม่ใช่ 4: 4)
- การแชร์เท็จสำหรับตัวแปรที่ไม่ใช่อะตอมมิกอื่น ๆ -> ไปป์ไลน์การเก็งกำไรการสั่งซื้อผิดพลาดของหน่วยความจำจะถูกลบออกไป
- แทนการใช้
-
กับตัวแปร FP, XOR ไบต์สูงกับ 0x80 พลิกบิตเครื่องหมายที่ก่อให้เกิดแผงลอยร้านค้าส่งต่อ
RDTSC
เวลาแต่ละซ้ำอิสระกับสิ่งที่ยิ่งหนักกว่า เช่นCPUID
/ RDTSC
หรือฟังก์ชั่นเวลาที่ทำให้การเรียกระบบ คำแนะนำในการซีเรียลไลซ์
- เปลี่ยนค่าคูณด้วยค่าคงที่เพื่อหารด้วยส่วนกลับ ("เพื่อความสะดวกในการอ่าน") div ช้าและไม่ได้ถูกวางท่ออย่างเต็มที่
- vectorize คูณ / sqrt กับ AVX (SIMD) แต่ล้มเหลวในการใช้งาน
vzeroupper
ก่อนที่จะโทรไปเกลาคณิตศาสตร์ห้องสมุดexp()
และlog()
ฟังก์ชั่นที่ก่อให้เกิดAVX <-> SSE แผงลอยเปลี่ยนแปลง
- เก็บเอาต์พุต RNG ในรายการที่เชื่อมโยงหรือในอาร์เรย์ที่คุณท่องไปตามลำดับ เหมือนกันสำหรับผลลัพธ์ของการวนซ้ำแต่ละครั้งและหาผลรวมในตอนท้าย
ยังครอบคลุมในคำตอบนี้ แต่ไม่รวมอยู่ในบทสรุป: คำแนะนำที่จะช้าเท่ากับซีพียูที่ไม่ได้ทำงานผิดพลาดหรือไม่ดูเหมือนว่าจะสมเหตุสมผลได้แม้จะมีความสามารถในการก่อวินาศกรรม เช่นแนวคิด gimp-the-compiler จำนวนมากที่สร้าง asm ที่แตกต่างกัน / แย่ลงอย่างเห็นได้ชัด
หลายเธรดไม่ดี
อาจใช้ OpenMP กับลูปแบบหลายเธรดที่มีการวนซ้ำน้อยมากโดยมีค่าใช้จ่ายมากกว่าการเพิ่มความเร็ว รหัส monte-carlo ของคุณมีความเท่าเทียมกันมากพอที่จะได้รับความเร็ว หากเราประสบความสำเร็จในการทำให้การทำซ้ำแต่ละครั้งช้าลง (แต่ละเธรดคำนวณส่วนที่payoff_sum
เพิ่มเข้ามาในตอนท้าย) #omp parallel
ในลูปนั้นน่าจะเป็นการเพิ่มประสิทธิภาพไม่ใช่การมองดูในแง่ร้าย
หลายเธรด แต่บังคับให้เธรดทั้งสองแบ่งใช้ตัวนับลูปเดียวกัน (พร้อมatomic
เพิ่มขึ้นดังนั้นจำนวนการวนซ้ำทั้งหมดจึงถูกต้อง) ดูเหมือนว่ามีเหตุผลที่โหดร้าย นี่หมายถึงการใช้static
ตัวแปรเป็นตัวนับลูป justifies นี้ใช้atomic
สำหรับเคาน์เตอร์ห่วงและสร้างจริงแคชเส้น ping-ponging (ตราบเท่าที่หัวข้อไม่ทำงานบนหลักกายภาพเดียวกันกับ hyperthreading นั่นอาจจะไม่เป็นช้า) อย่างไรก็ตามนี้เป็นมากlock inc
ช้ากว่ากรณียกเลิกการเกี่ยงสำหรับ และlock cmpxchg8b
เพื่อเพิ่มอะตอมเกี่ยงuint64_t
บนระบบ 32 inc
บิตจะต้องลองใหม่อีกครั้งในวงแทนที่จะต้องฮาร์ดแวร์ตัดสินอะตอม
สร้างการแบ่งปันที่ไม่ถูกต้องโดยที่หลาย ๆ เธรดเก็บข้อมูลส่วนตัว (เช่นสถานะ RNG) ในไบต์ที่ต่างกันของบรรทัดแคชเดียวกัน (Intel กวดวิชาเกี่ยวกับเรื่องนี้รวมทั้งเคาน์เตอร์ perf ไปดูที่) มีลักษณะสถาปัตยกรรมเฉพาะนี้เป็น : CPU ของ Intel เก็งกำไรในหน่วยความจำที่ผิดพลาดการสั่งซื้อไม่ได้เกิดขึ้นและมีเครื่องล้างหน่วยความจำการสั่งซื้อเหตุการณ์ perf ในการตรวจสอบเรื่องนี้อย่างน้อยใน P4 การลงโทษอาจไม่ใหญ่เท่าแฮสเวลล์ เมื่อลิงก์ชี้ให้เห็นlock
คำสั่ง ed จะถือว่าเกิดขึ้นโดยหลีกเลี่ยงการเก็งกำไรที่ไม่ถูกต้อง โหลดปกติคาดการณ์ว่าแกนอื่น ๆ จะไม่ทำให้แคชในบรรทัดระหว่างเมื่อโหลดดำเนินการและเมื่อมันเกษียณในการสั่งซื้อโปรแกรม (นอกเสียจากคุณจะใช้pause
) การแบ่งปันที่แท้จริงโดยไม่มีlock
คำแนะนำ ed มักเป็นข้อบกพร่อง มันน่าสนใจที่จะเปรียบเทียบตัวนับลูปแบบไม่ใช้อะตอมร่วมกับเคสอะตอม หากต้องการลดขนาดจริง ๆ ให้เก็บตัวนับอะตอมมิกแบบแบ่งใช้และทำให้การแบ่งปันแบบเท็จในบรรทัดแคชเดียวกันหรือแตกต่างกันสำหรับตัวแปรอื่น ๆ
ความคิดเฉพาะ uarch แบบสุ่ม:
หากคุณสามารถแนะนำสาขาที่ไม่สามารถคาดเดาได้สิ่งนั้นจะทำให้รหัสเสื่อมสภาพลงอย่างมาก ซีพียู x86 รุ่นใหม่มีท่อค่อนข้างยาวดังนั้นค่าใช้จ่ายที่ผิดพลาดประมาณ 15 รอบ (เมื่อเรียกใช้จากแคช uop)
โซ่พึ่งพา:
ฉันคิดว่านี่เป็นส่วนหนึ่งของงานที่ได้รับมอบหมาย
เอาชนะความสามารถของ CPU ในการใช้ประโยชน์จากความเท่าเทียมในระดับคำสั่งโดยการเลือกลำดับของการดำเนินการที่มีห่วงโซ่การพึ่งพายาวหนึ่งสายแทนที่จะเป็นห่วงโซ่พึ่งพาจำนวนสั้น คอมไพเลอร์ไม่ได้รับอนุญาตให้เปลี่ยนลำดับการดำเนินการสำหรับการคำนวณ FP เว้นแต่ว่าคุณจะใช้-ffast-math
เพราะสามารถเปลี่ยนผลลัพธ์ได้ (ดังที่อธิบายไว้ด้านล่าง)
ในการทำให้สิ่งนี้มีประสิทธิภาพจริงๆให้เพิ่มความยาวของห่วงโซ่การพึ่งพาแบบวนรอบที่ดำเนินการ ไม่มีอะไรกระโดดออกมาอย่างชัดเจนแม้ว่า: ลูปตามที่เขียนมีห่วงโซ่การพึ่งพาอาศัยแบบห่วงสั้นมาก: เพียงแค่เพิ่ม FP (3 รอบ) การวนซ้ำหลายครั้งสามารถคำนวณได้ในคราวเดียวเนื่องจากสามารถเริ่มต้นได้ดีก่อนpayoff_sum +=
สิ้นสุดการทำซ้ำครั้งก่อน ( log()
และexp
ทำตามคำแนะนำหลายอย่าง แต่ไม่เกินหน้าต่างออกคำสั่งของ Haswell สำหรับการค้นหาความเท่าเทียม: ROB ขนาด = 192 u fused โดเมนโดเมนและขนาดตัวกำหนดตารางเวลา = uops โดเมนที่ไม่ได้ใช้ 60 ครั้ง. ทันทีที่การประมวลผลของการทำซ้ำปัจจุบันดำเนินไปไกลพอที่จะทำให้มีที่ว่างสำหรับคำแนะนำจากการทำซ้ำครั้งต่อไปที่จะออกชิ้นส่วนใด ๆ ของมันที่มีอินพุตพร้อม (เช่นแยกอิสระ / แยกโซ่) สามารถเริ่มดำเนินการได้ ฟรี (เช่นเนื่องจากมีปัญหาเรื่องคอขวดเนื่องจากความล่าช้าไม่ใช่ปริมาณงาน)
รัฐ RNG addps
เกือบจะแน่นอนจะเป็นห่วงโซ่ดำเนินการพึ่งพานานกว่า
ใช้การดำเนินการ FP ช้าลง / มากขึ้น
หารด้วย 2.0 แทนการคูณ 0.5 และอื่น ๆ FP multiply มีการวางท่ออย่างหนักในการออกแบบของ Intel และมีหนึ่งอัตราต่อ 0.5c สำหรับ Haswell และใหม่กว่า FP divsd
/divpd
เป็นไปป์ไลน์เพียงบางส่วนเท่านั้น (แม้ว่า Skylake จะมีความน่าประทับใจในอัตราการส่งผ่านข้อมูล 4c divpd xmm
แต่ด้วยความหน่วงแฝง 13-14c เทียบกับ Nehalem (7-22c)
การdo { ...; euclid_sq = x*x + y*y; } while (euclid_sq >= 1.0);
ทดสอบระยะไกลนั้นชัดเจนดังนั้นมันจะเหมาะสมกับsqrt()
มัน : P ( sqrt
ช้ากว่าdiv
)
ตามที่ @Paul Clayton แนะนำการเขียนนิพจน์ด้วยการเชื่อมโยง / การแจกแจงแบบเทียบเท่าสามารถแนะนำการทำงานเพิ่มเติมได้ (ตราบใดที่คุณไม่ได้ใช้-ffast-math
เพื่ออนุญาตให้คอมไพเลอร์ทำการปรับให้เหมาะสมอีกครั้ง) อาจจะกลายเป็น(exp(T*(r-0.5*v*v))
exp(T*r - T*v*v/2.0)
โปรดทราบว่าในขณะที่การคำนวณทางคณิตศาสตร์เกี่ยวกับจำนวนจริงนั้นมีความสัมพันธ์กัน แต่คณิตศาสตร์จุดลอยตัวไม่ได้แม้ว่าจะไม่ได้พิจารณา overflow / NaN -ffast-math
ก็ตาม ดูความคิดเห็นของ Paulสำหรับpow()
คำแนะนำที่ซ้อนกันมีขนดกมาก
หากคุณสามารถปรับขนาดการคำนวณลงไปขนาดเล็กจำนวนมากแล้ว Ops คณิตศาสตร์ FP ใช้~ 120 รอบพิเศษในการดักเฟิเมื่อการดำเนินการเกี่ยวกับตัวเลขสองปกติผลิต denormal ดู microarch PDF ของ Agner Fog สำหรับหมายเลขและรายละเอียดที่แน่นอน สิ่งนี้ไม่น่าเป็นไปได้เนื่องจากคุณมีทวีคูณจำนวนมากดังนั้นตัวคูณสเกลจะถูกยกกำลังสองและต่ำไปจนถึง 0.0 ฉันไม่เห็นวิธีที่จะปรับขนาดที่จำเป็นด้วยการไร้ความสามารถ (แม้กระทั่งความโหดร้าย) เพียงความอาฆาตพยาบาทโดยเจตนา
หากคุณสามารถใช้อินทรินสิก ( <immintrin.h>
)
ใช้movnti
เพื่อขับไล่ข้อมูลของคุณจากแคช โหดเหี้ยม: มันใหม่และสั่งซื้ออย่างอ่อนดังนั้นมันจะทำให้ซีพียูทำงานเร็วขึ้นใช่ไหม? หรือดูคำถามที่เชื่อมโยงกันสำหรับกรณีที่มีคนกำลังตกอยู่ในอันตรายที่จะทำสิ่งนี้อย่างสมบูรณ์ (สำหรับการเขียนที่กระจัดกระจายซึ่งมีเพียงบางตำแหน่งที่ร้อน) clflush
อาจเป็นไปไม่ได้หากไม่มีการปองร้าย
ใช้จำนวนเต็มสับเปลี่ยนระหว่างการดำเนินการทางคณิตศาสตร์ FP เพื่อทำให้เกิดการบายพาสล่าช้า
การผสมคำสั่ง SSE และ AVX โดยไม่ต้องใช้vzeroupper
สาเหตุที่เหมาะสมแผงลอยขนาดใหญ่ใน pre-Skylake (และการลงโทษที่แตกต่างกันใน Skylake ) ถึงแม้ว่าจะไม่มีการทำให้ vectorizing แย่ลงกว่าสเกลาร์ (รอบเพิ่มเติมที่ใช้ข้อมูลสับเข้า / ออกของเวกเตอร์มากกว่าที่บันทึกไว้โดยการดำเนินการเพิ่ม / sub / mul / div / sqrt สำหรับการทำซ้ำ 4 Monte-Carlo ในครั้งเดียว . เพิ่ม / ย่อย / หน่วยปฏิบัติมัลไปป์ไลน์กำลังอย่างเต็มที่และเต็มความกว้าง แต่ div และ sqrt บน 256b เวกเตอร์ไม่ได้เป็นอย่างรวดเร็วบน 128b เวกเตอร์ (หรือสเกลา) double
เพื่อเพิ่มความเร็วไม่ได้เป็นอย่างมากสำหรับ
exp()
และlog()
ไม่มีการรองรับฮาร์ดแวร์ดังนั้นส่วนนั้นจะต้องแยกองค์ประกอบเวกเตอร์กลับไปที่สเกลาร์และเรียกใช้ฟังก์ชันไลบรารีแยกต่างหากจากนั้นจึงสับผลลัพธ์กลับเป็นเวกเตอร์ โดยทั่วไปแล้ว libm จะรวบรวมเพื่อใช้ SSE2 เท่านั้นดังนั้นจะใช้การเข้ารหัสแบบดั้งเดิม - SSE ของคำสั่งทางคณิตศาสตร์สเกลาร์ หากรหัสของคุณใช้พาหะและการโทร 256b exp
โดยไม่ทำการโทรvzeroupper
ก่อน หลังจากกลับมาคำสั่ง AVX-128 ต้องการvmovsd
ตั้งค่าองค์ประกอบเวกเตอร์ถัดไปเป็นอาร์กิวเมนต์สำหรับexp
จะหยุดชะงัก จากนั้นexp()
จะหยุดอีกครั้งเมื่อเรียกใช้คำสั่ง SSE นี่คือสิ่งที่เกิดขึ้นในคำถามนี้ทำให้เกิดการชะลอตัว 10 เท่า (ขอบคุณ @ZBoson)
ดูการทดลองของ Nathan Kurz กับ lib คณิตศาสตร์กับ glibc ของ Intel สำหรับโค้ดนี้นี้ glibc ในอนาคตจะมาพร้อมกับการใช้งานแบบเวกเตอร์exp()
และอื่น ๆ
หากกำหนดเป้าหมายล่วงหน้า IvB หรือ esp ตอนนี้ลองรับ gcc เพื่อทำให้แผงลอยลงทะเบียนบางส่วนกับการดำเนินการ 16 บิตหรือ 8 บิตตามด้วยการดำเนินงาน 32 บิตหรือ 64 บิต ในกรณีส่วนใหญ่ gcc จะใช้movzx
หลังจากการดำเนินการ 8 หรือ 16 บิต แต่นี่เป็นกรณีที่ gcc แก้ไขah
แล้วอ่านax
ด้วย asm (inline):
ด้วย asm (inline) คุณสามารถแบ่งแคช uop: โค้ด 32B ที่ไม่พอดีกับแคช 6uop สามบรรทัดบังคับให้สวิตช์จาก uop cache ไปยังตัวถอดรหัส คนไร้ความสามารถที่ALIGN
ใช้ไบต์เดี่ยวจำนวนมากnop
แทนที่จะยาวสองสามnop
วินาทีบนเป้าหมายสาขาภายในวงด้านในอาจทำการหลอก หรือใส่การจัดตำแหน่งช่องว่างด้านหลังฉลากแทนที่จะเป็นมาก่อน : P สิ่งนี้จะสำคัญก็ต่อเมื่อส่วนหน้าเป็นคอขวดซึ่งจะไม่เกิดขึ้นถ้าเราประสบความสำเร็จในการมองดูส่วนที่เหลือของรหัส
ใช้รหัสการแก้ไขด้วยตนเองเพื่อทริกเกอร์การล้างข้อมูลไปป์ไลน์ (aka machine-nukes)
LCP แผงลอยจากคำสั่ง 16 บิตที่มีขนาดใหญ่เกินไปในทันทีเพื่อให้พอดีกับ 8 บิตไม่น่าจะมีประโยชน์ แคช uop บน SnB และใหม่กว่าหมายความว่าคุณจ่ายค่าปรับการถอดรหัสเพียงครั้งเดียว บน Nehalem (i7 แรก) อาจทำงานได้กับลูปที่ไม่พอดีกับบัฟเฟอร์ 28 uop gcc บางครั้งจะสร้างคำแนะนำดังกล่าวแม้จะมี-mtune=intel
และเมื่อมันสามารถใช้คำสั่ง 32 บิต
สำนวนที่พบบ่อยสำหรับระยะเวลาที่เป็นCPUID
(เพื่อเป็นอันดับ) RDTSC
แล้ว เวลาย้ำทุกแยกที่มีCPUID
/ RDTSC
เพื่อให้แน่ใจว่าRDTSC
จะไม่จัดลำดับใหม่พร้อมกับคำแนะนำก่อนหน้านี้ซึ่งจะช้าสิ่งลงมาก (ในชีวิตจริงวิธีที่ชาญฉลาดในเวลาคือการทำซ้ำทั้งหมดเข้าด้วยกันแทนที่จะกำหนดเวลาแยกจากกันและรวมเข้าด้วยกัน)
ทำให้แคชหายไปจำนวนมากและหน่วยความจำช้าลง
ใช้union { double d; char a[8]; }
สำหรับตัวแปรบางตัวของคุณ ทำให้แผงลอยการส่งต่อร้านค้าโดยทำร้านแคบ (หรืออ่าน - แก้ไข - เขียน) ให้เป็นหนึ่งในไบต์ (บทความ wiki นั้นยังครอบคลุมเนื้อหา microarchitectural อื่น ๆ อีกมากมายสำหรับการโหลด / จัดคิว) เช่นพลิกสัญลักษณ์ของการdouble
ใช้ XOR 0x80 บนไบต์สูงแทนที่จะใช้-
ตัวดำเนินการ ผู้พัฒนาที่ไร้ความสามารถอย่างบ้าคลั่งอาจได้ยินว่า FP ช้ากว่าจำนวนเต็มและพยายามทำมากที่สุดโดยใช้ ops จำนวนเต็ม (คอมไพเลอร์ที่ดีมากที่กำหนดเป้าหมายทางคณิตศาสตร์ FP ในการลงทะเบียน SSE อาจจะรวบรวมสิ่งนี้กับxorps
ด้วยค่าคงที่ในการลงทะเบียน xmm อื่น แต่วิธีเดียวที่ไม่น่ากลัวสำหรับ x87 คือถ้าคอมไพเลอร์ตระหนักว่ามันเป็นการลบค่าและแทนที่การบวกถัดไปด้วยการลบ)
ใช้volatile
ถ้าคุณกำลังคอมไพล์ด้วย-O3
และไม่ได้ใช้std::atomic
เพื่อบังคับให้คอมไพเลอร์เก็บ / โหลดซ้ำทั่วสถานที่จริง ตัวแปรทั่วโลก (แทนที่จะเป็นคนในพื้นที่) จะบังคับให้ร้านค้า / รีโหลดบางส่วน แต่การสั่งซื้อที่อ่อนแอของรุ่น C + + หน่วยความจำไม่จำเป็นต้องให้คอมไพเลอร์รั่วไหล / โหลดซ้ำไปยังหน่วยความจำตลอดเวลา
แทนที่ vars โลคัลด้วยสมาชิกของ struct ขนาดใหญ่เพื่อให้คุณสามารถควบคุมโครงร่างหน่วยความจำ
ใช้อาร์เรย์ในโครงสร้างสำหรับการขยาย (และการจัดเก็บตัวเลขสุ่มเพื่อแสดงว่ามีอยู่)
เลือกรูปแบบหน่วยความจำของคุณเพื่อให้ทุกอย่างเป็นไปเป็นเส้นที่แตกต่างกันใน "ชุด" เดียวกันในแคช มันมีการเชื่อมโยง 8 ทางเท่านั้นนั่นคือแต่ละชุดมี 8 "วิธี" บรรทัดแคชคือ 64B
ยิ่งไปกว่านั้นนำสิ่งที่ตรง 4096B กันตั้งแต่โหลดมีการพึ่งพาที่ผิดพลาดในร้านค้าไปยังหน้าเว็บที่แตกต่างกัน แต่มีเดียวกันชดเชยภายในหน้า ก้าวร้าวออกจากการสั่งซื้อซีพียูใช้หน่วยความจำ Disambiguation ที่จะคิดออกเมื่อโหลดและร้านค้าสามารถจัดลำดับใหม่โดยไม่ต้องเปลี่ยนผลและการดำเนินการของอินเทลมีเท็จบวกที่ป้องกันไม่ให้โหลดเริ่มต้น อาจเป็นเพียงการตรวจสอบบิตด้านล่างออฟเซ็ตของหน้าดังนั้นการตรวจสอบจึงสามารถเริ่มต้นได้ก่อนที่ TLB จะแปลบิตสูงจากเพจเสมือนเป็นเพจจริง เช่นเดียวกับมัคคุเทศก์ของ Agner ให้ดูคำตอบจาก Stephen Canonและหมวดใกล้กับคำตอบของ @Krazy Glew ในคำถามเดียวกัน (Andy Glew เป็นหนึ่งในสถาปนิกของ P6 microar Architecture ดั้งเดิมของ Intel)
ใช้__attribute__((packed))
เพื่อให้คุณจัดแนวตัวแปรที่ไม่ถูกต้องเพื่อให้พวกเขาขยายขอบเขตแคชหรือแม้แต่ขอบเขตของหน้า (ดังนั้นการโหลดหนึ่งdouble
ต้องการข้อมูลจากสองบรรทัดแคช) โหลดที่ไม่ตรงแนวไม่มีโทษใน Intel i7 uarch ใด ๆ ยกเว้นเมื่อข้ามบรรทัดแคชและบรรทัดของหน้า การแยกแคชบรรทัดยังคงใช้รอบเพิ่มเติม Skylake ลดบทลงโทษสำหรับการโหลดแยกหน้าอย่างมากจาก 100 เป็น 5 รอบ (ส่วน 2.1.3) อาจเกี่ยวข้องกับความสามารถในการทำสองหน้าเดินขนานกัน
การแบ่งหน้าบนatomic<uint64_t>
ควรเป็นกรณีที่เลวร้ายที่สุดโดยเฉพาะ ถ้ามันเป็น 5 ไบต์ในหนึ่งหน้าและ 3 ไบต์ในอีกหน้าหรืออย่างอื่นที่ไม่ใช่ 4: 4 แม้การแยกลงตรงกลางจะมีประสิทธิภาพมากขึ้นสำหรับการแบ่งแคชกับเวกเตอร์ 16B ในบางระบบ IIRC ใส่ทุกอย่างไว้ในalignas(4096) struct __attribute((packed))
(เพื่อประหยัดพื้นที่แน่นอน) รวมถึงอาร์เรย์สำหรับจัดเก็บข้อมูลสำหรับผลลัพธ์ RNG บรรลุแนวที่ไม่ถูกต้องโดยใช้uint8_t
หรือuint16_t
ทำอะไรบางอย่างก่อนเคาน์เตอร์
ถ้าคุณได้รับคอมไพเลอร์ที่จะใช้จัดทำดัชนีที่อยู่โหมดที่จะเอาชนะ UOP ไมโครฟิวชั่น อาจจะโดยการใช้เพื่อแทนที่ตัวแปรสเกลาง่ายด้วย#define
my_data[constant]
หากคุณสามารถแนะนำทางอ้อมในระดับพิเศษดังนั้นจึงไม่ทราบที่อยู่โหลด / ร้านค้าในช่วงต้นซึ่งจะช่วยลดผลกระทบต่อไปได้
ทราเวิร์อาร์เรย์ในลำดับที่ไม่ต่อเนื่องกัน
ฉันคิดว่าเราสามารถหาเหตุผลที่ไร้ความสามารถในการแนะนำอาร์เรย์ในตอนแรก: มันช่วยให้เราแยกการสร้างหมายเลขสุ่มจากการใช้ตัวเลขสุ่ม ผลลัพธ์ของการวนซ้ำแต่ละครั้งอาจถูกเก็บไว้ในอาร์เรย์เพื่อหาผลรวมในภายหลัง (ด้วยความสามารถที่โหดร้ายมากขึ้น)
สำหรับ "randomness สูงสุด" เราสามารถให้เธรดวนซ้ำผ่านอาร์เรย์ที่สุ่มเขียนตัวเลขสุ่มใหม่เข้าไปได้ เธรดที่ใช้ตัวเลขสุ่มสามารถสร้างดัชนีสุ่มเพื่อโหลดตัวเลขสุ่มได้ (มีบางส่วนทำให้ที่นี่ทำงาน แต่ microarchitecturally ช่วยให้ที่อยู่โหลดเป็นที่รู้จักก่อนดังนั้นภาระภาระที่เป็นไปได้ใด ๆ สามารถแก้ไขได้ก่อนที่จะโหลดข้อมูลที่ต้องการ) การมีเครื่องอ่านและตัวเขียนบนแกนที่แตกต่างกัน ไปป์ไลน์การคำนวณพิเศษ (ตามที่กล่าวไว้ก่อนหน้านี้สำหรับกรณีการแชร์เท็จ)
เพื่อให้ได้ผลในแง่ร้ายสูงสุดให้วนลูปมากกว่าอาร์เรย์ของคุณด้วยความยาว 4096 ไบต์ (เช่น 512 doubles) เช่น
for (int i=0 ; i<512; i++)
for (int j=i ; j<UPPER_BOUND ; j+=512)
monte_carlo_step(rng_array[j]);
รูปแบบการเข้าถึงคือ 0, 4096, 8192, ... ,
8, 4104, 8200, ...
16, 4112, 8208, ...
นี่คือสิ่งที่คุณจะได้รับจากการเข้าถึงอาร์เรย์ 2 มิติตามลำดับdouble rng_array[MAX_ROWS][512]
ที่ไม่ถูกต้อง (วนซ้ำแถวแทนที่จะเป็นคอลัมน์ภายในหนึ่งแถวในวงในตามที่แนะนำโดย @JesperJuhl) หากการไร้ความสามารถที่โหดร้ายสามารถแสดงให้เห็นถึงอาเรย์ 2 มิติที่มีขนาดเช่นนั้นการที่ความหลากหลายของสวนในโลกแห่งความเป็นจริงนั้นง่ายต่อการวนซ้ำกับรูปแบบการเข้าถึงที่ผิด สิ่งนี้เกิดขึ้นในรหัสจริงในชีวิตจริง
ปรับขอบเขตการวนซ้ำถ้าจำเป็นเพื่อใช้หน้าต่าง ๆ แทนการใช้ซ้ำหน้าไม่กี่หน้าซ้ำหากอาร์เรย์ไม่ใหญ่มาก การดึงข้อมูลล่วงหน้าของฮาร์ดแวร์ไม่สามารถใช้งานได้ prefetcher สามารถติดตามหนึ่งไปข้างหน้าและสตรีมย้อนกลับหนึ่งรายการภายในแต่ละหน้า (ซึ่งเป็นสิ่งที่เกิดขึ้นที่นี่) แต่จะดำเนินการกับมันหากแบนด์วิดท์หน่วยความจำไม่อิ่มตัวด้วยการดึงข้อมูลล่วงหน้าที่ไม่ใช่
สิ่งนี้จะสร้างการพลาด TLB จำนวนมากเว้นแต่หน้าจะถูกรวมเข้ากับ hugepage ( Linux จะทำเช่นนี้เพื่อการจัดสรรที่ไม่ระบุชื่อ (ไม่ใช่ไฟล์ที่สำรองข้อมูล) เช่นmalloc
/ การnew
ใช้งานmmap(MAP_ANONYMOUS)
)
แทนที่จะเป็นอาร์เรย์เพื่อเก็บรายการผลลัพธ์คุณสามารถใช้รายการที่เชื่อมโยงได้ จากนั้นการวนซ้ำทุกครั้งจะต้องมีการโหลดตัวชี้การไล่ (ความเสี่ยงต่อการพึ่งพา RAW ที่แท้จริงสำหรับที่อยู่โหลดของการโหลดครั้งถัดไป) ด้วยตัวจัดสรรที่ไม่ดีคุณอาจจัดการเพื่อกระจายโหนดรายการรอบ ๆ ในหน่วยความจำเอาชนะแคช ด้วยตัวจัดสรรที่ไร้ความสามารถมันสามารถใส่ทุกโหนดที่จุดเริ่มต้นของหน้าของมันเอง (เช่นจัดสรรmmap(MAP_ANONYMOUS)
โดยตรงโดยไม่ต้องแยกหน้าหรือติดตามขนาดวัตถุเพื่อสนับสนุนอย่างเหมาะสมfree
)
นี่ไม่ใช่เฉพาะสถาปัตยกรรมแบบไมโครและมีส่วนเกี่ยวข้องกับขั้นตอนการทำงานเพียงเล็กน้อย (ส่วนใหญ่จะเป็นการชะลอตัวของ CPU ที่ไม่ใช่ไปป์ไลน์)
ค่อนข้างปิดหัวข้อ: ทำให้คอมไพเลอร์สร้างรหัสที่แย่ลง / ทำงานได้มากขึ้น:
ใช้ C ++ 11 std::atomic<int>
และstd::atomic<double>
สำหรับรหัสที่ผิดพลาดมากที่สุด MFENCEs และlock
คำแนะนำ ed ค่อนข้างช้าแม้จะไม่มีการโต้แย้งจากเธรดอื่น
-m32
จะทำให้รหัสช้าลงเนื่องจากรหัส x87 จะแย่กว่ารหัส SSE2 32bit เรียกประชุมสแต็คตามคำแนะนำจะใช้เวลามากขึ้นและผ่าน args FP exp()
แม้ในกองการทำงานเช่น atomic<uint64_t>::operator++
เปิด-m32
ต้องการlock cmpxchg8B
ลูป (i586) (ใช้มันเพื่อเคาน์เตอร์ลูป!
-march=i386
จะทำให้ลดน้อยลง (ขอบคุณ @Jesper) FP เปรียบเทียบกับfcom
จะช้ากว่า fcomi
686 Pre-586 ไม่ได้จัดเก็บ atomic 64 บิต (นับประสา cmpxchg) ดังนั้น 64 บิตatomic
ops ทั้งหมดจึงรวบรวมการเรียกใช้ฟังก์ชั่น libgcc (ซึ่งอาจจะรวบรวมสำหรับ i686 แทนที่จะใช้ล็อคจริง) ลองใช้ลิงค์ Godbolt Compiler Explorer ในย่อหน้าสุดท้าย
ใช้long double
/ sqrtl
/ expl
เพื่อความแม่นยำและความช้าพิเศษใน ABIs โดยที่ sizeof ( long double
) เท่ากับ 10 หรือ 16 (พร้อมการเว้นระยะห่างสำหรับการจัดตำแหน่ง) (IIRC, 64 บิต Windows ใช้ 8byte long double
เทียบเท่ากับdouble
(โหลด / เก็บ 10byte (80 บิต) ตัวถูกดำเนินการ FP คือ 4/7 uops เทียบกับfloat
หรือdouble
รับเพียง 1 uop ต่อfld m64/m32
/ fst
) บังคับ x87 ด้วยการlong double
เอาชนะ auto-vectorization แม้ -m64 -march=haswell -O3
GCC
หากไม่ได้ใช้atomic<uint64_t>
ตัวนับลูปให้ใช้long double
กับทุกอย่างรวมถึงตัวนับลูป
atomic<double>
คอมไพล์ แต่การดำเนินการอ่าน - แก้ไข - เขียนเช่น+=
ไม่ได้รับการสนับสนุน (แม้ใน 64 บิต) atomic<long double>
ต้องเรียกใช้ฟังก์ชันไลบรารีสำหรับโหลด / เก็บอะตอมเท่านั้น มันอาจไม่มีประสิทธิภาพจริง ๆเพราะ x86 ISA ไม่รองรับการโหลด / ร้านค้าขนาด 10byte ของอะตอมและวิธีเดียวที่ฉันสามารถคิดได้โดยไม่ต้องล็อก ( cmpxchg16b
) ต้องใช้โหมด 64 บิต
ที่การ-O0
สลายการแสดงออกโดยการกำหนดชิ้นส่วนให้กับ vars ชั่วคราวจะทำให้ร้านค้า / โหลดซ้ำมากขึ้น หากไม่มีvolatile
สิ่งใดสิ่งนี้จะไม่สำคัญกับการตั้งค่าการปรับให้เหมาะสมที่รหัสจริงของจริงจะใช้
C กฎ aliasing อนุญาตให้char
ไปยังนามแฝงอะไรดังนั้นการจัดเก็บผ่านchar*
กองกำลังรวบรวมเพื่อทุกอย่าง / ร้านโหลดก่อน / -O3
หลังไบต์ร้านได้ที่ (นี่เป็นปัญหาสำหรับรหัสการทำให้เป็นuint8_t
เวกเตอร์อัตโนมัติที่ทำงานกับอาเรย์ของเป็นต้น)
ลองใช้uint16_t
ตัวนับลูปเพื่อบังคับให้มีการตัดเป็น 16 บิตโดยใช้ขนาดตัวถูกดำเนินการ 16 บิต (แผงควบคุมที่เป็นไปได้) และ / หรือmovzx
คำแนะนำพิเศษ(ปลอดภัย) ลงนามล้นเป็นพฤติกรรมที่ไม่ได้กำหนดไว้ดังนั้นถ้าคุณใช้-fwrapv
หรืออย่างน้อย-fno-strict-overflow
, เคาน์เตอร์ลงนามวงจะได้ไม่ต้องเป็นอีกสัญญาณที่ขยายทุกซ้ำแม้ว่าใช้เป็นชดเชยที่จะชี้ 64bit
บังคับการแปลงจากจำนวนเต็มเป็นfloat
และย้อนกลับอีกครั้ง และ / หรือdouble
<=> float
การแปลง คำแนะนำมีเวลาแฝงมากกว่าหนึ่งและ scalar int-> float ( cvtsi2ss
) ได้รับการออกแบบมาไม่ดีเพื่อไม่ให้ส่วนที่เหลือของการลงทะเบียน xmm เป็นศูนย์ (gcc แทรกส่วนเพิ่มเติมpxor
เพื่อแยกการขึ้นต่อกันด้วยเหตุนี้)
ตั้งค่า CPU affinity ของคุณเป็น CPU อื่นบ่อยครั้ง(แนะนำโดย @Egwor) การใช้เหตุผลที่โหดร้าย: คุณไม่ต้องการให้แกนใดแกนหนึ่งร้อนเกินไปจากการรันเธรดของคุณเป็นเวลานานใช่ไหม? บางทีการเปลี่ยนไปใช้คอร์อื่นจะทำให้คอร์นั้นมีความเร็วสัญญาณนาฬิกาสูงขึ้น (ในความเป็นจริง: พวกมันใกล้กันมากจนแทบไม่น่าเป็นไปได้ยกเว้นในระบบมัลติซ็อกเก็ต) ตอนนี้เพิ่งปรับจูนผิดและทำบ่อยเกินไป นอกเหนือจากเวลาที่ใช้ในการบันทึก / เรียกคืนสถานะเธรด OS แกนใหม่นั้นมีแคช L2 / L1 เย็นแคช uop และตัวพยากรณ์สาขา
การแนะนำการเรียกใช้ระบบที่ไม่จำเป็นบ่อยครั้งจะทำให้คุณช้าลงไม่ว่าพวกเขาจะเป็นอะไร แม้ว่าสิ่งที่สำคัญ แต่เรียบง่ายเช่นgettimeofday
นั้นอาจถูกนำไปใช้ในพื้นที่ผู้ใช้ด้วยโดยไม่มีการเปลี่ยนไปใช้โหมดเคอร์เนล (glibc บน Linux ทำสิ่งนี้ด้วยความช่วยเหลือของเคอร์เนลเนื่องจากเคอร์เนลส่งออกโค้ดในvdso
)
สำหรับข้อมูลเพิ่มเติมเกี่ยวกับค่าใช้จ่ายในการโทรของระบบ (รวมถึงการสูญเสียแคช / TLB หลังจากกลับไปที่พื้นที่ผู้ใช้ไม่ใช่เพียงแค่การสลับบริบท) กระดาษ FlexSCมีการวิเคราะห์เคาน์เตอร์ที่สมบูรณ์แบบของสถานการณ์ปัจจุบันรวมถึงข้อเสนอสำหรับระบบแบทช์ การเรียกใช้จากกระบวนการเซิร์ฟเวอร์แบบมัลติเธรดขนาดใหญ่
while(true){}