ฟังก์ชั่นการโทรมีผลต่อประสิทธิภาพมากแค่ไหน


13

การแยกฟังก์ชั่นลงในวิธีการหรือฟังก์ชั่นเป็นสิ่งจำเป็นสำหรับรหัสแบบแยกส่วนการอ่านและการทำงานร่วมกันโดยเฉพาะอย่างยิ่งใน OOP

แต่นี่หมายถึงจะมีการเรียกใช้ฟังก์ชันเพิ่มเติม

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

* รายการยอดนิยม: C, Java, C ++, C #, Python, JavaScript, Ruby ...



1
ฉันคิดว่าการติดตั้งทุกภาษาที่มีคุณค่าของเกลือนั้นมีมานานหลายสิบปีแล้ว IOW ค่าโสหุ้ยเป็น 0 อย่างแม่นยำ
Jörg W Mittag

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

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

1
ผลกระทบถ้ามีน้อยจนคุณไม่เคยสังเกตเห็นมันเลย มีสิ่งอื่นที่สำคัญกว่าที่ต้องกังวล เช่นเดียวกับแท็บที่ควรจะเป็น 5 หรือ 7 ช่องว่าง
MetaFight

คำตอบ:


21

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

void DoSomething()
{
   a = a + 1;
   DoSomethingElse(a);
}

void DoSomethingElse(int a)
{
   b = a + 3;
}

คอมไพเลอร์ตัดสินใจที่จะอินไลน์DoSomethingElseและรหัสจะกลายเป็น

void DoSomething()
{
   a = a + 1;
   b = a + 3;
}

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

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

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

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


1
ฉันได้เห็นกับคอมไพเลอร์ที่ทันสมัย (GCC, เสียงดังกราว) ในสถานการณ์ที่ฉันได้รับการดูแลจริงๆที่พวกเขาสร้างรหัสที่เลวร้ายมากสำหรับลูปภายในฟังก์ชั่นขนาดใหญ่ การแยกลูปในฟังก์ชันคงที่ไม่ได้ช่วยเพราะ inlining การแยกลูปเข้าในฟังก์ชันภายนอกที่สร้างขึ้นในบางกรณีการปรับปรุงความเร็วอย่างมีนัยสำคัญ
gnasher729

1
ฉันจะย้อนกลับไปดูเรื่องนี้และบอกว่า OP ควรระวังเกี่ยวกับการเพิ่มประสิทธิภาพก่อนกำหนด
Patrick

1
@Patrick Bingo หากคุณกำลังจะปรับให้เหมาะสมใช้ profiler เพื่อดูว่าส่วนที่ช้าอยู่ตรงไหน อย่าเดา โดยปกติคุณสามารถรับรู้ได้ว่าส่วนที่ช้านั้นอาจอยู่ที่ใด แต่ยืนยันด้วย profiler
CHendrix

@ gnasher729 เพื่อแก้ปัญหาเฉพาะเรื่องหนึ่งคุณจะต้องมากกว่า profiler - เราจะต้องเรียนรู้การอ่านรหัสเครื่องที่ถอดประกอบได้เช่นกัน ในขณะที่มีการปรับให้เหมาะสมก่อนกำหนดไม่มีสิ่งเช่นการเรียนรู้ก่อนกำหนด (อย่างน้อยในการพัฒนาซอฟต์แวร์)

คุณอาจมีปัญหานี้หากคุณกำลังเรียกใช้ฟังก์ชั่นหนึ่งล้านครั้ง แต่คุณมีแนวโน้มที่จะมีปัญหาอื่น ๆ ที่มีผลกระทบมากขึ้นอย่างมาก
Michael Shaw

5

นี่เป็นเรื่องของการใช้งานคอมไพเลอร์หรือรันไทม์ (และตัวเลือก) และไม่สามารถพูดด้วยความมั่นใจใด ๆ

ภายใน C และ C ++ คอมไพเลอร์บางตัวจะทำการโทรแบบอินไลน์ตามการตั้งค่าการเพิ่มประสิทธิภาพซึ่งสามารถเห็นได้เล็กน้อยโดยการตรวจสอบชุดประกอบที่สร้างขึ้นเมื่อดูเครื่องมือเช่นhttps://gcc.godbolt.org/

ภาษาอื่น ๆ เช่น Java มีสิ่งนี้เป็นส่วนหนึ่งของรันไทม์ นี่เป็นส่วนหนึ่งของ JIT และได้อธิบายไว้ในคำถาม SOนี้ ในลักษณะ paticular ที่ตัวเลือก JVM สำหรับ HotSpot

-XX:InlineSmallCode=n วิธีการรวบรวมที่ก่อนหน้านี้เฉพาะในกรณีที่ขนาดรหัสเนทีฟที่สร้างขึ้นมีขนาดเล็กกว่านี้ ค่าดีฟอลต์จะแตกต่างกันไปตามแพลตฟอร์มที่ JVM กำลังทำงานอยู่
-XX:MaxInlineSize=35 ขนาดไบต์ใหญ่ที่สุดของวิธีการที่จะ inline
-XX:FreqInlineSize=n ขนาดไบต์สูงสุดของวิธีการที่ถูกเรียกใช้งานบ่อยครั้งที่จะถูกแทรก ค่าดีฟอลต์จะแตกต่างกันไปตามแพลตฟอร์มที่ JVM กำลังทำงานอยู่

ใช่แล้วคอมไพเลอร์ HotSpot JIT จะอินไลน์เมธอดที่ตรงกับเกณฑ์ที่แน่นอน

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

สิ่งนี้สามารถมองเห็นได้ว่าเป็นวิธีการที่เข้าใจผิดกับ CPython ไม่ใช่การทำอินไลน์ แต่ Jython (Python ทำงานใน JVM) ที่มีการโทรบางสาย ในทำนองเดียวกัน MRI Ruby จะไม่อินไลน์ในขณะที่ JRuby ต้องการและ ruby2c ซึ่งเป็น transpiler สำหรับ ruby ​​เข้าสู่ C ... ซึ่งสามารถทำการอินไลน์หรือไม่ขึ้นอยู่กับตัวเลือกคอมไพเลอร์ C ที่ถูกคอมไพล์ด้วย

ภาษาไม่อินไลน์ การใช้งานที่อาจ


5

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

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

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

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

while(!JobDone(idJob)){
    ...
}

หรือ

foreach(idJob in jobs){
    if (JobDone(idJob)){
        ...
    }
}

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


1

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

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

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

แต่

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

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

ฉันคิดว่าคำถามของคุณในทางปฏิบัตินั้นกว้างเกินไปที่จะตอบอย่างสมบูรณ์ใน stackexchange

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

คุณจะประหลาดใจกับจำนวนสิ่งที่คุณจะเรียนรู้ในกระบวนการนี้

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

หากคุณถามคำถามที่เฉพาะเจาะจงมากขึ้นฉันจะได้รับคำตอบที่เจาะจงยิ่งขึ้น


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

ประเด็นก็คือว่ามันขึ้นอยู่กับภาษา ใน C และ C ++ หากฟังก์ชั่นอินไลน์ผลกระทบคือ 0 หากไม่ได้อินไลน์มันขึ้นอยู่กับพารามิเตอร์ของมันถ้ามันอยู่ในแคชหรือไม่ ฯลฯ ...
ingframin

1

ฉันวัดค่าใช้จ่ายโดยตรงของฟังก์ชั่น C ++ โดยตรงและเสมือนบน Xenon PowerPC เมื่อไม่นานมานี้

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

เรื่องสั้นสั้นค่าใช้จ่ายในการเรียกใช้ฟังก์ชั่นโดยตรง (ไม่ใช่เสมือน) ประมาณ 5.5 นาโนวินาทีหรือ 18 รอบนาฬิกาเมื่อเทียบกับการเรียกฟังก์ชั่นอินไลน์ โอเวอร์เฮดของการเรียกใช้ฟังก์ชันเสมือนคือ 13.2 นาโนวินาทีหรือ 42 รอบนาฬิกาเมื่อเทียบกับอินไลน์

การกำหนดเวลาเหล่านี้อาจแตกต่างกันในตระกูลโปรเซสเซอร์ที่แตกต่างกัน รหัสการทดสอบของฉันอยู่ที่นี่ ; คุณสามารถเรียกใช้การทดสอบเดียวกันกับฮาร์ดแวร์ของคุณได้ ใช้ตัวจับเวลาที่มีความแม่นยำสูงเช่นrdtscสำหรับการติดตั้ง CFastTimer ของคุณ เวลาของระบบ () ไม่แม่นยำพอ

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