ทำไมคอมไพเลอร์ไม่รวมทุกอย่างเข้าไว้ด้วยกัน? [ปิด]


13

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

ดังนั้นคำถามของฉันคือทำไมไม่คอมไพเลอร์แบบอินไลน์ทุกอย่าง? ฉันคิดว่ามันจะทำให้การปฏิบัติการเร็วขึ้นอย่างเห็นได้ชัด

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

มีเหตุผลอื่นใดที่คอมไพเลอร์ไม่เพียงแค่อินไลน์การเรียกใช้ฟังก์ชันทั้งหมดหรือไม่


18
IDK เกี่ยวกับคุณ แต่ฉันไม่ได้มีหน่วยความจำหลายร้อย GB เพียงแค่โกหก
Ampt

2
Isn't the improved performance worth it?สำหรับวิธีการที่จะใช้วนรอบ 100 ครั้งและกระทืบตัวเลขร้ายแรงบางอย่างค่าใช้จ่ายในการย้ายอาร์กิวเมนต์ 2 หรือ 3 ไปยังการลงทะเบียน CPU คืออะไร
Doval

5
คุณเป็นคนสามัญมากเกินไป "คอมไพเลอร์" หมายถึง "คอมไพเลอร์ทั้งหมด" และ "ทุกอย่าง" หมายถึง "ทุกอย่าง" จริงหรือ? จากนั้นคำตอบนั้นง่ายมีสถานการณ์ที่คุณไม่สามารถอินไลน์ได้ การเรียกซ้ำเกิดขึ้นในใจ
OtávioDécio

17
ตำแหน่งแคชเป็นวิธีที่สำคัญกว่าการเรียกใช้ฟังก์ชันขนาดเล็ก
SK-logic

3
การปรับปรุงประสิทธิภาพมีความสำคัญมากในทุกวันนี้ด้วยพลังการประมวลผล GFLOPS นับร้อยหรือไม่
mouviciel

คำตอบ:


22

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

สำหรับคำถามของคุณ: มีบางสิ่งที่ยากหรือเป็นไปไม่ได้ที่จะอินไลน์:

  • ไลบรารีที่ลิงก์แบบไดนามิก

  • ฟังก์ชั่นที่กำหนดแบบไดนามิก (การจัดส่งแบบไดนามิกเรียกผ่านตัวชี้ฟังก์ชั่น)

  • ฟังก์ชั่นแบบเรียกซ้ำ (recursion แบบหางสามารถทำได้)

  • ฟังก์ชั่นที่คุณไม่มีรหัส (แต่การเพิ่มประสิทธิภาพเวลาลิงค์อนุญาตให้บางส่วนของพวกเขา)

จากนั้นการทำอินไลน์จะไม่เพียงมีผลประโยชน์:

  • ปฏิบัติการที่ใหญ่กว่าหมายถึงพื้นที่ดิสก์มากขึ้นและใช้เวลาโหลดมากขึ้น

  • ปฏิบัติการที่ใหญ่กว่าหมายถึงการเพิ่มความดันแคช (โปรดทราบว่าการใส่ฟังก์ชั่นเล็ก ๆ น้อย ๆ อย่างเช่น getters ง่าย ๆ อาจลดขนาดไฟล์เรียกทำงานและความดันแคช)

และในที่สุดสำหรับฟังก์ชั่นที่ใช้เวลาไม่ยุ่งยากในการทำงานการได้รับนั้นไม่คุ้มกับความเจ็บปวด


3
การเรียกแบบเรียกซ้ำบางครั้งสามารถ inlined (การเรียกแบบหาง), แต่ทุกอย่างสามารถเปลี่ยนเป็นการทำซ้ำได้ถ้าคุณเลือกที่จะเพิ่ม stack ที่ชัดเจน
ratchet freak

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

11

ข้อ จำกัด ที่สำคัญคือ polymorphism แบบรันไทม์ หากมีการจัดส่งแบบไดนามิกที่เกิดขึ้นเมื่อคุณเขียนfoo.bar()ก็เป็นไปไม่ได้ที่จะโทรแบบอินไลน์ สิ่งนี้อธิบายว่าทำไมคอมไพเลอร์ไม่รวมทุกอย่างไว้

การโทรแบบเรียกซ้ำไม่สามารถ inlined ได้ง่ายเช่นกัน

การข้ามอินไลน์ของโมดูลนั้นทำได้ยากด้วยเหตุผลทางเทคนิค

อย่างไรก็ตามคอมไพเลอร์ทำสิ่งต่างๆมากมาย


3
การฝังผ่านการจัดส่งเสมือนเป็นเรื่องยากมาก แต่ก็เป็นไปไม่ได้ คอมไพเลอร์ C ++ บางตัวสามารถทำได้ภายใต้สถานการณ์บางอย่าง
bstamour

2
... รวมถึงคอมไพเลอร์ JIT บางตัว (devirtualization)
แฟรงค์

@bstamour คอมไพเลอร์ครึ่งภาษาใด ๆ ที่มีการเพิ่มประสิทธิภาพที่เหมาะสมจะส่งแบบคงที่เช่น devirtualise การเรียกไปยังเมธอดเสมือนประกาศบนวัตถุที่มีชนิดแบบไดนามิกที่สามารถรู้ได้ในเวลารวบรวม สิ่งนี้สามารถอำนวยความสะดวกในการทำอินไลน์หากเกิดขั้นตอน แต่นี่เป็นเรื่องเล็กน้อย มีอะไรอีกที่คุณหมายถึง? ฉันไม่เห็นว่าการ "Inline In the Virtual Dispatch" สามารถทำได้จริง หากต้องการแบบอินไลน์หนึ่งต้องทราบประเภทคงที่ - devirtualise คือ - เพื่อการดำรงอยู่ของวิธี inlining มีคือไม่มีการจัดส่งเสมือน
underscore_d

9

ขั้นแรกคุณไม่สามารถอินไลน์ได้เสมอเช่นฟังก์ชั่นแบบเรียกซ้ำอาจไม่สามารถ inlinable ได้เสมอ (แต่โปรแกรมที่มีคำจำกัดความแบบเรียกซ้ำfactด้วยการพิมพ์เพียงอย่างเดียวfact(8)อาจถูกแทรกไว้)

จากนั้นอินไลน์จะไม่เป็นประโยชน์เสมอไป หากคอมไพเลอร์อินไลน์มากจนรหัสผลลัพธ์ใหญ่พอที่จะมีส่วนที่ร้อนไม่เหมาะเช่นแคชคำสั่ง L1 มันอาจช้ากว่าเวอร์ชั่นที่ไม่มีการอินไลน์มาก (ซึ่งจะพอดีกับแคช L1) ... ตัวประมวลผลล่าสุดนั้นเร็วมากในการดำเนินการCALLคำสั่งเครื่อง (อย่างน้อยก็ไปยังตำแหน่งที่รู้จักเช่นการโทรโดยตรงไม่ใช่ตัวชี้การโทรผ่าน)

ในที่สุดการแทรกแบบเต็มต้องการการวิเคราะห์โปรแกรมทั้งหมด สิ่งนี้อาจเป็นไปไม่ได้ (หรือแพงเกินไป) ด้วย C หรือ C ++ ที่คอมไพล์โดยGCC (และกับClang / LLVM ) คุณจำเป็นต้องเปิดใช้งานการเพิ่มประสิทธิภาพเวลาเชื่อมโยง (โดยการรวบรวมและเชื่อมโยงด้วยเช่นg++ -flto -O2) และใช้เวลารวบรวมค่อนข้างมาก


1
สำหรับบันทึกที่ LLVM / เสียงดังกราว (และคอมไพเลอร์อื่น ๆ หลาย ๆ คน) นอกจากนี้ยังสนับสนุนการเพิ่มประสิทธิภาพการเชื่อมโยงเวลา
คุณ

ฉันรู้แล้ว; LTO มีอยู่ในศตวรรษที่แล้ว (IIRC ในคอมไพเลอร์กรรมสิทธิ์ MIPS บางอย่างเป็นอย่างน้อย)
Basile Starynkevitch

7

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

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


ดูเหมือนว่าจะทำซ้ำคะแนนและอธิบายในคำตอบก่อนหน้าซึ่งโพสต์เมื่อชั่วโมงที่แล้ว
gnat

1
แคชอะไร L1? L2? L3? อันไหนสำคัญกว่ากัน?
Peter Mortensen

1

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

อีกเหตุผลหนึ่งอาจเป็นคอมไพเลอร์ของ JIT หากทุกอย่างเป็นแบบอินไลน์แล้วจะไม่มีจุดร้อนที่จะรวบรวมแบบไดนามิก


1

หนึ่งมีตัวอย่างง่ายๆที่การเรียงทุกอย่างเข้าด้วยกันจะออกมาแย่มาก พิจารณารหัส C ง่าย ๆ นี้:

void f1 (void) { printf ("Hello, world\n"); }
void f2 (void) { f1 (); f1 (); f1 (); f1 (); }
void f3 (void) { f2 (); f2 (); f2 (); f2 (); }
...
void f99 (void) { f98 (); f98 (); f98 (); f98 (); }

คาดเดาสิ่งที่การเรียงทุกอย่างจะทำเพื่อคุณ

ถัดไปคุณตั้งสมมติฐานว่าการอินไลน์จะทำให้สิ่งต่าง ๆ เร็วขึ้น เป็นกรณีที่บางครั้ง แต่ไม่เสมอไป เหตุผลหนึ่งคือรหัสที่เหมาะกับแคชคำสั่งทำงานเร็วขึ้นมาก ถ้าฉันเรียกฟังก์ชันจากสถานที่ 10 แห่งฉันจะเรียกใช้รหัสที่อยู่ในแคชคำสั่งเสมอ หากมีการ inline แล้วสำเนาทั้งหมดทั่วสถานที่และทำงานช้าลงมาก

มีปัญหาอื่น ๆ : Inlining สร้างฟังก์ชั่นมากมาย ฟังก์ชั่นขนาดใหญ่นั้นยากที่จะปรับให้เหมาะสม ฉันได้รับรหัสสำคัญในการปฏิบัติงานเพิ่มขึ้นอย่างมากโดยการซ่อนฟังก์ชั่นเป็นไฟล์แยกเพื่อป้องกันไม่ให้คอมไพเลอร์ฝังไว้ในไฟล์ เป็นผลให้รหัสที่สร้างขึ้นสำหรับฟังก์ชั่นเหล่านี้ดีขึ้นมากเมื่อพวกเขาถูกซ่อนไว้

BTW ฉันไม่มี "หน่วยความจำหลายร้อย GB" คอมพิวเตอร์ที่ใช้งานของฉันไม่มีแม้กระทั่ง "พื้นที่ฮาร์ดไดรฟ์หลายร้อย GB" และหากแอปพลิเคชันของฉันมี "หน่วยความจำหลายร้อย GB" ก็จะใช้เวลา 20 นาทีในการโหลดแอปพลิเคชันไปยังหน่วยความจำ

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