ทำไมคอมไพเลอร์ยืนยันที่จะใช้การลงทะเบียนที่บันทึกไว้ที่นี่?


10

พิจารณารหัส C นี้:

void foo(void);

long bar(long x) {
    foo();
    return x;
}

เมื่อฉันรวบรวมใน GCC 9.3 ด้วย-O3หรือ-Osฉันได้รับ:

bar:
        push    r12
        mov     r12, rdi
        call    foo
        mov     rax, r12
        pop     r12
        ret

เอาท์พุทจากเสียงดังกราวเหมือนกันยกเว้นการเลือกrbxแทนr12การลงทะเบียน callee- บันทึก

อย่างไรก็ตามฉันต้องการ / คาดหวังว่าจะเห็นชุดประกอบที่มีลักษณะดังนี้:

bar:
        push    rdi
        call    foo
        pop     rax
        ret

ในภาษาอังกฤษนี่คือสิ่งที่ฉันเห็นว่าเกิดขึ้น:

  • ผลักดันค่าเก่าของการลงทะเบียนที่บันทึกไว้แบบสลี
  • ย้ายxไปยังการลงทะเบียนที่ถูกบันทึกด้วย callee
  • โทร foo
  • ย้ายxจากการลงทะเบียนที่บันทึก callee มาไว้ในการลงทะเบียนค่าตอบแทน
  • วางสแต็กเพื่อเรียกคืนค่าเก่าของการลงทะเบียนที่บันทึกโดยผู้ใช้

ทำไมต้องยุ่งกับการลงทะเบียนที่บันทึกที่ callee เลย? ทำไมไม่ทำเช่นนี้แทน? ดูเหมือนว่าสั้นกว่าง่ายกว่าและเร็วกว่า:

  • กดxไปที่กองซ้อน
  • โทร foo
  • ป๊อปxจากสแต็กลงในการลงทะเบียนค่าตอบแทน

การชุมนุมของฉันผิดหรือเปล่า? มันมีประสิทธิภาพน้อยกว่า messing กับการลงทะเบียนพิเศษหรือไม่? หากคำตอบของทั้งคู่เป็น "ไม่" ทำไม GCC หรือเสียงดังกราวไม่ทำอย่างนี้

การเชื่อมโยง Godbolt


แก้ไข: นี่เป็นตัวอย่างเล็กน้อยที่แสดงให้เห็นว่ามันเกิดขึ้นแม้ว่าตัวแปรจะถูกใช้อย่างมีความหมาย:

long foo(long);

long bar(long x) {
    return foo(x * x) - x;
}

ฉันได้รับสิ่งนี้:

bar:
        push    rbx
        mov     rbx, rdi
        imul    rdi, rdi
        call    foo
        sub     rax, rbx
        pop     rbx
        ret

ฉันอยากได้สิ่งนี้:

bar:
        push    rdi
        imul    rdi, rdi
        call    foo
        pop     rdi
        sub     rax, rdi
        ret

ครั้งนี้เป็นเพียงคำสั่งปิดเทียบกับสอง แต่แนวคิดหลักเหมือนกัน

การเชื่อมโยง Godbolt


4
การเพิ่มประสิทธิภาพที่ไม่ได้รับที่น่าสนใจ
fuz

1
เป็นไปได้มากที่สุดคือการสันนิษฐานว่าจะใช้พารามิเตอร์ที่ส่งผ่านดังนั้นคุณต้องการบันทึกการลงทะเบียนแบบระเหยและเก็บพารามิเตอร์ที่ส่งผ่านไว้ในรีจิสเตอร์ที่ไม่ได้อยู่ในสแต็กเนื่องจากการเข้าถึงพารามิเตอร์นั้นเร็วกว่าการลงทะเบียน ผ่าน x ไปที่ foo แล้วคุณจะเห็นสิ่งนี้ ดังนั้นจึงเป็นเพียงส่วนทั่วไปของการตั้งค่าเฟรมสแต็กของพวกเขา
old_timer

ให้ฉันเห็นว่าไม่มี foo มันไม่ได้ใช้ stack ดังนั้นใช่มันเป็นการเพิ่มประสิทธิภาพที่ไม่ได้รับ แต่สิ่งที่ใครบางคนจะต้องเพิ่มการวิเคราะห์ฟังก์ชั่นและถ้าค่าไม่ได้ใช้และไม่มีความขัดแย้งกับการลงทะเบียนนั้น คือ).
old_timer

แบ็กเอนด์แขนทำเช่นนี้กับ gcc จึงไม่ใช่แบ็กเอนด์
old_timer

เสียงดังกราว 10 เรื่องเดียวกัน (แบ็กเอนด์แขน)
old_timer

คำตอบ:


5

TL: DR:

  • ตัวรวบรวมคอมไพเลอร์อาจไม่ได้ตั้งค่าให้ค้นหาการเพิ่มประสิทธิภาพนี้ได้อย่างง่ายดายและอาจมีประโยชน์เฉพาะกับฟังก์ชั่นเล็ก ๆ เท่านั้นไม่ใช่ฟังก์ชั่นขนาดใหญ่ระหว่างการโทร
  • การสร้างฟังก์ชั่นขนาดใหญ่เป็นการแก้ปัญหาที่ดีกว่าโดยส่วนใหญ่
  • อาจมีความหน่วงแฝงเทียบกับปริมาณงานหากfooไม่ให้บันทึก / กู้คืน RBX

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

ผมรายงานนี้เป็นGCC ข้อผิดพลาด 69986 - รหัสขนาดเล็กลงไปได้ด้วย -Os โดยใช้ผลัก / pop สาด / โหลดกลับมาในปี 2016 ; ไม่มีกิจกรรมหรือคำตอบจาก GCC devs : /

มีความเกี่ยวข้องเล็กน้อย: ข้อผิดพลาดของ GCC 70408 - การนำทะเบียนที่สงวนไว้เดิมมาใช้ซ้ำจะให้รหัสขนาดเล็กลงในบางกรณี - ผู้รวบรวมคอมไพเลอร์บอกฉันว่าจะต้องใช้เวลาจำนวนมากในการทำงานเพื่อให้ GCC สามารถเพิ่มประสิทธิภาพได้ จากการfoo(int)โทรสองครั้งโดยอิงจากสิ่งที่จะทำให้เป้าหมาย asm ง่ายขึ้น


หาก fooไม่บันทึก / กู้คืนrbxตัวเองจะมีการแลกเปลี่ยนระหว่างปริมาณงาน (จำนวนคำสั่ง) เทียบกับร้านค้า / เวลาแฝงที่โหลดเพิ่มเติมในx-> ห่วงโซ่การพึ่งพา retval

คอมไพเลอร์มักจะให้ความสำคัญกับความล่าช้าในการรับส่งข้อมูลเช่นการใช้ 2x LEA แทนimul reg, reg, 10(ความล่าช้า 3 รอบ, 1 / นาฬิกาทรูพุต) เนื่องจากค่าเฉลี่ยของโค้ดส่วนใหญ่น้อยกว่า 4 uops / นาฬิกาในท่อ 4-wide ทั่วไปเช่น Skylake (คำแนะนำเพิ่มเติม / uops ใช้พื้นที่มากขึ้นใน ROB ลดลงไปข้างหน้าไกลออกไปข้างนอกหน้าต่างเดียวกันของการสั่งซื้อสามารถดูแม้ว่าและการดำเนินการเป็นจริงระเบิดด้วยแผงลอยอาจบัญชีสำหรับบาง uops น้อยกว่า 4 / ค่าเฉลี่ยของนาฬิกา)

ถ้าfooทำ push / pop RBX แสดงว่ามีจำนวน latency ไม่มากนัก การมีการกู้คืนเกิดขึ้นก่อนretแทนที่จะเป็นเพียงหลังจากนั้นอาจไม่เกี่ยวข้องเว้นแต่จะมีการคาดretคะเนผิดพลาดหรือ I-cache ที่ทำให้เกิดความล่าช้าในการดึงรหัสในที่อยู่ผู้ส่ง

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


ดังนั้นใช่push rdi/ pop raxจะมีประสิทธิภาพมากขึ้นในการนี้กรณีและนี่น่าจะเป็นการเพิ่มประสิทธิภาพพลาดสำหรับฟังก์ชั่นที่ไม่ใช่ใบเล็ก ๆ ขึ้นอยู่กับสิ่งfooที่ไม่และความสมดุลระหว่างความล่าช้า / ร้านโหลดพิเศษสำหรับxเทียบกับคำแนะนำเพิ่มเติมในการบันทึก / rbxเรียกคืนโทร

เป็นไปได้สำหรับเมตาดาต้าสแต็กเพื่อแสดงการเปลี่ยนแปลงของ RSP ที่นี่เช่นเดียวกับที่เคยใช้sub rsp, 8ในการรั่วไหล / โหลดซ้ำxลงในสแต็กสล็อต ( แต่คอมไพเลอร์ไม่ทราบว่าการเพิ่มประสิทธิภาพนี้อย่างใดอย่างหนึ่งของการใช้pushพื้นที่สำรองและเริ่มต้นตัวแปร. สิ่งที่ C / C ++ คอมไพเลอร์สามารถใช้คำแนะนำการผลักดันป๊อปสำหรับการสร้างตัวแปรท้องถิ่นแทนเพียงเพิ่มขึ้น ESP ครั้ง? . และการทำที่นานกว่า หนึ่ง var ท้องถิ่นจะนำไปสู่.eh_frameสแต็คขนาดใหญ่คลายเมตาดาต้าเนื่องจากคุณกำลังย้ายตัวชี้สแต็กแยกต่างหากกับการกดแต่ละครั้งซึ่งไม่ได้หยุดคอมไพเลอร์จากการใช้ push / pop เพื่อบันทึก / เรียกคืน regs ที่สงวนไว้สำหรับการโทร)


IDK ถ้ามันคุ้มค่าที่จะสอนคอมไพเลอร์ให้มองหาการปรับให้เหมาะสมนี้

อาจเป็นความคิดที่ดีเกี่ยวกับฟังก์ชั่นทั้งหมดไม่ใช่การโทรหนึ่งครั้งภายในฟังก์ชั่น และอย่างที่ฉันพูดมันขึ้นอยู่กับสมมติฐานในแง่ร้ายที่fooจะบันทึก / กู้คืน RBX ต่อไป (หรือการปรับให้เหมาะสมสำหรับปริมาณงานถ้าคุณรู้ว่าเวลาแฝงจาก x ถึงคืนค่านั้นไม่สำคัญ แต่คอมไพเลอร์ไม่ทราบและมักจะปรับให้เหมาะกับเวลาแฝง)

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

คุณไม่ต้องการให้การบันทึก / กู้คืน push / pop พิเศษในลูปเพียงบันทึก / กู้คืน RBX ภายนอกลูปและใช้รีจิสเตอร์ที่สงวนการโทรไว้ในลูปที่ทำการเรียกฟังก์ชัน แม้ว่าจะไม่มีลูปก็ตามในกรณีทั่วไปฟังก์ชั่นส่วนใหญ่จะทำการโทรหลายฟังก์ชั่น แนวคิดการปรับให้เหมาะสมนี้สามารถนำไปใช้ได้หากคุณไม่ได้ใช้จริงxระหว่างการโทรใด ๆ ก่อนหน้าแรกและหลังสุดท้ายมิฉะนั้นคุณมีปัญหาในการคงการจัดแนวสแต็ก 16 ไบต์สำหรับแต่ละรายการcallหากคุณทำหนึ่งป๊อปหลังจาก โทรก่อนโทรอีกครั้ง

คอมไพเลอร์ไม่เก่งในฟังก์ชั่นเล็ก ๆ โดยทั่วไป แต่มันก็ไม่ได้ยอดเยี่ยมสำหรับซีพียูเช่นกัน การเรียกใช้ฟังก์ชั่นที่ไม่ใช่แบบอินไลน์มีผลกระทบต่อการปรับให้เหมาะสมที่สุดเท่าที่จะทำได้เว้นแต่ว่าคอมไพเลอร์สามารถมองเห็นภายในของผู้ถูกเจาะและทำการตั้งสมมติฐานมากกว่าปกติ การเรียกใช้ฟังก์ชั่นที่ไม่ใช่แบบอินไลน์เป็นอุปสรรคหน่วยความจำโดยนัย: ผู้เรียกต้องคิดว่าฟังก์ชั่นอาจอ่านหรือเขียนข้อมูลใด ๆ ที่สามารถเข้าถึงได้ทั่วโลกดังนั้น vars ดังกล่าวทั้งหมดต้องซิงค์กับเครื่องนามธรรม C (การวิเคราะห์การหลบหนีช่วยให้คนในท้องถิ่นสามารถลงทะเบียนผ่านการโทรได้หากที่อยู่ของพวกเขาไม่ได้หลบเลี่ยงการทำงาน) นอกจากนี้คอมไพเลอร์จะต้องสมมติว่าการลงทะเบียนการโทรที่ถูกปิดบังนั้นถูกปิดกั้นทั้งหมด สิ่งนี้ดูดสำหรับทศนิยมใน x86-64 System V ซึ่งไม่มีการลงทะเบียน XMM ที่สงวนการโทรไว้

ฟังก์ชั่นเล็ก ๆ อย่างbar()นั้นดีกว่า inline ในโทรของพวกเขา คอมไพล์ด้วย-fltoดังนั้นสิ่งนี้สามารถเกิดขึ้นได้ข้ามขอบเขตไฟล์ในกรณีส่วนใหญ่ (พอยน์เตอร์ของฟังก์ชันและขอบเขตไลบรารีที่แชร์สามารถเอาชนะสิ่งนี้ได้)


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

เช่นมันจะเป็นงานที่ต้องใช้จำนวนมากและมีโค้ดจำนวนมากที่ต้องดูแลและถ้ามันกระตือรือร้นที่จะทำสิ่งนี้มันอาจทำให้โค้ดแย่ลง

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


ไม่แน่ใจว่ามีความรู้สึกถามว่าทำไมคอมไพเลอร์แปลรหัสด้วยวิธีนี้เมื่อใดจึงควรใช้ดีกว่า .. หากไม่ใช่ข้อผิดพลาดในการแปล เช่นเป็นไปได้ถามว่าทำไมเสียงดังกราวแปลก ๆ (ไม่ปรับให้เหมาะสม) ทำให้เกิดลูปนี้เปรียบเทียบกับ gcc, icc และแม้แต่ msvc
RbMm

1
@RbMm: ฉันไม่เข้าใจประเด็นของคุณ ดูเหมือนว่าการเพิ่มประสิทธิภาพที่ไม่ได้รับทั้งหมดสำหรับเสียงดังกราวซึ่งไม่เกี่ยวข้องกับคำถามนี้ มีข้อผิดพลาดในการเพิ่มประสิทธิภาพที่พลาดอยู่และในกรณีส่วนใหญ่ควรได้รับการแก้ไข ไปข้างหน้าและรายงานเกี่ยวกับbugs.llvm.org
Peter Cordes

ใช่ตัวอย่างรหัสของฉันไม่เกี่ยวข้องกับคำถามต้นฉบับแน่นอน เป็นอีกตัวอย่างหนึ่งของการแปลที่แปลก (สำหรับฉัน) แต่ผลลัพธ์รหัส asm ถูกต้องอยู่แล้ว ไม่ดีที่สุดเท่านั้นและไม่ได้ใช้ภาษาท้องถิ่นเปรียบเทียบ gcc / icc / msvc
RbMm
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.