Hiệu suất: đệ quy so với lặp trong Javascript


24

Gần đây tôi đã đọc một số bài viết (ví dụ: http://dailyjs.com/2012/09/14/feftal-programming/ ) về các khía cạnh chức năng của Javascript và mối quan hệ giữa Scheme và Javascript (cái sau bị ảnh hưởng bởi cái đầu tiên là một ngôn ngữ chức năng, trong khi các khía cạnh OO được kế thừa từ Tự là ngôn ngữ dựa trên nguyên mẫu).

Tuy nhiên, câu hỏi của tôi cụ thể hơn: Tôi đã tự hỏi nếu có số liệu về hiệu suất của đệ quy so với lặp trong Javascript.

Tôi biết rằng trong một số ngôn ngữ (trong đó việc lặp lại thiết kế hoạt động tốt hơn) sự khác biệt là tối thiểu vì trình thông dịch / trình biên dịch chuyển đổi đệ quy thành phép lặp, tuy nhiên tôi đoán rằng có lẽ đây không phải là trường hợp của Javascript vì ít nhất là một phần chức năng ngôn ngữ.


3
Tự kiểm tra và tìm hiểu ngay tại jsperf.com
TehShrike

với tiền thưởng và một câu trả lời đề cập đến TCO. Dường như ES6 chỉ định TCO nhưng cho đến nay không ai thực hiện nó nếu chúng tôi tin rằng kangax.github.io/compat-table/es6 Tôi có thiếu điều gì không?
Matthias Kauer

Câu trả lời:


28

JavaScript không thực hiện tối ưu hóa đệ quy đuôi , vì vậy nếu đệ quy của bạn quá sâu, bạn có thể bị tràn ngăn xếp cuộc gọi. Lặp đi lặp lại không có vấn đề như vậy. Nếu bạn nghĩ rằng bạn sẽ tái diễn quá nhiều và bạn thực sự cần đệ quy (ví dụ, để thực hiện lấp lũ), hãy thay thế đệ quy bằng ngăn xếp của riêng bạn.

Hiệu suất đệ quy có thể kém hơn hiệu suất lặp, bởi vì các lệnh gọi và trả về hàm yêu cầu bảo toàn và khôi phục trạng thái, trong khi phép lặp chỉ đơn giản là nhảy đến một điểm khác trong hàm.


Chỉ cần tự hỏi ... Tôi đã thấy một chút mã trong đó một mảng trống được tạo và trang chức năng đệ quy được gán cho một vị trí vào mảng và sau đó giá trị được lưu trữ trong mảng được trả về. Đó có phải là ý của bạn khi "thay thế đệ quy bằng ngăn xếp của riêng bạn" không? Ví dụ: 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: Không, điều này sẽ tạo ra một ngăn xếp cuộc gọi vô dụng cùng với cuộc gọi nội bộ. Tôi có nghĩa là một cái gì đó giống như lấp đầy dựa trên hàng đợi từ Wikipedia .
Triang3l

Điều đáng chú ý là một ngôn ngữ không thực hiện TCO, nhưng việc triển khai có thể. Cách mọi người tối ưu hóa JS có nghĩa là có lẽ TCO có thể xuất hiện trong một vài triển khai
Daniel Gratzer

1
@mastazi Thay thế elsechức năng đó bằng else if (stack[n-1]) { return stack[n-1]; } elsevà bạn có khả năng ghi nhớ . Bất cứ ai đã viết mã giai thừa đó đều có một triển khai không đầy đủ (và có lẽ nên sử dụng stack[n]ở mọi nơi thay vì stack[n-1]).
Izkata

Cảm ơn bạn @Izkata, tôi thường thực hiện kiểu tối ưu hóa đó nhưng cho đến hôm nay tôi vẫn chưa biết tên của nó. Tôi nên học CS thay vì CNTT ;-)
mastazi

20

Cập nhật : kể từ ES2015, JavaScript có TCO , do đó, một phần của đối số bên dưới không còn tồn tại nữa.


Mặc dù Javascript không có tối ưu hóa cuộc gọi đuôi, đệ quy thường là cách tốt nhất để đi. Và chân thành, ngoại trừ trong các trường hợp cạnh, bạn sẽ không bị tràn ngăn xếp cuộc gọi.

Hiệu suất là điều cần lưu ý, nhưng tối ưu hóa quá sớm. Nếu bạn nghĩ rằng đệ quy là thanh lịch hơn lặp đi lặp lại, thì hãy đi cho nó. Nếu hóa ra đây là nút cổ chai của bạn (có thể không bao giờ), thì bạn có thể thay thế bằng một số lần lặp xấu xí. Nhưng hầu hết thời gian, nút cổ chai nằm trong các thao tác DOM hoặc nói chung là I / O, không phải bản thân mã.

Đệ quy luôn thanh lịch hơn 1 .

1 : Ý kiến ​​cá nhân.


3
Tôi đồng ý rằng đệ quy là thanh lịch hơn, và thanh lịch là quan trọng vì nó dễ đọc cũng như khả năng duy trì (điều này là chủ quan, nhưng theo tôi thì đệ quy rất dễ đọc, do đó có thể duy trì được). Tuy nhiên, đôi khi vấn đề hiệu suất; bạn có thể ủng hộ tuyên bố rằng đệ quy là cách tốt nhất để đi, thậm chí là hiệu quả không?
mastazi

3
@mastazi như đã nói trong câu trả lời của tôi, tôi nghi ngờ rằng đệ quy sẽ là nút cổ chai của bạn. Hầu hết thời gian, đó là thao tác DOM, hay nói chung là I / O. Và đừng quên rằng tối ưu hóa sớm là gốc rễ của mọi tệ nạn;)
Florian Margaine

+1 cho thao tác DOM hầu hết là một nút cổ chai! Tôi nhớ một cuộc phỏng vấn rất thú vị với Yehuda Katz (Ember.js) về điều này.
mastazi

1
@mike Định nghĩa của "sinh non" là "trưởng thành hoặc chín muồi trước thời điểm thích hợp." Nếu bạn biết rằng làm đệ quy một cái gì đó sẽ gây ra một stackoverflow, thì đó không phải là sớm. Tuy nhiên, nếu bạn giả sử một ý thích bất chợt (không có bất kỳ dữ liệu thực tế nào), thì đó là quá sớm.
Zirak

2
Với Javascript, bạn không có bao nhiêu chương trình sẽ có sẵn. Bạn có thể có một ngăn xếp nhỏ trong IE6 hoặc một ngăn xếp lớn trong FireFox. Các thuật toán đệ quy hiếm khi có độ sâu cố định trừ khi bạn thực hiện một vòng lặp đệ quy theo kiểu Scheme. Có vẻ như không có đệ quy dựa trên vòng lặp phù hợp để tránh tối ưu hóa sớm.
mike30

7

Tôi cũng khá tò mò về hiệu suất này trong javascript, vì vậy tôi đã thực hiện một số thử nghiệm (mặc dù trên phiên bản cũ hơn của nút). Tôi đã viết một máy tính giai thừa đệ quy so với các lần lặp và chạy nó một vài lần cục bộ. Kết quả có vẻ khá sai lệch về việc đệ quy có thuế (dự kiến).

Mã: 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

Bạn có thể thử điều này với "use strict";và xem nếu nó làm cho một sự khác biệt. (Nó sẽ tạo jumps thay vì chuỗi cuộc gọi tiêu chuẩn)
Burdock

1
Trên một phiên bản gần đây của nút (6.9.1) tôi đã nhận được kết quả cực kỳ giống nhau. Có một chút thuế đối với đệ quy, nhưng tôi coi đó là trường hợp cạnh - chênh lệch 400ms cho 1.000.000 vòng là 0,0025 ms mỗi vòng. Nếu bạn đang thực hiện 1.000.000 vòng thì đó là điều cần lưu ý.
Kelz

6

Theo yêu cầu của OP, tôi sẽ tham gia (không tự lừa dối bản thân mình, hy vọng: P)

Tôi nghĩ rằng tất cả chúng ta đều đồng ý rằng đệ quy chỉ là một cách mã hóa thanh lịch hơn. Nếu được thực hiện tốt, nó có thể tạo ra mã có thể bảo trì nhiều hơn, đó là IMHO cũng quan trọng (nếu không phải là nhiều hơn) mà cạo đi 0,0001ms.

Theo như lập luận rằng JS không thực hiện tối ưu hóa cuộc gọi đuôi có liên quan: điều đó không hoàn toàn đúng nữa, sử dụng chế độ nghiêm ngặt của ECMA5 cho phép TCO. Đó là điều mà tôi đã không vui khi trở lại, nhưng ít nhất bây giờ tôi biết tại sao lại arguments.calleeném lỗi trong chế độ nghiêm ngặt. Tôi biết các liên kết ở trên liên kết đến một báo cáo lỗi, nhưng lỗi được đặt thành WONTFIX. Bên cạnh đó, TCO tiêu chuẩn sắp ra mắt: ECMA6 (tháng 12 năm 2013).

Theo bản năng và bám sát bản chất chức năng của JS, tôi muốn nói rằng đệ quy là kiểu mã hóa hiệu quả hơn 99,99% thời gian. Tuy nhiên, Florian Margaine có quan điểm khi nói rằng nút cổ chai có khả năng được tìm thấy ở nơi khác. Nếu bạn đang thao túng DOM, có lẽ bạn tập trung vào việc viết mã của mình càng tốt càng tốt. API DOM là những gì nó là: chậm.

Tôi nghĩ rằng không thể đưa ra một câu trả lời dứt khoát vì đó là lựa chọn nhanh hơn. Gần đây, rất nhiều jspref đã thấy cho thấy rằng động cơ V8 của Chrome có tốc độ nhanh một cách lố bịch ở một số tác vụ, chạy chậm hơn 4 lần trên SpiderMonkey của FF và ngược lại. Các công cụ JS hiện đại có tất cả các loại thủ thuật để tối ưu hóa mã của bạn. Tôi không phải là chuyên gia, nhưng tôi cảm thấy rằng V8, chẳng hạn, được tối ưu hóa cao cho việc đóng (và đệ quy), trong khi công cụ JScript của MS thì không. SpiderMonkey thường hoạt động tốt hơn khi DOM có liên quan ...

Tóm lại: Tôi muốn nói kỹ thuật nào sẽ hiệu quả hơn, như mọi khi trong JS, không thể dự đoán được.


3

Không có chế độ nghiêm ngặt, hiệu suất lặp thường nhanh hơn một chút sau đó đệ quy ( ngoài việc làm cho JIT làm được nhiều việc hơn ). Tối ưu hóa đệ quy đuôi về cơ bản giúp loại bỏ bất kỳ sự khác biệt đáng chú ý nào vì nó biến toàn bộ chuỗi cuộc gọi thành một bước nhảy.

Ví dụ: Jsperf

Tôi sẽ đề nghị lo lắng nhiều hơn về sự rõ ràng và đơn giản của mã khi nói đến việc lựa chọn giữa đệ quy và lặp.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.