Branch Predictor แบบ 2-Ahead ของ Zen 5: แนวคิดอายุ 30 ปีที่เปิดความเป็นไปได้ใหม่
(chipsandcheese.com)- AMD Zen 5 นำ 2-Ahead Branch Predictor มาใช้ท่ามกลางการ ออกแบบใหม่ทั้งส่วนหน้า ของสถาปัตยกรรม Zen และดึงงานวิจัยการทำนายสาขาแบบนำหน้าหลายบล็อกจากทศวรรษ 1990 กลับมาสู่การขยายส่วนหน้า x86 สมัยใหม่
- การทำนายสาขาเป็นกลไกที่ทำให้ CPU frontend ดึงคำสั่งต่อไปได้แม้ผลของ conditional branch จะยังไม่ออกมา ส่วนการทำนายผิดจะนำไปสู่ pipeline flush และต้นทุนในการเริ่มใหม่
- Zen 5 มี fetch pipe 2 ชุดที่ 32 ไบต์/รอบ จาก L1 instruction cache ขนาด 32KB โดยแต่ละชุดเชื่อมกับ 4-wide decode cluster และ Op Cache ก็เป็นโครงสร้าง dual-ported 6-wide ที่จ่าย operand ได้สูงสุด 12 รายการให้ Op Queue
- โครงสร้างใหม่นี้ประมวลผล taken branch 2 รายการ ในหนึ่งรอบข้ามบล็อกคำสั่งที่ไม่ต่อเนื่องกัน และใช้ฟิลด์ความยาว 5 บิตเพื่อหาจุดเริ่มต้นของ prediction window ที่สาม ลดการใช้ทรัพยากร decode และ Op Cache เกินจำเป็น
- เนื่องจาก x86 มีคำสั่งความยาวแปรผัน จึงทำให้การตัดสินขอบเขตคำสั่งแบบขนานทำได้ยาก ดังนั้น 2-Ahead Branch Predictor และโครงสร้าง fetch·decode แบบคู่จึงกลายเป็นการเปลี่ยนแปลงหลักในการขยาย frontend ของตระกูล Zen
จุดที่ frontend ของ Zen 5 เปลี่ยนไป
- AMD แนะนำ Zen 5 ว่าเป็นการ ออกแบบใหม่ทั้งส่วนหน้า ของสถาปัตยกรรม Zen และ 2-Ahead Branch Predictor ก็เป็นส่วนหนึ่งที่โดดเด่นในความเปลี่ยนแปลงนั้น
- โครงสร้างนี้อิงจากแนวคิดที่เคยกล่าวถึงในเปเปอร์เมื่อ 30 ปีก่อน และกลับมาปรากฏอีกครั้งท่ามกลางแนวโน้มที่ต้องการยกระดับประสิทธิภาพ single core ให้สูงขึ้น
- หากทำนาย taken branch หลายรายการได้ในครั้งเดียว frontend จะสามารถมองล่วงหน้าไปถึง instruction stream ที่อยู่ไกลกว่าเดิมได้
เหตุผลที่ CPU pipeline ต้องมีการทำนายสาขา
- CPU สมัยใหม่ใช้โครงสร้าง pipeline ที่แบ่งการดึงคำสั่ง (fetch), decode และ execution ออกเป็นหลายขั้น
- เมื่อมี conditional branch frontend ต้องตัดสินตำแหน่งคำสั่งถัดไปที่จะดึงมาก่อนที่การประเมินเงื่อนไขจะเสร็จ
- ตัวเลือกหลักมีสองแบบ
- หยุดรอจนกว่าผลของ branch จะออกมา
- ทำนายเส้นทางถัดไป และถ้าผิดก็ทิ้งงานที่ทำผิดไป แล้วเริ่มใหม่จากจุดที่แน่นอน
- หากทำนายผิด จะต้องทำ flush เพื่อนำงานที่พึ่งพาการคาดเดานั้นออกจาก pipeline
- ต้นทุนของการหยุดที่เงื่อนไข branch แปรผันตามจำนวนขั้นของ pipeline ระหว่าง instruction fetch กับการประเมิน branch condition ที่เสร็จสิ้น
- สุดท้าย CPU จึงต้องทำนายลำดับการไหลของคำสั่งในโปรแกรมให้แม่นยำที่สุดเท่าที่เป็นไปได้
บริบทที่ทำให้แนวคิด 2-Ahead เกิดขึ้น
- วิธีทำนายแบบง่ายเคยมีกลยุทธ์ที่มอง backward jump สั้น ๆ ว่าเป็น taken เสมอ ซึ่งในอดีตให้ประโยชน์มากกว่าวิธีที่ดึงที่อยู่ถัดไปเสมอ
- หากเก็บสถานะเล็ก ๆ เช่นประวัติ branch ล่าสุดหรือบันทึก address ก็จะได้ผลลัพธ์ที่ดีกว่าในโปรแกรมจริง
- แม้ใช้โครงสร้างขนาดเพียงหลักสิบ KB ก็สามารถทำอัตราทำนาย branch ได้ใน ช่วงปลาย 90%
- 2-Ahead Branch Predictor เป็นข้อเสนอที่มีการถกเถียงมาตั้งแต่ต้นทศวรรษ 1990 และในตอนนั้นก็มีการพูดถึงปัญหาการขยายความกว้างของสถาปัตยกรรมเป็น 8-wide หรือมากกว่าควบคู่กัน
- ต่อมาเมื่อ CPU เชิงพาณิชย์ย้ายไปสู่ multicore พื้นที่ของแต่ละคอร์จึงสำคัญขึ้น และแวดวงวิชาการก็หันไปมุ่งปรับปรุง TAGE predictor ที่มีประสิทธิภาพต่อพื้นที่มากกว่า
- เมื่อการย่อกระบวนการผลิตทำให้ใส่ทรานซิสเตอร์ได้มากขึ้นในพื้นที่ใกล้เคียงเดิม ความสนใจต่อประสิทธิภาพ single core ก็เพิ่มขึ้นอีกครั้ง ควบคู่กับกระแสการขยายไปถึง CPU แบบ out-of-order หลายร้อยตัว
เหตุผลที่ frontend ของ x86 ยุ่งยากกว่า
- ISA ที่ใช้คำสั่งความยาวคงที่อย่าง 64-bit Arm สามารถ decode ส่วนใดก็ได้ของ instruction cache line แบบขนานได้ง่าย
- แค่แบ่งข้อมูลอินพุตตามขอบ byte ของคำสั่งที่รับประกันไว้ แล้วทำซ้ำ decoder logic
- x86 มีความยาวคำสั่งแบบแปรผัน จึงต้อง parse instruction byte แบบเชิงเส้นเพื่อรู้ขอบเขตถัดไปของแต่ละคำสั่ง
- สามารถทำขนานได้บางส่วนด้วย pipelining เช่น partial decode ของ prefix ก่อน แต่ต้นทุนบน x86 ไม่ได้ต่ำ
- นี่เป็นเบื้องหลังที่ทำให้ 4-wide decode พบได้ทั่วไปในคอร์ x86 ที่เน้นประสิทธิภาพมาอย่างยาวนาน
- การออกแบบอย่าง 6-wide decode ของ Intel Golden Cove เป็นไปได้เชิงพาณิชย์เพราะความหนาแน่นของลอจิกในกระบวนการผลิตสมัยใหม่เพิ่มขึ้น แต่ต้นทุนด้านพื้นที่และพลังงานของ monolithic parallel x86 decoding จะเพิ่มแบบ super-linear ตามการเพิ่ม width
- โค้ดจำนวนเต็มของแอปพลิเคชันทั่วไปมี branch density ประมาณ branch 1 รายการต่อคำสั่ง 5~6 คำสั่ง ทำให้แรงจูงใจในการขยายความกว้างของ decoder ไปมากกว่านั้นลดลง
- frontend ของ x86 ต้องการวิธีข้ามขั้นตอนการตัดสินขอบเขตคำสั่งที่ทำขนานได้ยาก และไปถึงขอบเขตคำสั่งถัดไปได้อย่างปลอดภัย
การใช้งาน fetch·decode แบบคู่ใน Zen 5
- “Multiple-block ahead branch predictors” โดย Seznec et al. เป็นเปเปอร์หลักที่อธิบายเหตุผลและวิธีใช้งาน 2-Ahead Branch Predictor
- แค่มี predictor ที่จัดการ taken branch หลายรายการยังไม่พอ และ Seznec et al. เห็นว่าหากต้องการใช้ประโยชน์โดยไม่ทำให้ความต้องการด้านพื้นที่พุ่งสูง จำเป็นต้องมี instruction fetch dual-porting
- Zen 5 เปลี่ยน instruction fetch และ Op Cache เป็นโครงสร้าง dual-port
- ใช้ fetch pipe 2 ชุดที่ 32 ไบต์/รอบ จาก L1 instruction cache ขนาด 32KB
- fetch pipe แต่ละชุดเชื่อมต่อกับ 4-wide decode cluster ของตัวเอง
- Op Cache เป็นดีไซน์ dual-ported 6-wide และจ่าย operand ได้สูงสุด 12 รายการ ให้ Op Queue
- Branch Target Buffer ก็รวมอยู่ในส่วนที่ทำ dual-porting ด้วย
- ขนาดใหญ่ของ L1 BTB ใน Zen 5 ที่เข้าถึงได้ 16K entries อาจอธิบายความเป็นไปได้ของ L1 BTB แบบ dual-ported
- L2 BTB มี 8K entries ซึ่งเล็กกว่า L1 BTB
- AMD ใช้ L2 BTB คล้ายกับ victim cache โดย entry ที่ถูก evict จาก L1 BTB จะย้ายไปยัง L2 BTB
วิธีประมวลผล taken branch 2 รายการในหนึ่งรอบ
- Zen 5 สามารถประมวลผล taken branch 2 รายการ ในหนึ่งรอบข้ามบล็อกคำสั่งที่ไม่ต่อเนื่องกันได้
- การเปลี่ยนแปลงนี้ลดการสูญเสีย fetch bandwidth เมื่อพบ taken branch และทำให้ทำนายข้าม taken branch สองรายการไปได้
- สามารถมอง instruction stream หลัง taken branch ที่สองได้ไกลขึ้น จึงมี prediction window ได้สามชุด
- prediction window ทั้งสามสามารถใช้ในการผลิตคำสั่งสำหรับ decode ได้ทั้งหมด
- prediction window ที่สองมี length field 5 บิต แนบอยู่
- ป้องกันสถานการณ์ที่ทรัพยากร decode หรือ Op Cache ถูกจองมากเกินไป
- มีขนาดเล็กกว่า pointer แต่ให้จุดเริ่มต้นของ prediction window ที่สามได้
- เมื่อ prediction window ที่สามข้าม cache line boundary ก็ไม่จำเป็นต้องเก็บสถานะเพิ่มเติมใน prediction lookup index สำหรับรอบถัดไป
- หาก prediction window ที่สามอยู่ใน cache line เดียวกับ prediction window แรกหรือที่สอง partial third window นั้นจะไม่ได้ผลเท่ากับ prediction window ที่สามแบบเต็ม
ข้อจำกัดที่ยังเหลือใน SMT
- เมื่อเปิดใช้งานสองเธรดใน Zen 5 fetch pipe ที่ผูกกับ decode cluster จะถูก แบ่ง partition แบบคงที่
- ในสถานะนี้ หากจะทำงานเหมือน dual fetch core จำเป็นต้อง fetch จากทั้ง L1 instruction cache และ Op Cache
- เหตุผลที่ AMD ทำ Op Cache เป็น dual-port อาจเพื่อรักษา dual fetch pipeline ให้ทำงานได้ดีขึ้น
เปเปอร์ที่เกี่ยวข้องซึ่งถูกกล่าวถึงร่วมกัน
- “Multiple-block ahead branch predictors” โดย Seznec et al. – ASPLOS 1996: กล่าวถึงเหตุผลและแนวทางการใช้งาน 2-Ahead Branch Predictor
- “Optimization of Instruction Fetch Mechanisms for High Issue Rates” โดย Conte et al. – ISCA 1995: กล่าวถึงการปรับปรุงกลไก instruction fetch สำหรับ issue rate สูง
- “Increasing the instruction fetch rate via multiple branch prediction and a branch address cache” โดย Yeh et al. – ICS 1993: กล่าวถึงการเพิ่ม fetch rate ผ่าน multiple branch prediction และ branch address cache
- “Out-of-Order Instruction Fetch using Multiple Sequencers” โดย Oberoi and Sohi – ICPP’02: กล่าวถึง out-of-order instruction fetch ที่ใช้ multiple sequencer
- “Parallelism in the Front-End” โดย Oberoi and Sohi – ISCA 2003: กล่าวถึง parallelism ใน frontend ของ CPU
1 ความคิดเห็น
ความคิดเห็นใน Hacker News
มีบทความที่อธิบายการทำนายการแตกแขนงได้ดีตั้งแต่การอิมพลีเมนต์ช่วงแรก ๆ คือ https://danluu.com/branch-prediction/
[0]: https://www.youtube.com/watch?v=nhXevKMm3JI&list=PLzH6n4zXuc...
[1]: https://www.youtube.com/watch?v=nczJ58WvtYo&list=PLzH6n4zXuc...
อยากรู้ว่าประสิทธิภาพของ SMT จะออกมาเป็นอย่างไร และคาดว่าแนวทางครั้งนี้จะให้ผลดีพร้อมถูกขัดเกลาเพิ่มในรุ่นถัดไป
Zen5c ไปได้ถึง 192 คอร์ หรือ 384 vCPU และใน Zen 6c ปีหน้าน่าจะไปถึง 256 คอร์ได้ ถ้าเป็นเซิร์ฟเวอร์ 1U แบบดูอัลซ็อกเก็ตก็อาจกลายเป็น 512 คอร์, 1024 vCPU ได้
ปัญหาการสเกลเว็บแอปที่เคยเจอในปี 2014 ตอนนี้ถ้าระบายความร้อนได้ก็น่าจะยัดลงในเซิร์ฟเวอร์เครื่องเดียวได้แล้ว แค่คิด 1 RPS ต่อ vCPU ก็ได้ 1000 RPS แบบยังไม่รวม cache hit และแม้แต่หน้าแรกของ HN เองก็คงไม่ได้โดน 1000 pageview ต่อวินาทีจากเซิร์ฟเวอร์
แต่อยากรู้ว่าประสิทธิภาพฝั่ง HPC จะเป็นอย่างไร เรื่องการระบายความร้อนอาจไม่ใช่ปัญหาใหญ่ในตัวมันเอง แต่พอถึงจุดหนึ่งก็อาจกลายเป็นปัญหาแบบเปียกที่ต้องใช้ direct liquid cooling (DLC)
ถ้านับแร็กเซิร์ฟเวอร์แบบหนาแน่นที่มี GPU ติดอยู่เป็นเครื่องเดียว เราก็มาถึงระดับหลายร้อยกิโลคอร์แล้ว
เคยเถียงกับคนที่ไม่เข้าใจแนวคิดว่าบริการขนาด Wikipedia สามารถรันบนเซิร์ฟเวอร์เครื่องเดียวได้ ซึ่งก็ค่อนข้างสนุก เพราะจริง ๆ มันเป็นเรื่องง่ายมาพักใหญ่แล้ว เพียงแต่ไม่ทำกันเพราะเหตุผลเชิงปฏิบัติอย่างความพร้อมใช้งานหรือความคุ้มค่าด้านต้นทุน
ตามสัญชาตญาณ ถ้ามีงานเพิ่มขึ้นที่แก้ปัญหาเดิมด้วยความเร็วครึ่งหนึ่ง ก็น่าจะมีต้นทุนด้านการใช้หน่วยความจำอยู่บ้าง เลยสงสัยว่ามีแอปที่พอเปิด SMT แล้วใช้หน่วยความจำเพิ่ม แต่ไม่ได้ความเร็วเพิ่มเลยอยู่บ่อยไหม
จากเบนช์มาร์กที่เผยแพร่ออกมาหลายตัว ดูเหมือนว่าแอปส่วนใหญ่ไม่ได้ประโยชน์ด้านความเร็วรันอย่างชัดเจนนัก
เป็นเรื่องน่าสนใจเสมอเมื่อเห็นงานวิจัยอายุหลายสิบปีซึ่งตอนออกมาแทบไม่มีใครสนใจ แต่พอฮาร์ดแวร์แรงพอแล้วกลับกลายเป็นเทคโนโลยีล้ำหน้าขึ้นมาทันที
ตัวอย่างเช่น Z-buffer ที่ใช้ในวิดีโอเกม 3D ตอนปรากฏในเปเปอร์ครั้งแรกมันแทบถูกพูดถึงเป็นเรื่องประกอบ เพราะไม่ใช่ว่าหัวข้อไม่ดี แต่เพราะต้องใช้หน่วยความจำมากเกินไป
หลายสิบปีต่อมา เมกะไบต์มีราคาถูกลงมากพอสมควร และท้ายที่สุด real-time 3D renderer ทุกตัวก็ใช้มัน
ดูเหมือนว่าจะมีช่องว่างในวรรณกรรมอยู่ราว 38 ปี จนกระทั่ง David MacKay ค้นพบมันใหม่
การใช้งานกระแสหลักครั้งแรกคือในปี 2003 และตอนนี้มันถูกใช้ใน WiFi, Ethernet และ 5G
[1] https://en.wikipedia.org/wiki/Low-density_parity-check_code
[2] https://scholar.google.com/scholar?q=%22low+density+parity+c...
ถ้าไปห้องสมุดแล้วอ่านงานที่นักวิจัยวิทยาการคอมพิวเตอร์เคยตีพิมพ์เป็นเปเปอร์กระดาษ อาจมีไอเดียที่ตอนนั้นยังทำไม่ได้ แต่ตอนนี้นำมาทำจริงได้แล้วก็ได้
ตลอด 20 ปีที่ผ่านมา ประสิทธิภาพ single-core ถูกกดไว้เพราะให้ความสำคัญกับการสเกลแนวนอนหรือเพิ่มจำนวนคอร์ก่อน ทำให้ความซับซ้อนและพื้นที่ไดของแต่ละคอร์กลายเป็นประเด็น ถ้าแนวโน้มนี้ไม่เกิดขึ้นและผู้ออกแบบ CPU ยังมุ่งที่ประสิทธิภาพ single-core เป็นหลัก เราอาจได้เห็นการอิมพลีเมนต์แบบนี้เร็วกว่านี้มาก
Z-buffer เป็นแนวคิดที่เรียบง่าย เลยอาจดูเหมือนประเด็นรองในเปเปอร์ ตัวอย่างที่ดีกว่าอาจเป็น ray tracing ซึ่งแม้ไม่มีพื้นฐานกราฟิก 3D ก็ยังเข้าใจแนวคิดได้ค่อนข้างชัด แต่จนไม่นานมานี้มันยังไม่สมจริงในแง่ประสิทธิภาพสำหรับการเรนเดอร์แบบเรียลไทม์
สิ่งที่น่าสนใจก็คือ เราหาแนวทางที่ง่ายกว่าเพื่อประมาณการเรนเดอร์ที่เหมือนจริงไม่ได้ จนสุดท้ายต้องย้อนกลับไปใช้วิธีแก้เก่า ๆ ที่ค่อนข้างตรงไปตรงมาและมีต้นทุนสูง
นักวิชาการจำนวนมากเคยมองว่าระบบชนิดแบบ substructural ตายไปแล้วในทางปฏิบัติ เพราะแพ้ให้กับ garbage collection แต่ Rust ชุบชีวิตมันขึ้นมาอีกครั้งด้วยการผสานเข้ากับไอเดียใหม่ ๆ ของ C++ ในยุคนั้น
ความต้องการแบนด์วิดท์หน่วยความจำที่เพิ่มขึ้นนี้เองที่ทำให้การอิมพลีเมนต์อย่างถูกต้องทำได้ยากและมีราคาแพง ระบบระดับสูงใช้ช่อง RAM เฉพาะทาง แต่ในฮาร์ดแวร์ราคาถูกกว่ากลับไปแย่งแบนด์วิดท์จากอินเทอร์เฟซหน่วยความจำแบบใช้ร่วมกันอย่างมาก
ตัวอย่างเช่น เกม N64 บางเกมปิด Z-buffer และไปปรับแต่งการวาดฉากหลัง/ฉากหน้าในซอฟต์แวร์แทน เพื่อหลีกเลี่ยงต้นทุนของการอ่านและอัปเดตข้อมูลความลึก
ตัวทำนายเชิงคาดเดา เคยเป็นเป้าหมายของการโจมตีหลายแบบเพื่อดึงข้อมูลลับออกมา
ถ้า ISA ทั่วไปจำนวนมากมีช่องโหว่ ก็สงสัยว่ามีมาตรการอะไรเพื่อลดผลกระทบจากการโจมตีแบบนี้หรือไม่
วิธีป้องกันเรื่องนี้ แทบไม่มีเลยนอกจากแนวทางระดับเดือดทั้งมหาสมุทร[0] การประมวลผลแบบคาดเดามีคุณค่าต่อประสิทธิภาพมากเกินไป จนคอมพิวเตอร์ที่ไม่มีมันแทบใช้งานจริงไม่ได้เลย ถ้าอยากได้โปรเซสเซอร์ที่ไม่มีการประมวลผลแบบคาดเดาจริง ๆ ก็ไปซื้อ Pentium รุ่นแรกแบบเก่ามาใช้ได้
วิธีบรรเทาที่ใช้งานได้จริงมีหลายแบบ แต่ขั้นต่ำที่สุดคือต้องรับประกัน การแยกโปรเซส ระหว่างโปรเซสเหยื่อที่มีความลับ กับผู้โจมตีที่เป็นไปได้ซึ่งสามารถมีอิทธิพลต่อการทำงานของเหยื่อได้
Intel เคยถูกจับได้ว่าปล่อยให้มีการประมวลผลแบบคาดเดาข้าม ring ได้ จึงทำให้สามารถอ่านหน่วยความจำของเคอร์เนลหรือไฮเปอร์ไวเซอร์จาก user space ได้ ถ้าเป็น CPU ที่ออกแบบมาไม่แย่นัก สิ่งที่มักต้องกังวลหลัก ๆ จะเป็น HTML iframe
แหล่งที่มาต่างกันไม่สามารถส่ง HTTP request ตามใจชอบหากันได้[1] แต่สามารถฝังอีกฝ่ายได้โดยไม่ต้องขออนุญาต[2] ตามธรรมเนียมแล้ว ข้อมูลนั้นจะถูกโหลดเข้าไปยังโปรเซสของผู้โจมตี และอาจรั่วไหลผ่านการโจมตีเชิงเวลาได้
วิธีแก้ปัญหาในยุคแรกของเว็บไม่ใช่การแยก iframe ออกเป็นคนละโปรเซส แต่คือการถอด shared-memory multithreading ออกไปทั้งระบบ ถ้าทำให้ผู้โจมตีไม่มีฐานเวลาได้ ก็จะสำคัญน้อยลงว่าเหยื่อจะประมวลผลอะไรแบบคาดเดา แต่การทำแบบนั้นต้องตัด multithreading ออก เพราะไม่เช่นนั้นเธรดหนึ่งสามารถเขียนข้อมูลที่รู้ค่าอยู่แล้วซ้ำ ๆ ในลูปเพื่อสร้างนาฬิกาได้
[0] https://hackaday.com/2013/08/02/the-mill-cpu-architecture/
[1] อย่างน้อยก็ทำไม่ได้ถ้า origin ปลายทางไม่ได้อนุญาตผ่าน CORS
[2] เช่น image hotlink หรือ iframe embed
ไม่ใช่ช่องโหว่ที่มีอยู่ในตัวการทำนายเอง
ในมุมของคนที่เพิ่งเริ่มศึกษา อ่านบทความแล้วก็ยังไม่ชัดเจนว่า ตัวทำนายสาขาแบบ 2-ahead คืออะไรกันแน่
ในยุค 90 มักจะมีอย่างใดอย่างหนึ่ง แต่โปรเซสเซอร์สมัยใหม่ดูเหมือนจะมีทั้งสองอย่าง
บทคัดย่อของ “Multiple-block ahead branch predictors” อธิบายว่าเป็นวิธีทำนายบล็อกถัดไปอีกหนึ่งบล็อก โดยไม่ใช้ข้อมูลของบล็อกคำสั่งปัจจุบันในการทำนายที่อยู่ของบล็อกคำสั่งถัดไป แบบนี้ช่วยลดคอขวดของการดึงคำสั่งในโปรเซสเซอร์ “brainiac” ที่มีการ dispatch กว้าง และทำให้ทำนายที่อยู่ของบล็อกคำสั่งได้สองบล็อกต่อหนึ่งไซเคิลอย่างมีประสิทธิภาพ
และสำหรับโปรเซสเซอร์ “speed demon” ก็สามารถทำให้กระบวนการทำนายสาขาเป็นแบบ pipeline ได้ เพื่อให้ได้ทั้งสัญญาณนาฬิกาที่สูงขึ้นหรือความแม่นยำที่ดีขึ้นจากโครงสร้างตัวทำนายที่ใหญ่กว่า ต่างจากวิธี multiple-predictor แบบเดิม ตัวทำนายสาขาแบบหลายบล็อกล่วงหน้าสามารถใช้วิธีทำนายสาขาแบบใดก็ได้
[0] https://dl.acm.org/doi/10.1145/237090.237169
เพิ่มเติมคือ ดูเหมือน eyegor จะโพสต์ลิงก์ไว้แล้ว แต่ตั้งใจจะบอกว่าอย่างน้อยควรอ่านบทคัดย่อ
มันน่าจะยากกว่าการทำนายสาขาถัดไปมาก แต่ก็ทำให้สามารถดึงโค้ดมาเลี้ยง pipeline ที่ลึกกว่าได้เร็วขึ้นมาก
ดังนั้นแทนที่จะประเมินล่วงหน้าแค่ n+1 แบบตัวทำนายสาขาทั่วไป ก็อาจประเมินผลไปได้ถึง n+2 ผมยังไม่ค่อยเข้าใจว่ามันทำงานแบบนี้ได้อย่างไรโดยไม่ทำให้ L1 cache ปั่นป่วน
ถ้าเป็นการมองล่วงหน้าเกิน n+1 จริง ก็น่าจะทำให้มีการขับข้อมูลออกจากแคชมากขึ้นมาก เลยคิดว่าน่าจะมีอะไรบางอย่างที่ผมยังพลาดไป
ว่ากันว่า Zen 5 สามารถมองไปได้ไกลกว่า second taken branch ใน instruction stream และด้วยเหตุนี้จึงมีหน้าต่างการทำนาย 3 ชุดที่เป็นประโยชน์ต่อการสร้างคำสั่งสำหรับถอดรหัส
งานวิจัยต้นฉบับเปิดให้อ่านได้สาธารณะ แต่ผมยังไม่ได้อ่านลึกมากนัก: https://dl.acm.org/doi/10.1145/237090.237169
แต่ละทิศทางของสาขาจะพาไปยังจุดเริ่มต้นของบล็อกคำสั่งใหม่ และคำสั่งสุดท้ายของบล็อกแบบนั้นก็มักเป็นอีกสาขาหนึ่ง
พูดอีกอย่างคือ ตัวทำนายสาขาคืออุปกรณ์ที่เดาที่อยู่ของบล็อกถัดไป ตัวทำนายสาขาแบบ 2-ahead ก็ทำสิ่งเดียวกัน แต่ทำกับสองบล็อกถัดไป
ตามถ้อยคำในงานวิจัย “ข้อมูลของบล็อกคำสั่งปัจจุบันถูกใช้เพื่อทำนายที่อยู่ของบล็อกที่อยู่ถัดจากบล็อกคำสั่งถัดไป”
มันทำได้โดยไม่ต้องรอให้คำสั่งของบล็อกถัดไปถูกถอดรหัสก่อน ต่างจากตัวทำนายสาขาทั่วไป จึงสามารถป้อนงานให้ตัวถอดรหัสคำสั่งหลายตัวพร้อมกันได้
สิ่งนี้มีประโยชน์เป็นพิเศษใน CPU สมัยใหม่ที่ตัวถอดรหัสคำสั่งกลายเป็นคอขวด เพราะมีตัวถอดรหัสเพียง 1 ตัวที่ถอดรหัสได้แค่ 1 คำสั่งต่อไซเคิล ย่อมตามไม่ทัน front-end แบบกว้างที่สามารถรันคำสั่งได้จำนวนมาก เช่น 4~6 คำสั่งต่อไซเคิล
อาจต้องมี branch hint มากขึ้น: https://github.com/ziglang/zig/issues/5177
น่าจะทำแบบมี cold, warm, warmer แล้วให้ hot เป็นค่าเริ่มต้นจึงละไว้ได้หรือไม่ บางครั้งอาจกำหนดให้ทุกสาขาเป็น cold ยกเว้นแค่กรณีเดียวก็ได้
อาจเป็นความคิดที่ไม่ดี แต่ก็อยากรู้เหตุผล
ถ้ามีการแตกแขนงแบบมีเงื่อนไข ทำไมถึงไม่ดึงและเตรียมคำสั่งของทั้งสองทางไว้ แล้วค่อยทิ้งฝั่งที่ผิดไป?
อยากรู้ว่านี่เป็นงานที่ยากกว่ามาก หรือมีเหตุผลอื่นที่ทำให้มันไม่คุ้มค่า
ตัวทำนายการแตกแขนงแบบ TAGE สมัยใหม่ทำนายถูกได้มากกว่า 99% อยู่แล้วมาก ดังนั้นคำสั่งส่วนเพิ่มของอีกฝั่งจึงแทบจะถูกทิ้งเสมอ
ที่แย่กว่านั้นคือฟรอนต์เอนด์ดึงคำสั่งล่วงหน้าไปไกลกว่าจุดที่แบ็กเอนด์จะยืนยันทิศทางจริงได้เป็นหลายสิบแขนง แล้วจะแก้กับแขนงถัดไปอย่างไร? จะถอดรหัสความเป็นไปได้ 4 ทาง แล้วต่อไป 8, 16, 32 ทางหรือ? สุดท้ายส่วนใหญ่ก็ต้องทิ้งอยู่ดี
ถ้ามีฮาร์ดแวร์ที่ดึงหลายสตรีมคำสั่งแบบขนานได้ อย่าง Intel Gracemont/Goldmont/Skymont และ AMD Zen 5 กลยุทธ์ที่ดีกว่าคือสมมติว่าตัวทำนายการแตกแขนงถูก 100% แล้วตามแขนงหนึ่งไป จากนั้นก็ตามแขนงถัดไปต่อ
Intel Skymont มีดีโคเดอร์ 3 ตัว และแต่ละตัวเป็นแบบ 3-wide จึงถอดรหัสเป้าหมายแขนงถัดไป 3 จุดพร้อมกัน Intel ถึงกับใส่แขนงปลอมเพื่อแยกบล็อกโค้ดขนาดใหญ่ ให้ดีโคเดอร์ทั้งสามถอดรหัสคนละส่วนของสตรีมคำสั่งที่กำลังจะมาถึงอยู่เสมอ จากนั้นจึงรวมสตรีมไมโครออปทั้ง 3 เข้าด้วยกัน เพื่อให้ Skymont รักษาแบนด์วิดท์การถอดรหัสที่มีผลจริงได้ 9 คำสั่งต่อไซเคิล
การทำทั้งสองแขนงช่วยลดดีเลย์ได้เล็กน้อยเฉพาะในกรณีหายากที่ทำนายพลาดเท่านั้น ขณะที่การตามการทำนายถัดไปอีกสองสามครั้งในทิศทางเดียวกันทำให้ Intel และ AMD ใช้ดีโคเดอร์หลายตัวทำงานขนานกันได้ Intel จึงสร้าง 9-wide ได้ด้วยดีโคเดอร์ 3-wide ที่เรียบง่ายกว่า 3 ตัว และ AMD ก็สร้าง 8-wide ได้ด้วยดีโคเดอร์ 4-wide ที่เรียบง่ายกว่า 2 ตัว
คอมไพเลอร์ รันไทม์ และ CPU มักเดาถูกว่าผลลัพธ์ไหนน่าจะเกิดขึ้นมากกว่า และโดยทั่วไปกลยุทธ์ที่ดีกว่าคือไม่ทำงานเพิ่มตั้งแต่แรก ดีกว่าเอาซิลิคอนและความร้อนไปเผื่อไว้กับคำตอบที่ผิดในกรณีที่อาจเดาผิด
ดูเหมือนหลายคนยังไม่มีสัญชาตญาณว่าการทำนายการแตกแขนงแม่นยำได้ขนาดไหน แค่ดูโค้ดของตัวเองก็มักจะพอเห็นได้ทันทีว่า “โฟลว์ควบคุมส่วนใหญ่ไปทางนี้ และแขนงนี้มีไว้จัดการกรณียกเว้น”
คอมไพเลอร์ยุคนี้ก็อนุมานเรื่องนี้ได้ดีพอสมควร และ CPU/JIT/รันไทม์ ก็สร้างฮิวริสติกที่น่าทึ่งได้เช่นกัน ถึงอย่างนั้นถ้ายังพลาด ก็ยังสามารถใส่ hint ไว้ในโค้ดเพื่อบอกทิศทางที่คาดหวังให้คอมไพเลอร์หรือส่วนอื่น ๆ ทราบได้
reorder buffer ของคำสั่งใน CPU สมัยใหม่ลึกได้ถึงหลายร้อยคำสั่ง และถ้าในนั้นมี conditional jump แค่ 8 จุด เส้นทางที่โปรแกรมอาจไปได้ก็มี 256 ทางแล้ว
ถ้าตัวทำนายการแตกแขนงมีโอกาสทายถูกทั้ง 8 จุดเกิน 50% ซึ่งในความเป็นจริงก็เกิน การทำงานเผื่อแบบ คูณ 256 ก็ไม่คุ้ม
มันต้องใช้ซิลิคอนเพิ่มเพื่อเก็บสถานะจุลสถาปัตยกรรมให้มากขึ้น และต้องมี execution unit มากขึ้นเพื่อใช้เทคนิคนี้ให้เต็มที่ แต่สำหรับ CPU แบบ superscalar สิ่งเหล่านี้ก็จำเป็นอยู่แล้วเพื่อดึง instruction-level parallelism ออกจากโค้ดที่มีแขนงน้อย
ที่เหลือก็เป็นงานน่าปวดหัวในการจัดการเรื่องซับซ้อนอย่าง aliasing และ interrupt แต่พวกวิศวกรฮาร์ดแวร์ก็เหมือนพ่อมดที่ทำเรื่องพวกนี้สำเร็จได้
อย่างไรก็ตาม speculative execution เปิดช่องให้มีการใช้ประโยชน์จาก cache timing side channel เพื่อดึงข้อมูลจากข้อมูลที่โค้ดซึ่งถูก execute แบบ speculative เท่านั้น แต่ผลข้างเคียงเชิงสถาปัตยกรรมยังไม่ถูก commit เคยไปแตะต้องไว้ กล่าวคือ ข้อมูลอาจรั่วออกมาจากโค้ดที่ไม่ได้ถูกรัน “จริง ๆ” ก็ได้
ซึ่งรวมถึงโค้ดที่ไม่ได้ถูกรันอย่างชัดเจนเพราะติดการตรวจสอบเงื่อนไข เช่น การตรวจสอบสิทธิ์
ตัวอย่างการโจมตีที่คุ้นกันคือ Spectre: https://en.m.wikipedia.org/wiki/Spectre_(security_vulnerabil...
ดังนั้นโดยพื้นฐานแล้วมันไม่คุ้มเลย ทรัพยากรบนชิปพวกนั้นเอาไปใช้กับเธรดหรือคอร์อื่นจะดีกว่ามาก
ถ้าจะตัดสินว่านี่เป็นไอเดียที่ดีไหม ผมอยากเห็น ข้อมูลประสิทธิภาพ ก่อน ไม่มีแม้แต่ข้อมูลเรื่องโทษจากการทำนายการแตกแขนงพลาดของแนวทางนี้ด้วย
ไม่ว่าอย่างไร สัญชาตญาณของแนวทางนี้ดูเหมือนจะอยู่ที่การดึงและถอดรหัสคำสั่งอย่างก้าวร้าว สำหรับคำสั่งที่อาจยังไม่มีอยู่ใน L1 instruction cache หรือ micro-op cache
เรื่องนี้สำคัญกับ x86 และน่าจะรวมถึง RISC-V ด้วย เพราะทั้งคู่มีความยาวคำสั่งแปรผัน แค่ดูบล็อกใน instruction cache อย่างเดียว คอร์จะไม่รู้ว่าควรถอดรหัสคำสั่งภายในบล็อกนั้นอย่างไร สำหรับ ISA ทั้งสองแบบ การจะเริ่มถอดรหัสบล็อกจาก instruction cache ได้ ต้องรู้ PC ของคำสั่งอย่างน้อยหนึ่งคำสั่งก่อน
ดังนั้นถ้ารู้ได้ว่าแอปพลิเคชันอาจกระโดดไปที่ไหนอีกสองบล็อกข้างหน้า ก็จะช่วยให้ดึงและถอดรหัสล่วงหน้าไปได้ไกลกว่าวิธีปัจจุบัน
แนวทางนี้คล้าย instruction prefetching แต่ instruction prefetching ไม่ได้ให้ข้อมูลจุดเริ่มต้นแก่คอร์
คอร์ ARM ประสิทธิภาพสูงอาจไม่เจอปัญหา “หาจุดเริ่มต้น” นี้ เพราะทุกคำสั่งยาว 32 บิต ดังนั้นจึงอาจทำขั้นตอนถอดรหัสแบบขนานได้แม้ไม่รู้จุดเริ่มต้น
วิธีนี้น่าจะเป็นประโยชน์กับแอปพลิเคชันที่ติดข้อจำกัดฝั่งฟรอนต์เอนด์ เช่น cloud workload ที่บล็อกโค้ดร้อนกระจายอยู่ทั่วไบนารี ผมก็สงสัยเหมือนกันว่าสำหรับแอปพลิเคชันประเภทอื่นจะได้ประโยชน์ด้านประสิทธิภาพหรือกลับเสียประสิทธิภาพ
ผมก็ยังไม่เข้าใจเลยว่า ตัวทำนายการแตกแขนงแบบ 2-ahead คืออะไร
โดยทั่วไปงานวิจัยเก่ามักสมมติว่าผู้อ่านรู้เรื่องหัวข้อพวกนี้น้อยกว่ามาก เพราะในสมัยนั้นความรู้แบบนี้ยังเป็นเรื่องเฉพาะทางกว่านี้มาก
ตอนนี้สิ่งที่จำเป็นคือ แบนด์วิดท์หน่วยความจำ ช่องหน่วยความจำ 2 ช่องของซ็อกเก็ต AM5 สำหรับผู้บริโภคดูน้อยนิดเมื่อเทียบกับสมรรถนะการประมวลผลระดับนี้ โดยเฉพาะเมื่อเทียบกับ Apple Silicon รุ่นพื้นฐานด้วยซ้ำ
ผมย้ายจากระบบ Zen แบบจัดเต็มไปใช้ M2 Max แล้วก็ประหลาดใจอีกครั้งว่าการเพิ่มขึ้นของแบนด์วิดท์หน่วยความจำช่วยเร่งงานข้อมูลเข้มข้นได้มากแค่ไหน แม้ในงานมัลติทาสก์ที่ค่อนข้างหนัก ท่อส่งหน่วยความจำอันคับแคบของระบบ Zen ก็มักจะตันอยู่บ่อยครั้ง
เหตุผลที่ผู้คนหันไปมอง Apple Silicon โดยเฉพาะก็เพราะ LLM แม้ LLM จะเหมาะกับ GPU มากกว่า แต่ก็ต้องการ VRAM มาก และ NVIDIA ก็ตั้งราคา GPU ที่มี VRAM เยอะไว้สูงเกินเหตุ
ถ้า AMD อยากสกัดกระแสของ NVIDIA จริง ๆ ก็ควรขาย GPU สำหรับผู้บริโภคที่มี VRAM 64~128GB