ค่าใช้จ่ายในการเรียกใช้ฟังก์ชันเมื่อใดยังคงมีความสำคัญในคอมไพเลอร์สมัยใหม่?


95

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

โปรดจำไว้ว่าค่าใช้จ่ายของการเรียกใช้วิธีการอาจมีความสำคัญขึ้นอยู่กับภาษา มักจะมีข้อเสียระหว่างการเขียนรหัสที่สามารถอ่านได้และการเขียนรหัสนักแสดง

คำกล่าวที่ยกมานี้ยังคงมีผลบังคับใช้ในทุกวันนี้เนื่องจากอุตสาหกรรมที่อุดมไปด้วยคอมไพเลอร์สมัยใหม่ของนักแสดง

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


11
เขียนรหัสที่สามารถอ่านได้และบำรุงรักษาได้ เฉพาะเมื่อคุณประสบปัญหากับการล้นสแต็คคุณสามารถคิด spproach ของคุณอีกครั้ง
Fabio

33
คำตอบทั่วไปที่นี่เป็นไปไม่ได้ มีคอมไพเลอร์ต่าง ๆ มากเกินไปมีการใช้ข้อกำหนดภาษาที่แตกต่างกันมากเกินไป แล้วก็มีภาษาที่คอมไพล์ด้วย JIT ภาษาที่แปลแบบไดนามิกและอื่น ๆ แต่ถ้าคุณกำลังคอมไพล์โค้ด C หรือ C ++ กับคอมไพเลอร์สมัยใหม่คุณก็ไม่ต้องกังวลกับค่าใช้จ่ายในการโทรฟังก์ชั่น เครื่องมือเพิ่มประสิทธิภาพจะอินไลน์สิ่งเหล่านี้เมื่อใดก็ตามที่เหมาะสม ในฐานะผู้ที่ชื่นชอบการเพิ่มประสิทธิภาพขนาดเล็กฉันไม่ค่อยเห็นคอมไพเลอร์ตัดสินใจอินไลน์ที่ฉันหรือตัวชี้วัดของฉันไม่เห็นด้วย
Cody Gray

6
พูดจากประสบการณ์ส่วนตัวผมเขียนโค้ดในภาษาที่เป็นกรรมสิทธิ์ที่มีความทันสมัยอย่างเป็นธรรมในแง่ของความสามารถ แต่ฟังก์ชั่นการโทรมีราคาแพงขันไปยังจุดที่แม้ปกติสำหรับลูปจะต้องมีการปรับให้เหมาะสมกับความเร็ว: แทนที่จะfor(Integer index = 0, size = someList.size(); index < size; index++) for(Integer index = 0; index < someList.size(); index++)เพียงเพราะคอมไพเลอร์ของคุณถูกสร้างขึ้นในไม่กี่ปีที่ผ่านมาไม่ได้แปลว่าคุณสามารถทำโปรไฟล์ได้
phyrfox

5
@phyrfox ที่เหมาะสมแล้วรับค่า someList.size () นอกลูปแทนการเรียกมันทุกครั้งที่ผ่านลูป โดยเฉพาะอย่างยิ่งหากมีโอกาสเกิดปัญหาการซิงโครไนซ์ซึ่งผู้อ่านและผู้เขียนอาจพยายามปะทะกันในระหว่างการทำซ้ำซึ่งในกรณีนี้คุณต้องการป้องกันรายการจากการเปลี่ยนแปลงใด ๆ ในระหว่างการทำซ้ำ
เครก

8
ระวังอย่าให้ฟังก์ชั่นเล็กเกินไปมันอาจทำให้โค้ดยุ่งเหยิงได้อย่างมีประสิทธิภาพเช่นเดียวกับฟังก์ชั่น mega-monega หากคุณไม่เชื่อฉันให้ตรวจสอบผู้ชนะของioccc.org : โค้ดบางอย่างเป็นหนึ่งเดียวmain()บางส่วนแบ่งทุกอย่างออกเป็น 50 ฟังก์ชั่นเล็ก ๆ และทุกอย่างก็ไม่สามารถอ่านได้อย่างเต็มที่ เคล็ดลับคือเป็นเสมอที่จะตีดีสมดุล
cmaster

คำตอบ:


148

ขึ้นอยู่กับโดเมนของคุณ

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

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

และสุดท้ายมีกฎทองของประสิทธิภาพ: เสมอประวัติส่วนตัว อย่าเขียนรหัส "ปรับให้เหมาะสม" ตามข้อสมมติ หากคุณไม่ปกติให้เขียนทั้งสองกรณีและดูว่าแบบไหนดีกว่ากัน


13
และเช่นคอมไพเลอร์ HotSpot ดำเนินการเก็งกำไร Inliningซึ่งในบางแง่มุมอินไลน์ซับเน็ตแม้ว่ามันจะเป็นไปไม่ได้
Jörg W Mittag

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

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

2
@ Mehrdad แม้ในกรณีนี้ฉันจะแปลกใจหากไม่มีอะไรที่เกี่ยวข้องกับการเพิ่มประสิทธิภาพในรหัส เมื่อสร้างโปรไฟล์รหัสฉันเห็นสิ่งต่าง ๆ หนักกว่าฟังก์ชั่นการโทรและนั่นคือสิ่งที่เกี่ยวข้องเพื่อค้นหาการปรับให้เหมาะสม นักพัฒนาซอฟต์แวร์บางคนคลั่งไคล้ LOC ที่ไม่ได้เพิ่มประสิทธิภาพหนึ่งหรือสองตัว แต่เมื่อคุณโปรไฟล์ SW คุณรู้ว่าการออกแบบมีความสำคัญมากกว่านี้อย่างน้อยก็สำหรับส่วนที่ใหญ่ที่สุดของรหัส เมื่อคุณพบคอขวดคุณสามารถลองปรับให้เหมาะสมและมันจะมีผลกระทบมากกว่าการปรับให้เหมาะสมตามอำเภอใจในระดับต่ำเช่นการเขียนฟังก์ชั่นใหญ่เพื่อหลีกเลี่ยงค่าใช้จ่ายในการโทร
Tim

8
คำตอบที่ดี! จุดสุดท้ายของคุณควรเป็นที่หนึ่ง: ทำโปรไฟล์ทุกครั้งก่อนตัดสินใจว่าจะปรับให้เหมาะสมที่ไหน
CJ Dennis

56

ค่าใช้จ่ายในการเรียกใช้ฟังก์ชั่นขึ้นอยู่กับภาษาทั้งหมดและในระดับที่คุณปรับให้เหมาะสม

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

ในระดับสูงภาษาอย่าง Perl, Python และ Ruby มีการทำบัญชีจำนวนมากต่อการเรียกใช้ฟังก์ชันทำให้มีราคาค่อนข้างสูง สิ่งนี้ทำให้แย่ลงโดยการเขียนโปรแกรมเมตา ฉันเคยเร่งความเร็วซอฟต์แวร์ Python 3x เพียงแค่ยกฟังก์ชั่นการโทรออกจากลูปที่ร้อนแรงมาก ในรหัสประสิทธิภาพที่สำคัญฟังก์ชันของตัวช่วยอินไลน์สามารถมีผลที่เห็นได้ชัดเจน

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

  • หากรหัสของคุณไม่สำคัญกับประสิทธิภาพการทำงานทำให้การบำรุงรักษาง่ายขึ้น แม้ในซอฟต์แวร์ที่มีประสิทธิภาพที่สำคัญรหัสส่วนใหญ่จะไม่เป็น "ฮอตสปอต"

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

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


16
DBA ที่ฉลาดจริง ๆ เคยบอกฉันว่า "ทำให้ปกติเป็นปกติจนกว่าจะเจ็บจากนั้นจึงทำให้เป็นปกติจนไม่เป็นเช่นนั้น" ดูเหมือนว่าสำหรับฉันมันอาจจะถูกใช้ถ้อยคำใหม่เพื่อ "แยกวิธีการออกไปจนกว่ามันจะเจ็บ
RubberDuck

1
นอกเหนือจากค่าใช้จ่ายในการรับรู้แล้วยังมีค่าโสหุ้ยเป็นสัญลักษณ์ในข้อมูลดีบักเกอร์และโดยปกติแล้วค่าโสหุ้ยในไบนารีสุดท้ายจะหลีกเลี่ยงไม่ได้
Frank Hileman

เกี่ยวกับคอมไพเลอร์อัจฉริยะ - พวกเขาสามารถทำเช่นนั้นได้เสมอไป ตัวอย่างเช่น jvm สามารถอินไลน์สิ่งต่าง ๆ ตามโปรไฟล์รันไทม์ที่มีกับดักที่ราคาถูก / ฟรีมากสำหรับพา ธ ที่ผิดปกติหรือฟังก์ชั่น polymorphic แบบอินไลน์ซึ่งมีการใช้งานเพียงครั้งเดียวของเมธอด / อินเตอร์เฟสที่กำหนด รันไทม์ แต่ใช่มีหลายภาษาที่เป็นไปไม่ได้และหลาย ๆ กรณีแม้จะเป็น jvm เมื่อมันไม่คุ้มค่าหรือเป็นไปได้ในกรณีทั่วไป
Artur Biesiadowski

19

เกือบทั้งหมด adages เกี่ยวกับรหัสสำหรับการปรับแต่งประสิทธิภาพการทำงานเป็นกรณีพิเศษของกฎหมายของดาห์ล คำแถลงสั้น ๆ ที่ตลกขำขันของกฎของอัมดาห์คือ

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

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

นี่คือเหตุผลที่คนทั่วไปพูดว่าไม่ต้องกังวลเกี่ยวกับค่าใช้จ่ายในการโทรของฟังก์ชั่น: ไม่ว่าพวกเขาจะแพงแค่ไหนโดยปกติแล้วโปรแกรมโดยรวมจะใช้เวลาเพียงเล็กน้อยในการโทรบนโอเวอร์เฮด .

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

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

  • หากรันไทม์ต่อการร้องขอของฟังก์ชันน้อยกว่ามิลลิวินาที แต่ฟังก์ชันนั้นเรียกว่าหลายร้อยหลายพันครั้งก็น่าจะมีการแทรกไว้

  • หากโปรไฟล์ของโปรแกรมแสดงฟังก์ชั่นหลายพันฟังก์ชั่นและไม่มีฟังก์ชั่นใดเลยที่ใช้งานได้มากกว่า 0.1% ของเวลารันไทม์ดังนั้นโอเวอร์เฮดของการเรียกใช้ฟังก์ชันอาจมีความสำคัญในภาพรวม

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


7
แค่ระวังสิ่งที่มีราคาแพงทำลึกเข้าไปในลูปซ้อนกัน ฉันได้เพิ่มประสิทธิภาพฟังก์ชั่นหนึ่งและรหัสอากาศที่รันเร็ว 10 เท่า นั่นคือหลังจากที่ผู้สร้างโปรไฟล์ชี้ผู้กระทำผิดออกมา (มันถูกเรียกซ้ำไปซ้ำมาในลูปจาก O (n ^ 3) ถึง n o O ขนาดเล็ก (n ^ 6))
Loren Pechtel

"น่าเสียดายที่วิธีแก้ปัญหาเพียงอย่างเดียวคือกำจัดชั้นบาง ๆ ซึ่งมักจะยากมาก" - สิ่งนี้ขึ้นอยู่กับคอมไพเลอร์ภาษาและ / หรือเทคโนโลยีเครื่องเสมือนของคุณเป็นอย่างมาก หากคุณสามารถแก้ไขโค้ดเพื่อให้คอมไพเลอร์เป็นอินไลน์ได้ง่ายขึ้น (เช่นโดยใช้finalคลาสและวิธีการที่ใช้งานได้ใน Java หรือไม่ใช่virtualวิธีการใน C # หรือ C ++) ดังนั้นการกำจัดคอมไพล์ / รันไทม์และทิศทางของคุณ จะเห็นผลกำไรโดยไม่มีการปรับโครงสร้างครั้งใหญ่ ตามที่ @ JorgWMittag ชี้ให้เห็นข้างต้น JVM ยังสามารถอินไลน์ในกรณีที่ไม่สามารถพิสูจน์ได้ว่าการเพิ่มประสิทธิภาพคือ ...
Jules

... ถูกต้องดังนั้นอาจเป็นไปได้ว่ามันทำในรหัสของคุณแม้จะมีการฝังรากลึกอยู่แล้ว
Jules

@Jules แม้ว่าจะเป็นความจริงที่คอมไพเลอร์ของ JIT สามารถทำการปรับให้เหมาะสมแบบเก็งกำไร แต่ก็ไม่ได้หมายความว่าการปรับให้เหมาะสมนั้นจะถูกนำไปใช้อย่างสม่ำเสมอ โดยเฉพาะเกี่ยวกับ Java ประสบการณ์ของฉันคือวัฒนธรรมการพัฒนาชอบเลเยอร์ซ้อนอยู่ด้านบนของเลเยอร์ที่นำไปสู่สแต็คการโทรที่ลึกมาก โดยทั่วไปแล้วมีส่วนช่วยให้แอปพลิเคชัน Java จำนวนมากรู้สึกอืดและป่อง สถาปัตยกรรมที่มีเลเยอร์สูงดังกล่าวสามารถทำงานกับ JIT รันไทม์ได้ JIT ไม่ใช่กระสุนวิเศษที่สามารถแก้ไขปัญหาเชิงโครงสร้างได้โดยอัตโนมัติ
amon

@ มอนประสบการณ์ของฉันกับ "รหัสลาซานญ่า" มาจากแอพพลิเคชั่น C ++ ที่มีขนาดใหญ่มากซึ่งมีการเขียนโค้ดจำนวนมากจนถึงปี 1990 เมื่อลำดับชั้นของวัตถุที่ซ้อนกันอย่างลึกล้ำและ COM เป็นแฟชั่น คอมไพเลอร์ C ++ มีความพยายามอย่างกล้าหาญที่จะกำจัดบทลงโทษที่เป็นนามธรรมในโปรแกรมเช่นนี้และคุณยังคงเห็นว่าพวกเขาใช้เวลาเศษเสี้ยวของนาฬิกาแขวนบนแผงทางไปป์ไลน์สาขา (และอีกอันที่สำคัญเกี่ยวกับ I-cache) .
zwol

17

ฉันจะท้าทายคำพูดนี้:

มักจะมีข้อเสียระหว่างการเขียนรหัสที่สามารถอ่านได้และการเขียนรหัสนักแสดง

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

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

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

(ภาษาแบบไดนามิกบางภาษาเช่น Python มีค่าใช้จ่ายในการโทรด้วยวิธีการที่สำคัญ แต่หากประสิทธิภาพกลายเป็นปัญหาคุณอาจไม่ควรใช้ Python ตั้งแต่แรก)

หลักการส่วนใหญ่สำหรับโค้ดที่อ่านได้ - การจัดรูปแบบที่สอดคล้องกันชื่อตัวระบุที่มีความหมายความคิดเห็นที่เหมาะสมและเป็นประโยชน์และอื่น ๆ จะไม่มีผลต่อประสิทธิภาพ และบางอย่างเช่นการใช้ enums แทนที่จะเป็นสายก็มีประโยชน์ต่อประสิทธิภาพ


5

ค่าใช้จ่ายในการเรียกใช้ฟังก์ชันไม่สำคัญในกรณีส่วนใหญ่

อย่างไรก็ตามกำไรที่ใหญ่กว่าจากโค้ดอินไลน์คือการเพิ่มประสิทธิภาพรหัสใหม่หลังจากอินไลน์

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

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


5

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

หากฟังก์ชันที่เรียกใช้มีขนาดเล็กและคอมไพเลอร์สามารถอินไลน์ได้ค่าใช้จ่ายจะเป็นศูนย์ การใช้งานคอมไพเลอร์ / ภาษาสมัยใหม่มี JIT, การเพิ่มประสิทธิภาพการเชื่อมโยงเวลาและ / หรือระบบโมดูลที่ออกแบบมาเพื่อเพิ่มความสามารถในการทำงานแบบอินไลน์เมื่อมันเป็นประโยชน์

OTOH มีค่าใช้จ่ายที่ไม่ชัดเจนในการเรียกใช้ฟังก์ชัน: การมีอยู่ของพวกเขาอาจยับยั้งการปรับให้เหมาะสมของคอมไพเลอร์ก่อนและหลังการโทร

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

ตัวอย่างเช่นถ้าคุณไม่จำเป็นต้องเรียกใช้ฟังก์ชันในการวนซ้ำแต่ละครั้ง:

for(int i=0; i < /* gasp! */ strlen(s); i++) x ^= s[i];

คอมไพเลอร์อาจรู้ว่ามันเป็นฟังก์ชั่นที่บริสุทธิ์และย้ายออกจากลูป (ในกรณีที่แย่มากเช่นตัวอย่างนี้ยังแก้ไขอัลกอริทึม O (n ^ 2) โดยไม่ตั้งใจให้เป็น O (n)):

for(int i=0, end=strlen(s); i < end; i++) x ^= s[i];

จากนั้นอาจจะเขียนวนซ้ำเพื่อประมวลผลองค์ประกอบ 4/8/16 ต่อครั้งโดยใช้คำแนะนำแบบกว้าง / SIMD

แต่ถ้าคุณเพิ่มการเรียกไปยังโค้ดทึบแสงในลูปแม้ว่าการโทรจะไม่ทำอะไรเลยและมันราคาถูกมากตัวคอมไพเลอร์จะต้องถือว่าแย่ที่สุด - การโทรจะเข้าถึงตัวแปรทั่วโลกที่ชี้ไปยังหน่วยความจำเดียวกันกับsการเปลี่ยนแปลง เนื้อหาของมัน (แม้ว่าจะconstอยู่ในฟังก์ชั่นของคุณ แต่อาจไม่ใช่constที่อื่น) ทำให้การเพิ่มประสิทธิภาพเป็นไปไม่ได้:

for(int i=0; i < strlen(s); i++) {
    x ^= s[i];
    do_nothing();
}

3

เอกสารเก่านี้อาจตอบคำถามของคุณ:

Guy Lewis Steele, Jr .. "การเปิดโปงตำนาน 'การเรียกขั้นตอนราคาแพง' หรือการดำเนินการตามขั้นตอนการโทรถือว่าเป็นอันตรายหรือแลมบ์ดา: The Ultimate GOTO" MIT AI Lab AI Lab Memo AIM-443 ต.ค. 2520

บทคัดย่อ:

ชาวบ้านกล่าวว่างบ GOTO เป็น "ถูก" ในขณะที่การเรียกขั้นตอนเป็น "แพง" ตำนานนี้ส่วนใหญ่เป็นผลมาจากการใช้งานภาษาที่ออกแบบมาไม่ดี การเติบโตทางประวัติศาสตร์ของตำนานนี้ได้รับการพิจารณา ทั้งแนวคิดทางทฤษฎีและการนำไปปฏิบัติที่มีอยู่ถูกกล่าวถึงซึ่งทำให้ debunk myth นี้ มันแสดงให้เห็นว่าการใช้การโทรแบบไม่ จำกัด ขั้นตอนช่วยให้มีสไตล์ที่ยอดเยี่ยม โดยเฉพาะอย่างยิ่งผังงานใด ๆ ที่สามารถเขียนเป็นโปรแกรม "โครงสร้าง" โดยไม่ต้องแนะนำตัวแปรเพิ่มเติม ความยากลำบากในการใช้คำสั่ง GOTO และการเรียกโพรซีเดอร์นั้นเป็นลักษณะที่ขัดแย้งกันระหว่างแนวคิดการเขียนโปรแกรมเชิงนามธรรมและการสร้างภาษาที่เป็นรูปธรรม


12
ฉันสงสัยอย่างมากว่ากระดาษแบบเก่าที่จะตอบคำถามว่า "ต้นทุนการโทรฟังก์ชั่นยังคงมีความสำคัญในคอมไพเลอร์สมัยใหม่ " หรือไม่
Cody Gray

6
@CodyGray ฉันคิดว่าเทคโนโลยีคอมไพเลอร์ควรมีระดับสูงตั้งแต่ปี 1977 ดังนั้นหากการเรียกใช้ฟังก์ชันสามารถทำได้ในราคาถูกในปี 1977 เราควรจะสามารถทำได้ในตอนนี้ ดังนั้นคำตอบคือไม่ แน่นอนว่าคุณกำลังใช้การใช้ภาษาที่เหมาะสมซึ่งสามารถทำสิ่งต่าง ๆ เช่นฟังก์ชั่นอินไลน์
Alex Vong

4
@AlexVong อาศัยการเพิ่มประสิทธิภาพคอมไพเลอร์ในปี 1977 เป็นเหมือนการพึ่งพาแนวโน้มราคาสินค้าโภคภัณฑ์ในยุคหิน ทุกอย่างเปลี่ยนไปมากเกินไป ตัวอย่างเช่นการคูณที่ใช้เพื่อแทนที่ด้วยการเข้าถึงหน่วยความจำเป็นการดำเนินการที่ถูกกว่า ปัจจุบันมีราคาแพงกว่ามาก การเรียกใช้เมธอดเสมือนมีราคาค่อนข้างแพงกว่าที่เคยเป็น (การเข้าถึงหน่วยความจำและการคาดคะเนความผิดพลาดสาขา) แต่บ่อยครั้งที่พวกเขาสามารถปรับให้เหมาะที่สุดและการโทรด้วยวิธีเสมือนสามารถอินไลน์ได้ (Java ทำตลอดเวลา) ศูนย์แน่นอน ไม่มีอะไรเช่นนี้ในปี 1977
maaartinus

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

2
@AlexVong เพื่อให้แม่นยำยิ่งขึ้นเกี่ยวกับการเปลี่ยนแปลงของ CPU ที่ทำให้กระดาษนั้นล้าสมัย: ย้อนกลับไปในปี 1977 การเข้าถึงหน่วยความจำหลักเป็นรอบ CPU เดียว วันนี้แม้แต่การเข้าถึงแคช L1 (!) อย่างง่าย ๆ ก็มีเวลาแฝงอยู่ 3 ถึง 4 รอบ ตอนนี้การเรียกใช้ฟังก์ชั่นนั้นค่อนข้างหนักในการเข้าถึงหน่วยความจำ (การสร้างเฟรมสแต็ค, การบันทึกที่อยู่ผู้ส่งคืน, การบันทึกการลงทะเบียนสำหรับตัวแปรโลคอล) ซึ่งทำให้ค่าใช้จ่ายในการเรียกใช้ฟังก์ชัน หากฟังก์ชั่นของคุณจัดเรียงอาร์กิวเมนต์ใหม่และอาจเพิ่มอาร์กิวเมนต์คงที่อีกค่าหนึ่งเพื่อส่งผ่านการเรียกผ่านแสดงว่านั่นเป็นค่าใช้จ่ายเกือบ 100%
cmaster

3
  • ใน C ++ ระวังการออกแบบฟังก์ชั่นการโทรที่คัดลอกอาร์กิวเมนต์ค่าเริ่มต้นคือ "pass by value" ค่าใช้จ่ายในการเรียกใช้ฟังก์ชันเนื่องจากการบันทึกการลงทะเบียนและสิ่งอื่น ๆ ที่เกี่ยวข้องกับสแต็กเฟรมสามารถถูกครอบงำโดยสำเนาของวัตถุที่ไม่ได้ตั้งใจ (และอาจมีราคาแพงมาก)

  • มีการเพิ่มประสิทธิภาพที่เกี่ยวข้องกับสแต็กเฟรมที่คุณควรตรวจสอบก่อนที่จะยอมแพ้กับรหัสที่มีการแยกตัวประกอบสูง

  • ส่วนใหญ่เวลาที่ฉันต้องจัดการกับโปรแกรมที่ช้าฉันพบว่าการเปลี่ยนแปลงอัลกอริทึมให้ความเร็วที่สูงกว่าการเรียกฟังก์ชั่นอินไลน์ ตัวอย่างเช่น: วิศวกรคนอื่นแยก parser ที่เติมโครงสร้าง map-of-maps ส่วนหนึ่งของสิ่งนั้นคือเขาลบดัชนีแคชออกจากแผนที่หนึ่งไปยังแผนที่ที่มีเหตุผล นั่นเป็นการย้ายรหัสที่ดีอย่างไรก็ตามมันทำให้โปรแกรมไม่สามารถใช้งานได้เนื่องจากปัจจัยการชะลอตัว 100 ครั้งเนื่องจากการค้นหาแฮชสำหรับการเข้าถึงในอนาคตทั้งหมดเมื่อเทียบกับการใช้ดัชนีที่เก็บไว้ การทำโปรไฟล์แสดงให้เห็นว่าส่วนใหญ่ใช้เวลาในฟังก์ชั่นการแฮช


4
คำแนะนำแรกเก่าไปหน่อย ตั้งแต่ C ++ 11 การย้ายเป็นไปได้ โดยเฉพาะอย่างยิ่งสำหรับฟังก์ชั่นที่จำเป็นต้องแก้ไขข้อโต้แย้งของพวกเขาภายในการโต้แย้งโดยมูลค่าและการแก้ไขในสถานที่สามารถเป็นทางเลือกที่มีประสิทธิภาพมากที่สุด
MSalters

@Malters: ฉันคิดว่าคุณเข้าใจผิดว่า "โดยเฉพาะ" กับ "นอกจากนี้" หรือบางสิ่งบางอย่าง การตัดสินใจส่งสำเนาหรือเอกสารอ้างอิงอยู่ที่นั่นก่อนหน้า C ++ 11 (ฉันรู้ว่าคุณรู้)
phresnel

@phresnel: ฉันคิดว่าถูกต้องแล้ว กรณีเฉพาะที่ฉันอ้างถึงคือกรณีที่คุณสร้างชั่วคราวในผู้โทรย้ายไปยังอาร์กิวเมนต์แล้วปรับเปลี่ยนใน callee สิ่งนี้เป็นไปไม่ได้ก่อนที่ C ++ 11 เนื่องจาก C ++ 03 ไม่สามารถ / ไม่ผูกการอ้างอิงที่ไม่ใช่กลุ่มอ้างอิงชั่วคราวชั่วคราว
MSalters

@MSalters: จากนั้นฉันเข้าใจผิดความคิดเห็นของคุณเมื่ออ่านมันครั้งแรก สำหรับฉันดูเหมือนว่าคุณกำลังบอกว่าก่อน C ++ 11 การส่งผ่านค่าไม่ใช่สิ่งที่ใครจะทำได้หากต้องการแก้ไขค่าที่ส่งผ่าน
phresnel

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

3

อย่างที่คนอื่นพูดคุณควรวัดประสิทธิภาพโปรแกรมของคุณก่อนและอาจไม่พบความแตกต่างในทางปฏิบัติ

ถึงกระนั้นจากระดับแนวความคิดฉันคิดว่าฉันจะอธิบายบางสิ่งที่ทำให้สับสนในคำถามของคุณ ประการแรกคุณถาม:

ค่าใช้จ่ายในการใช้งานฟังก์ชั่นยังคงมีความสำคัญในคอมไพเลอร์สมัยใหม่หรือไม่?

สังเกตคำสำคัญ "ฟังก์ชั่น" และ "คอมไพเลอร์" คำพูดของคุณแตกต่างกันเล็กน้อย:

โปรดจำไว้ว่าค่าใช้จ่ายของการเรียกใช้วิธีการอาจมีความสำคัญขึ้นอยู่กับภาษา

นี่คือวิธีการพูดคุยในแง่ของวัตถุ

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

โดยเฉพาะอย่างยิ่งที่เราต้องรู้เกี่ยวกับการจัดส่งแบบคงที่เทียบกับการจัดส่งแบบไดนามิก ฉันจะเพิกเฉยต่อการเพิ่มประสิทธิภาพในขณะนี้

ในภาษาเช่น C ที่เรามักจะเรียกฟังก์ชั่นที่มีการจัดส่งแบบคงที่ ตัวอย่างเช่น:

int foo(int x) {
  return x + 1;
}

int bar(int y) {
  return foo(y);
}

int main() {
  return bar(42);
}

เมื่อคอมไพเลอร์เห็นการเรียกfoo(y)มันจะรู้ว่าฟังก์ชั่นfooชื่อนั้นหมายถึงอะไรดังนั้นโปรแกรมเอาต์พุตสามารถข้ามไปยังfooฟังก์ชั่นได้ซึ่งค่อนข้างถูก นั่นคือความหมายของการส่งแบบคงที่

อีกทางเลือกหนึ่งคือการจัดส่งแบบไดนามิกที่คอมไพเลอร์ไม่ทราบว่าจะเรียกใช้ฟังก์ชันใด ตัวอย่างเช่นนี่คือรหัส Haskell บางส่วน (เนื่องจากค่าเทียบเท่า C จะยุ่ง!):

foo x = x + 1

bar f x = f x

main = print (bar foo 42)

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

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

class A:
  def __init__(self, x):
    self.x = x

  def foo(self):
    return self.x + 1

def bar(y):
  return y.foo()

z = A(42)
bar(z)

การy.foo()เรียกใช้การจัดส่งแบบไดนามิกเนื่องจากเป็นการค้นหาค่าของfooคุณสมบัติในyวัตถุและเรียกสิ่งที่พบ ไม่รู้ว่าyจะมีคลาสAหรือAคลาสมีfooเมธอดดังนั้นเราจึงไม่สามารถกระโดดข้ามไปที่มันได้

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

แล้วสิ่งนี้จะส่งผลกระทบต่อคอมไพเลอร์ที่ทันสมัยและปรับให้เหมาะสมได้อย่างไร

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

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

ในตัวอย่างของ Python ข้างต้นการอนุมานนั้นค่อนข้างสิ้นหวังเนื่องจาก Python อนุญาตให้โค้ดอื่นแทนที่คลาสและคุณสมบัติได้ดังนั้นจึงเป็นการยากที่จะอนุมานได้ว่าจะเก็บไว้ในทุกกรณี

หากภาษาของเราทำให้เรามีข้อ จำกัด มากขึ้นตัวอย่างเช่นโดยการ จำกัดyชั้นเรียนAโดยใช้คำอธิบายประกอบดังนั้นเราสามารถใช้ข้อมูลนั้นเพื่อสรุปฟังก์ชั่นเป้าหมาย ในภาษาที่มีคลาสย่อย (ซึ่งเกือบทั้งหมดเป็นภาษาที่มีคลาส!) ที่จริงแล้วไม่เพียงพอเนื่องจากyจริง ๆ แล้วอาจมีคลาส (ย่อย) ที่แตกต่างกันดังนั้นเราจึงต้องการข้อมูลเพิ่มเติมเช่นfinalคำอธิบายประกอบของ Java เพื่อทราบว่าจะเรียกฟังก์ชันใด

Haskell ไม่ได้เป็นภาษา OO แต่เราสามารถอนุมานค่าของfโดย inlining bar(ซึ่งคงส่ง) ลงmainแทนสำหรับfoo yเนื่องจากเป้าหมายของfooin in mainเป็นที่รู้จักกันในแบบคงที่การเรียกจะถูกส่งแบบสแตติกและอาจได้รับการอินไลน์และปรับให้เหมาะสมอย่างสมบูรณ์ (เนื่องจากฟังก์ชั่นเหล่านี้มีขนาดเล็กคอมไพเลอร์มีแนวโน้มที่จะอินไลน์พวกมัน )

ดังนั้นราคาลดลงมาที่:

  • ภาษาส่งการโทรของคุณแบบคงที่หรือแบบไดนามิกหรือไม่?
  • หากเป็นภาษาหลังให้ใช้งานเพื่ออนุมานเป้าหมายโดยใช้ข้อมูลอื่น ๆ (เช่นประเภทคลาสคำอธิบายประกอบอินไลน์ ฯลฯ ) หรือไม่
  • การจัดส่งแบบสแตติก (เชิงอนุมานหรืออย่างอื่น) จะเพิ่มประสิทธิภาพได้อย่างไร

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


ฉันไม่เห็นด้วยที่เรียกการปิด (หรือตัวชี้ฟังก์ชั่นบางอย่าง) - เป็นตัวอย่าง Haskell ของคุณ - คือการจัดส่งแบบไดนามิก การจัดส่งแบบไดนามิกเกี่ยวข้องกับการคำนวณบางอย่าง(เช่นการใช้vtableบางอย่าง) เพื่อให้ได้การปิดดังนั้นจึงมีค่าใช้จ่ายมากกว่าการโทรทางอ้อม มิฉะนั้นคำตอบที่ดี
Basile Starynkevitch

2

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

ยกตัวอย่างเช่นพิจารณา Java เมื่อแรกเห็นแล้วค่าใช้จ่ายในการเรียกใช้ฟังก์ชันควรโดดเด่นเป็นพิเศษในภาษานี้:

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

ด้วยความหวาดกลัวจากการปฏิบัติเหล่านี้โปรแกรมเมอร์ C โดยเฉลี่ยจะทำนายว่า Java จะต้องมีขนาดอย่างน้อยหนึ่งลำดับที่ช้ากว่า C และ 20 ปีที่แล้วเขาก็จะถูกต้อง มาตรฐานที่ทันสมัย ​​แต่วางรหัส Java สำนวนภายในไม่กี่เปอร์เซ็นต์ของรหัส C เทียบเท่า เป็นไปได้อย่างไร?

เหตุผลหนึ่งก็คือฟังก์ชั่นอินไลน์ JVMs ที่ทันสมัยเรียกว่าเป็นเรื่องของหลักสูตร มันใช้อินไลน์การเก็งกำไร:

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

นั่นคือรหัส:

int x = point.getX();

ได้รับการเขียนใหม่ถึง

if (point.class != Point) GOTO interpreter;
x = point.x;

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

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


4
“ ไม่มีเหตุผลโดยธรรมชาติว่าทำไมคอมไพเลอร์ไม่สามารถรองรับการอินไลน์อัตโนมัติ” - มี คุณได้พูดคุยเกี่ยวกับการรวบรวม JIT ซึ่งมีจำนวนรหัสการแก้ไขด้วยตนเอง (ซึ่งระบบปฏิบัติการอาจป้องกันเพราะความปลอดภัย) และความสามารถในการเพิ่มประสิทธิภาพโปรแกรมเต็มรูปแบบแนวทางอัตโนมัติ คอมไพเลอร์ AOT สำหรับภาษาที่อนุญาตให้มีการเชื่อมโยงแบบไดนามิกไม่เพียงพอที่จะพัฒนาระบบเสมือนและอินไลน์การโทรใด ๆ OTOH: คอมไพเลอร์ AOT มีเวลาในการปรับแต่งทุกอย่างเท่าที่จะทำได้คอมไพเลอร์ JIT มีเวลาที่จะมุ่งเน้นไปที่การปรับให้เหมาะสมในจุดร้อน ในกรณีส่วนใหญ่ทำให้ JIT เสียเปรียบเล็กน้อย
amon

2
บอกหนึ่งระบบปฏิบัติการที่ป้องกันไม่ให้ใช้งาน Google Chrome "เพราะความปลอดภัย" (V8 คอมไพล์ JavaScript ให้กับรหัสเนทีฟขณะใช้งาน) นอกจากนี้ยังอยากจะ inline ทอทไม่ได้ค่อนข้างเหตุผลโดยธรรมชาติ (มันไม่ได้กำหนดโดยภาษา แต่สถาปัตยกรรมที่คุณเลือกสำหรับคอมไพเลอร์ของคุณ) และในขณะที่เชื่อมโยงแบบไดนามิกไม่ยับยั้งอินไลน์ทอทในหน่วยรวบรวมก็ไม่ได้ยับยั้งอินไลน์ภายในรวบรวม หน่วยที่การโทรส่วนใหญ่เกิดขึ้น ในความเป็นจริงการทำอินไลน์มีประโยชน์นั้นง่ายกว่าในภาษาที่ใช้การเชื่อมโยงแบบไดนามิกน้อยกว่า Java
meriton

4
โดยเฉพาะอย่างยิ่ง iOS ป้องกัน JIT สำหรับแอปที่ไม่มีสิทธิพิเศษ Chrome หรือ Firefox ต้องใช้มุมมองเว็บที่ Apple จัดหาให้แทนเครื่องมือของตัวเอง จุดที่ดีแม้ว่า AOT กับ JIT เป็นระดับการใช้งานไม่ใช่ตัวเลือกระดับภาษา
amon

@meriton Windows 10 S และระบบปฏิบัติการคอนโซลวิดีโอเกมยังมีแนวโน้มที่จะบล็อกเอ็นจิน JIT ของบุคคลที่สาม
Damian Yerrick

2

โปรดจำไว้ว่าค่าใช้จ่ายของการเรียกใช้วิธีการอาจมีความสำคัญขึ้นอยู่กับภาษา มักจะมีข้อเสียระหว่างการเขียนรหัสที่สามารถอ่านได้และการเขียนรหัสนักแสดง

นี่คือโชคไม่ดีขึ้นอยู่กับ:

  • toolchain ของคอมไพเลอร์รวมถึง JIT ถ้ามี
  • โดเมน

แรกของทุกกฎข้อที่หนึ่งของการเพิ่มประสิทธิภาพการปฏิบัติงานเป็นรายแรก มีหลายโดเมนที่ประสิทธิภาพของส่วนซอฟต์แวร์ไม่เกี่ยวข้องกับประสิทธิภาพของสแต็กทั้งหมด: การเรียกฐานข้อมูล, การทำงานของเครือข่าย, การปฏิบัติการของ OS, ...

นี่หมายความว่าประสิทธิภาพของซอฟต์แวร์นั้นไม่เกี่ยวข้องอย่างสมบูรณ์แม้ว่าจะไม่ได้ปรับปรุงความหน่วงแฝงการปรับซอฟต์แวร์ให้เหมาะสมอาจส่งผลให้ประหยัดพลังงานและประหยัดฮาร์ดแวร์ (หรือประหยัดแบตเตอรี่สำหรับแอพมือถือ) ซึ่งอาจสำคัญ

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

ดังนั้นก่อนที่จะปรับให้เหมาะสมคุณต้องเข้าใจสิ่งที่คุณปรับให้เหมาะสมสำหรับ ... และไม่ว่ามันจะคุ้มค่าหรือไม่


ตอนนี้เกี่ยวกับประสิทธิภาพของซอฟต์แวร์ล้วน ๆ มันแตกต่างกันมากระหว่าง toolchains

การเรียกใช้ฟังก์ชันมีสองค่าใช้จ่าย:

  • ค่าใช้จ่ายเวลาทำงาน
  • ต้นทุนเวลารวบรวม

ต้นทุนเวลาทำงานค่อนข้างชัดเจน เพื่อที่จะทำการเรียกฟังก์ชั่นจำนวนหนึ่งของการทำงานเป็นสิ่งที่จำเป็น ตัวอย่างเช่นการใช้ C บน x86 การเรียกใช้ฟังก์ชั่นจะต้องใช้ (1) การหกลงทะเบียนไปยังสแต็ก (2) การส่งอาร์กิวเมนต์ไปยังรีจิสเตอร์การเรียกและหลังจากนั้น (3) การกู้คืนรีจิสเตอร์จากสแต็ก ดูบทสรุปของการเรียกประชุมเพื่อดูการทำงานที่เกี่ยวข้องกับการนี้

การรั่วไหล / การรีจิสเตอร์รีจิสเตอร์นี้ใช้เวลาน้อยมาก (รอบ CPU หลายสิบ)

โดยทั่วไปแล้วคาดว่าค่าใช้จ่ายนี้จะเล็กน้อยเมื่อเทียบกับค่าใช้จ่ายจริงของการใช้งานฟังก์ชั่นอย่างไรก็ตามรูปแบบบางอย่างมีประสิทธิภาพที่นี่: getters, ฟังก์ชั่นที่ได้รับการปกป้องโดยเงื่อนไขง่าย ๆ ...

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

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

ตัวอย่างทั่วไปคือ:

void func(condition: boolean) {
    if (condition) {
        doLotsOfWork();
    }
}

void call() { func(false); }

หากfuncมีการ inlined แล้วเพิ่มประสิทธิภาพจะตระหนักว่าสาขาจะไม่ดำเนินการและเพิ่มประสิทธิภาพในการcallvoid call() {}

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


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


1

"โปรดจำไว้ว่าค่าใช้จ่ายในการเรียกใช้วิธีการอาจมีความสำคัญขึ้นอยู่กับภาษาซึ่งมักจะมีข้อเสียระหว่างการเขียนรหัสที่อ่านได้และการเขียนรหัสนักแสดง"

คำกล่าวที่ยกมานี้ยังคงมีผลบังคับใช้ในทุกวันนี้เนื่องจากอุตสาหกรรมที่อุดมไปด้วยคอมไพเลอร์สมัยใหม่ของนักแสดง

ฉันแค่จะบอกว่าไม่มีทางแบน ฉันเชื่อว่าคำพูดจะไม่ประมาทเพียงแค่โยนออกไปที่นั่น

แน่นอนว่าฉันไม่ได้พูดความจริงทั้งหมด แต่ฉันไม่สนใจที่จะพูดความจริงอย่างมาก มันเหมือนในภาพยนตร์เรื่อง Matrix ที่ฉันลืมไปว่ามันเป็น 1 หรือ 2 หรือ 3 - ฉันคิดว่ามันเป็นภาพยนตร์ที่มีนักแสดงหญิงชาวอิตาลีเซ็กซี่ที่มีแตงโมขนาดใหญ่ (ฉันไม่ชอบ แต่อย่างใดอย่างแรก) เมื่อ oracle lady บอกกับ Keanu Reeves ว่า"ฉันแค่บอกคุณว่าคุณต้องการได้ยินอะไร"หรืออะไรทำนองนี้นั่นคือสิ่งที่ฉันต้องการจะทำตอนนี้

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

อย่างไรก็ตามเพื่อให้ได้คำตอบที่แม่นยำยิ่งขึ้น เงื่อนไขของเรือบางส่วนมีการระบุไว้แล้วในคำตอบที่ดี เงื่อนไขที่เป็นไปได้เพียงแค่เลือกภาษาเดียวมีขนาดใหญ่เช่น C ++ ซึ่งจะต้องมีการจัดส่งแบบไดนามิกในการโทรเสมือนและเมื่อสามารถปรับให้เหมาะสมและอยู่ภายใต้คอมไพเลอร์และผู้เชื่อมโยงและรับประกันการตอบสนองโดยละเอียด เพื่อจัดการกับเงื่อนไขในทุกภาษาที่เป็นไปได้และเรียบเรียง แต่ฉันจะเพิ่มด้านบน"ใครสนใจ?" เพราะแม้แต่การทำงานในพื้นที่ที่มีความสำคัญต่อประสิทธิภาพในฐานะ raytracing สิ่งสุดท้ายที่ฉันจะเริ่มทำก่อนคือวิธีการอินไลน์ด้วยมือก่อนที่ฉันจะทำการวัดใด ๆ

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

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

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