ประสิทธิภาพ: การเรียกซ้ำกับการทำซ้ำใน Javascript


24

ฉันได้อ่านเมื่อเร็ว ๆ นี้บางบทความ (เช่นhttp://dailyjs.com/2012/09/14/functional-programming/ ) เกี่ยวกับลักษณะการทำงานของ Javascript และความสัมพันธ์ระหว่าง Scheme และ Javascript (อันหลังได้รับอิทธิพลจากอันแรกซึ่ง เป็นภาษาที่ใช้งานได้ในขณะที่ลักษณะของ OO นั้นสืบทอดมาจาก Self ซึ่งเป็นภาษาที่ใช้สร้างต้นแบบ

อย่างไรก็ตามคำถามของฉันเฉพาะเจาะจงมากขึ้น: ฉันสงสัยว่ามีตัวชี้วัดเกี่ยวกับประสิทธิภาพของการเรียกซ้ำกับการทำซ้ำใน Javascript หรือไม่

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


3
ทำแบบทดสอบของคุณเองและค้นหาได้ทันทีที่jsperf.com
TehShrike

ด้วยความโปรดปรานและหนึ่งคำตอบที่กล่าวถึง TCO ปรากฏว่า ES6 ระบุ TCO แต่จนถึงขณะนี้ไม่มีใครนำไปใช้ถ้าเราเชื่อว่าkangax.github.io/compat-table/es6ฉันขาดอะไรไปหรือเปล่า?
Matthias Kauer

คำตอบ:


28

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

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


แค่สงสัย ... ฉันเห็นโค้ดที่อาร์เรย์ว่างถูกสร้างขึ้นและไซต์ฟังก์ชันเวียนเกิดถูกกำหนดให้กับตำแหน่งในอาเรย์แล้วค่าที่เก็บไว้ในอาเรย์จะถูกส่งกลับ นั่นคือสิ่งที่คุณหมายถึงโดย "แทนที่สอบถามซ้ำด้วยสแต็คของคุณเอง"? เช่น: var stack = []; var factorial = function(n) { if(n === 0) { return 1 } else { stack[n-1] = n * factorial(n - 1); return stack[n-1]; } }
mastazi

@mastazi: ไม่นี่จะทำให้ call stack ไร้ประโยชน์ไปพร้อมกับ call ภายใน ฉันหมายถึงสิ่งที่ต้องการคิวตามน้ำท่วมเติมจากวิกิพีเดีย
Triang3l

เป็นที่น่าสังเกตว่าภาษาไม่สามารถใช้งาน TCO ได้ แต่อาจมีการนำไปใช้ วิธีที่ผู้คนเพิ่มประสิทธิภาพ JS หมายความว่าบางที TCO อาจปรากฏในการใช้งานไม่กี่
Daniel Gratzer

1
@mastazi แทนที่elseในฟังก์ชั่นที่มีelse if (stack[n-1]) { return stack[n-1]; } elseและคุณมีmemoization ใครก็ตามที่เขียนรหัสแฟคทอเรียลนั้นมีการใช้งานที่ไม่สมบูรณ์ (และน่าจะใช้ได้stack[n]ทุกที่แทนที่จะเป็นstack[n-1])
Izkata

ขอบคุณ @Izkata ฉันมักจะเพิ่มประสิทธิภาพแบบนั้น แต่จนถึงวันนี้ฉันไม่รู้ชื่อของมัน ฉันควรเรียน CS แทนไอที ;-)
mastazi

20

อัปเดต : ตั้งแต่ ES2015, JavaScript มี TCOดังนั้นบางส่วนของอาร์กิวเมนต์ด้านล่างจึงไม่สามารถใช้งานได้อีก


แม้ว่า Javascript จะไม่มีการเพิ่มประสิทธิภาพการโทรหาง แต่การเรียกซ้ำมักเป็นวิธีที่ดีที่สุด และอย่างจริงใจยกเว้นในกรณีขอบคุณจะไม่ได้รับ call stack overflows

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

การเรียกซ้ำเป็นสิ่งที่สง่างามกว่าเสมอ1 .

1 : ความคิดเห็นส่วนตัว


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

3
@astazi ตามที่กล่าวไว้ในคำตอบของฉันฉันสงสัยว่าการเรียกซ้ำจะเป็นคอขวดของคุณ ส่วนใหญ่เป็นการจัดการ DOM หรือโดยทั่วไปคือ I / O และอย่าลืมว่าการปรับให้เหมาะสมก่อนวัยอันควรเป็นรากฐานของความชั่วร้ายทั้งหมด)
Florian Margaine

+1 สำหรับการจัดการ DOM เป็นคอขวดส่วนใหญ่! ฉันจำบทสัมภาษณ์ที่น่าสนใจของ Yehuda Katz (Ember.js) เกี่ยวกับเรื่องนี้
mastazi

1
@ ไมค์คำจำกัดความของ "คลอดก่อนกำหนด"คือ "สุกหรือสุกก่อนเวลาที่เหมาะสม" หากคุณรู้ว่าการทำอะไรซ้ำ ๆ จะทำให้เกิดการสแต็คโอเวอร์โฟลว์นั่นไม่ใช่การคลอดก่อนกำหนด อย่างไรก็ตามหากคุณคาดหวังว่าจะมีความตั้งใจ (ไม่มีข้อมูลจริง) แสดงว่าเป็นการคลอดก่อนกำหนด
Zirak

2
ด้วย Javascript คุณจะไม่สามารถใช้โปรแกรมได้เท่าไร คุณสามารถมีกองเล็ก ๆ ใน IE6 หรือกองใหญ่ใน FireFox อัลกอริทึมแบบเรียกซ้ำไม่ค่อยมีความลึกคงที่เว้นแต่ว่าคุณจะทำวนซ้ำแบบ Scheme ดูเหมือนจะไม่เหมือนกับว่าการเรียกซ้ำที่ไม่วนซ้ำเหมาะกับการหลีกเลี่ยงการปรับให้เหมาะสมก่อนวัยอันควร
mike30

7

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

รหัส: https://github.com/j03m/trickyQuestions/blob/master/factorial.js

Result:
j03m-MacBook-Air:trickyQuestions j03m$ node factorial.js 
Time:557
Time:126
j03m-MacBook-Air:trickyQuestions j03m$ node factorial.js 
Time:519
Time:120
j03m-MacBook-Air:trickyQuestions j03m$ node factorial.js 
Time:541
Time:123
j03m-MacBook-Air:trickyQuestions j03m$ node --version
v0.8.22

คุณสามารถลองและลอง"use strict";ดูว่ามันสร้างความแตกต่างหรือไม่ (มันจะสร้างjumps แทนลำดับการโทรมาตรฐาน)
Burdock

1
ในโหนดรุ่นล่าสุด (6.9.1) ฉันได้ผลลัพธ์ที่คล้ายกันมาก มีการเรียกเก็บภาษีอีกเล็กน้อย แต่ฉันคิดว่ามันเป็นกรณีขอบ - ความแตกต่าง 400ms สำหรับ 1,000,000 ลูปคือ 0.0025 ms ต่อลูป หากคุณทำ 1,000,000 ลูปมันเป็นสิ่งที่คุณควรคำนึงถึง
Kelz

6

ตามคำขอโดย OPฉันจะชิปใน (โดยไม่ต้องหลอกตัวเองหวังว่า: P)

ฉันคิดว่าเราทุกคนต่างเห็นพ้องกันว่าการเรียกซ้ำเป็นวิธีการเข้ารหัสที่หรูหรายิ่งขึ้น หากทำได้ดีก็สามารถสร้างรหัสที่สามารถบำรุงรักษาได้มากขึ้นซึ่งเป็น IMHO ที่สำคัญเช่นกัน (ถ้าไม่ใช่มากกว่านั้น) ซึ่งจะกำจัด 0.0001ms

เท่าที่อาร์กิวเมนต์ที่ JS ไม่ดำเนินการปรับให้เหมาะสมแบบ Tail-call นั้นไม่เป็นความจริงอีกต่อไปการใช้โหมดที่เข้มงวดของ ECMA5เปิดใช้งาน TCO มันเป็นสิ่งที่ฉันไม่พอใจเกินไปในขณะที่กลับมา แต่อย่างน้อยตอนนี้ฉันรู้ว่าทำไมarguments.calleeข้อผิดพลาดในโหมดเข้มงวด ฉันรู้ลิงค์ด้านบนลิงก์ไปยังรายงานข้อผิดพลาด แต่ข้อผิดพลาดตั้งเป็น WONTFIX นอกจากนี้ TCO มาตรฐานกำลังมา: ECMA6 (ธันวาคม 2013)

ตามสัญชาตญาณและยึดติดกับลักษณะการทำงานของ JS ฉันจะบอกว่าการเรียกซ้ำเป็นรูปแบบการเข้ารหัสที่มีประสิทธิภาพมากขึ้น 99.99% ของเวลา อย่างไรก็ตาม Florian Margaine มีจุดเมื่อเขาบอกว่าคอขวดมีแนวโน้มที่จะพบที่อื่น หากคุณจัดการกับ DOM คุณควรจดจ่อกับการเขียนโค้ดของคุณให้ดีที่สุดเท่าที่จะทำได้ DOM API คืออะไร: ช้า

ฉันคิดว่าเป็นไปไม่ได้ที่จะเสนอคำตอบที่ชัดเจนว่าเป็นตัวเลือกใดที่เร็วกว่า เมื่อเร็ว ๆ นี้ jspref จำนวนมากที่ฉันเคยเห็นแสดงให้เห็นว่าเครื่องยนต์ V8 ของ Chrome ทำงานได้อย่างรวดเร็วอย่างน่าขันในบางงานซึ่งทำงานช้ากว่า 4x ใน SpiderMonkey ของ FF และในทางกลับกัน เอ็นจิ้น JS สมัยใหม่มีกลอุบายมากมายเพื่อเพิ่มประสิทธิภาพรหัสของคุณ ฉันไม่มีความเชี่ยวชาญ แต่ฉันรู้สึกว่า V8 นั้นเหมาะสำหรับการปิด (และการเรียกซ้ำ) ในขณะที่เอ็นจิ้น JScript ของ MS ไม่ได้เป็นเช่นนั้น SpiderMonkey มักจะทำงานได้ดีขึ้นเมื่อ DOM เกี่ยวข้อง ...

ในระยะสั้น: ฉันจะบอกว่าเทคนิคใดจะมีประสิทธิภาพมากกว่าเช่นใน JS โดยที่ไม่สามารถคาดเดาได้


3

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

ตัวอย่าง: Jsperf

ฉันขอแนะนำให้กังวลมากขึ้นเกี่ยวกับความชัดเจนของรหัสและความเรียบง่ายเมื่อพูดถึงการเลือกระหว่างการเรียกซ้ำและการวนซ้ำ

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