TL; DR Vòng lặp chậm hơn là do truy cập vào các 'vùng ngoài' của Mảng, điều này buộc động cơ phải biên dịch lại hàm với ít hoặc thậm chí không có tối ưu hóa HOẶC không biên dịch hàm với bất kỳ tối ưu hóa nào để bắt đầu ( nếu Trình biên dịch (JIT-) phát hiện / nghi ngờ tình trạng này trước khi biên dịch 'phiên bản' đầu tiên), hãy đọc phần bên dưới tại sao;
Có người chỉ
có nói điều này (hoàn toàn ngạc nhiên không ai đã làm):
Đã từng có một thời gian khi đoạn của OP sẽ là một ví dụ de-facto trong một người mới bắt đầu lập trình cuốn sách nhằm phác thảo / nhấn mạnh rằng 'mảng' trong javascript là được lập chỉ mục bắt đầu ở 0, không phải 1 và như vậy được sử dụng như một ví dụ về 'lỗi người mới bắt đầu' phổ biến (bạn không thích cách tôi tránh cụm từ 'lỗi lập trình'
;)
):
truy cập ngoài phạm vi .
Ví dụ 1:
a Dense Array
(không liền kề (có nghĩa là không có khoảng cách giữa các chỉ mục) VÀ thực sự là một phần tử ở mỗi chỉ mục) của 5 phần tử sử dụng lập chỉ mục dựa trên 0 (luôn có trong ES262).
var arr_five_char=['a', 'b', 'c', 'd', 'e']; // arr_five_char.length === 5
// indexes are: 0 , 1 , 2 , 3 , 4 // there is NO index number 5
Do đó, chúng tôi không thực sự nói về sự khác biệt hiệu suất giữa <
vs <=
(hoặc 'một lần lặp thêm'), nhưng chúng tôi đang nói:
'tại sao đoạn trích chính xác (b) chạy nhanh hơn đoạn trích sai (a)'?
Câu trả lời là 2 lần (mặc dù theo quan điểm của người triển khai ngôn ngữ ES262, cả hai đều là hình thức tối ưu hóa):
- Biểu diễn dữ liệu: cách biểu diễn / lưu trữ Mảng bên trong trong bộ nhớ (đối tượng, hàm băm, mảng số 'thực', v.v.)
- Mã máy chức năng: cách biên dịch mã truy cập / xử lý (đọc / sửa đổi) các 'Mảng' này
Mục 1 là đủ (và chính xác IMHO) được giải thích bằng câu trả lời được chấp nhận , nhưng chỉ dành 2 từ ('mã') cho Mục 2: biên dịch .
Chính xác hơn: Biên soạn JIT và quan trọng hơn nữa là JIT- RE -Compilation!
Đặc tả ngôn ngữ về cơ bản chỉ là một mô tả về một tập hợp các thuật toán ('các bước cần thực hiện để đạt được kết quả cuối cùng được xác định'). Mà, hóa ra là một cách rất hay để mô tả một ngôn ngữ. Và nó để lại phương pháp thực tế mà một công cụ sử dụng để đạt được kết quả xác định mở cho người thực hiện, tạo cơ hội rộng rãi để đưa ra những cách hiệu quả hơn để tạo ra kết quả được xác định. Một công cụ tuân thủ thông số kỹ thuật sẽ cung cấp kết quả tuân thủ thông số kỹ thuật cho bất kỳ đầu vào được xác định.
Bây giờ, với mã javascript / thư viện / mức sử dụng ngày càng tăng và ghi nhớ bao nhiêu tài nguyên (thời gian / bộ nhớ / vv) mà trình biên dịch 'thực' sử dụng, rõ ràng chúng ta không thể khiến người dùng truy cập trang web chờ lâu (và yêu cầu chúng để có nhiều tài nguyên có sẵn).
Hãy tưởng tượng các chức năng đơn giản sau đây:
function sum(arr){
var r=0, i=0;
for(;i<arr.length;) r+=arr[i++];
return r;
}
Hoàn toàn rõ ràng, phải không? Không yêu cầu làm rõ thêm, phải không? Kiểu trả về là Number
, phải không?
Chà .. không, không & không ... Nó phụ thuộc vào đối số bạn chuyển đến tham số hàm được đặt tên arr
...
sum('abcde'); // String('0abcde')
sum([1,2,3]); // Number(6)
sum([1,,3]); // Number(NaN)
sum(['1',,3]); // String('01undefined3')
sum([1,,'3']); // String('NaN3')
sum([1,2,{valueOf:function(){return this.val}, val:6}]); // Number(9)
var val=5; sum([1,2,{valueOf:function(){return val}}]); // Number(8)
Thấy vấn đề? Sau đó, hãy xem xét điều này chỉ vừa đủ để loại bỏ các hoán vị lớn có thể có ... Chúng ta thậm chí không biết loại LOẠI chức năng TRẢ LẠI cho đến khi chúng ta hoàn thành ...
Bây giờ hãy tưởng tượng chức năng tương tự này- mã thực sự được sử dụng trên các loại khác nhau hoặc thậm chí các biến thể đầu vào, cả hai hoàn toàn theo nghĩa đen (trong mã nguồn) được mô tả và động trong các chương trình được tạo ra 'mảng' ..
Vì vậy, nếu bạn đã biên dịch hàm sum
CHỈ MỘT LẦN, thì cách duy nhất luôn trả về kết quả được xác định cụ thể cho bất kỳ và tất cả các loại đầu vào sau đó, rõ ràng, chỉ bằng cách thực hiện TẤT CẢ các bước chính và phụ được quy định cụ thể mới có thể đảm bảo kết quả tuân thủ quy định cụ thể (giống như một trình duyệt pre-y2k chưa được đặt tên). Không tối ưu hóa (vì không có giả định) và ngôn ngữ kịch bản được giải thích chậm vẫn còn.
JIT-Compilation (JIT như trong Just in Time) là giải pháp phổ biến hiện nay.
Vì vậy, bạn bắt đầu biên dịch hàm bằng các giả định liên quan đến những gì nó làm, trả về và chấp nhận.
bạn đưa ra các kiểm tra đơn giản nhất có thể để phát hiện xem hàm có thể bắt đầu trả về kết quả tuân thủ không theo thông số không (như vì nó nhận được đầu vào không mong muốn). Sau đó, bỏ kết quả đã biên dịch trước đó và biên dịch lại thành một cái gì đó chi tiết hơn, quyết định làm gì với kết quả một phần bạn đã có (có hợp lệ để được tin cậy hoặc tính toán lại để đảm bảo), gắn lại hàm vào chương trình và thử lại. Cuối cùng rơi trở lại để giải thích kịch bản từng bước như trong spec.
Tất cả điều này cần có thời gian!
Tất cả các trình duyệt hoạt động trên công cụ của họ, cho mỗi phiên bản phụ, bạn sẽ thấy mọi thứ được cải thiện và hồi quy. Các chuỗi đã ở một thời điểm nào đó trong lịch sử các chuỗi thực sự bất biến (do đó mảng.join nhanh hơn nối chuỗi), bây giờ chúng tôi sử dụng các dây (hoặc tương tự) để giảm bớt vấn đề. Cả hai đều trả về kết quả phù hợp với thông số kỹ thuật và đó là điều quan trọng!
Câu chuyện dài: chỉ vì ngữ nghĩa của ngôn ngữ javascript thường khiến chúng ta quay lại (như với lỗi im lặng này trong ví dụ của OP) không có nghĩa là những lỗi 'ngu ngốc' làm tăng cơ hội trình biên dịch phát ra mã máy nhanh. Nó giả định rằng chúng tôi đã viết các hướng dẫn chính xác 'thường': câu thần chú hiện tại mà chúng tôi 'người dùng' (của ngôn ngữ lập trình) phải có là: giúp trình biên dịch, mô tả những gì chúng tôi muốn, ủng hộ các thành ngữ phổ biến (lấy gợi ý từ asm.js để hiểu cơ bản những trình duyệt nào có thể cố gắng tối ưu hóa và tại sao).
Bởi vì điều này, nói về hiệu suất là cả quan trọng NHƯNG CSONG là một lĩnh vực mỏ (và vì lĩnh vực mỏ đã nói tôi thực sự muốn kết thúc bằng việc chỉ vào (và trích dẫn) một số tài liệu có liên quan:
Truy cập vào các thuộc tính đối tượng không tồn tại và các phần tử mảng ngoài giới hạn trả về undefined
giá trị thay vì đưa ra một ngoại lệ. Các tính năng động này giúp lập trình trong JavaScript thuận tiện, nhưng chúng cũng gây khó khăn cho việc biên dịch JavaScript thành mã máy hiệu quả.
...
Một tiền đề quan trọng để tối ưu hóa JIT hiệu quả là các lập trình viên sử dụng các tính năng động của JavaScript một cách có hệ thống. Ví dụ, trình biên dịch JIT khai thác thực tế là các thuộc tính đối tượng thường được thêm vào một đối tượng của một kiểu nhất định theo một thứ tự cụ thể hoặc việc truy cập mảng ngoài giới hạn hiếm khi xảy ra. Trình biên dịch JIT khai thác các giả định đều đặn này để tạo mã máy hiệu quả khi chạy. Nếu một khối mã thỏa mãn các giả định, công cụ JavaScript sẽ thực thi mã máy được tạo hiệu quả. Nếu không, công cụ phải quay lại mã chậm hơn hoặc để diễn giải chương trình.
Nguồn:
"JITProf: Xác định mã JavaScript không thân thiện với JIT"
ấn phẩm Berkeley, 2014, bởi Liang Gong, Michael Pradel, Koushik Sen.
http://software-lab.org/publications/jitprof_tr_aug3_2014.pdf
ASM.JS (cũng không thích truy cập mảng bị ràng buộc):
Biên soạn trước thời gian
Vì asm.js là một tập hợp con nghiêm ngặt của JavaScript, đặc tả này chỉ xác định logic xác thực, nên ngữ nghĩa thực thi chỉ đơn giản là của JavaScript. Tuy nhiên, asm.js được xác thực có thể tuân theo việc biên dịch trước (AOT). Hơn nữa, mã được tạo bởi trình biên dịch AOT có thể khá hiệu quả, có tính năng:
- biểu diễn unboxed của số nguyên và số dấu phẩy động;
- không có kiểm tra loại thời gian chạy;
- không có thu gom rác; và
- tải heap hiệu quả và các cửa hàng (với các chiến lược thực hiện khác nhau tùy theo nền tảng).
Mã không xác nhận hợp lệ phải quay trở lại thực thi bằng các phương thức truyền thống, ví dụ: giải thích và / hoặc biên dịch đúng lúc (JIT).
http://asmjs.org/spec/latest/
và cuối cùng https://bloss.windows.com/msedgedev/2015/05/07/browing-asm-js-to-chakra-microsoft-edge/
đã có một phần nhỏ về cải tiến hiệu suất bên trong của động cơ khi xóa giới hạn- kiểm tra (trong khi chỉ cần nâng giới hạn - kiểm tra bên ngoài vòng lặp đã cải thiện 40%).
EDIT:
lưu ý rằng nhiều nguồn nói về các cấp độ khác nhau của JIT-Recompilation để giải thích.
Ví dụ lý thuyết dựa trên thông tin trên, liên quan đến đoạn trích của OP:
- Gọi tới isPrimeDivisible
- Biên dịch isPrimeDivisible bằng các giả định chung (như không truy cập giới hạn)
- Làm việc
- BAM, đột nhiên mảng truy cập ngoài giới hạn (ngay ở cuối).
- Crap, nói là engine, hãy biên dịch lại isPrimeDivisible bằng các giả định khác nhau (ít hơn) và công cụ ví dụ này không cố gắng tìm hiểu xem nó có thể sử dụng lại kết quả một phần hiện tại hay không, vì vậy
- Tính toán lại tất cả các công việc bằng cách sử dụng chức năng chậm hơn (hy vọng nó kết thúc, nếu không lặp lại và lần này chỉ diễn giải mã).
- Kết quả trả về
Do đó thời gian sau đó là:
Lần chạy đầu tiên (thất bại ở cuối) + thực hiện lại tất cả công việc bằng cách sử dụng mã máy chậm hơn cho mỗi lần lặp + biên dịch lại, v.v. rõ ràng mất hơn 2 lần trong ví dụ lý thuyết này !
EDIT 2: (từ chối trách nhiệm: phỏng đoán dựa trên các sự kiện bên dưới)
Càng nghĩ về nó, tôi càng nghĩ rằng câu trả lời này thực sự có thể giải thích lý do chi phối hơn cho 'hình phạt' này đối với đoạn trích sai (hoặc phần thưởng hiệu suất trên đoạn trích b , tùy thuộc vào cách bạn nghĩ về nó), chính xác lý do tại sao tôi lại gọi đó là (đoạn a) một lỗi lập trình:
Thật hấp dẫn khi cho rằng đó this.primes
là một số thuần túy 'mảng dày đặc'
- Chữ được mã hóa cứng trong mã nguồn (ứng cử viên xuất sắc được biết đến để trở thành một mảng 'thực' vì mọi thứ đã được trình biên dịch biết trước thời gian biên dịch) HOẶC
- rất có thể được tạo bằng cách sử dụng hàm số điền vào một kích thước trước (
new Array(/*size value*/)
) theo thứ tự tăng dần (một ứng cử viên đã biết từ lâu khác để trở thành một mảng 'thực').
Chúng tôi cũng biết rằng primes
chiều dài của mảng được lưu trữ dưới dạng prime_count
! (chỉ ra ý định và kích thước cố định).
Chúng tôi cũng biết rằng hầu hết các công cụ ban đầu đều vượt qua Mảng như là sao chép khi sửa đổi (khi cần), điều này giúp xử lý chúng nhanh hơn nhiều (nếu bạn không thay đổi chúng).
Do đó, thật hợp lý khi giả định rằng Array primes
rất có thể đã là một mảng được tối ưu hóa bên trong mà không bị thay đổi sau khi tạo (đơn giản để biết trình biên dịch nếu không có mã sửa đổi mảng sau khi tạo) và do đó đã có (nếu có thể áp dụng cho công cụ) được lưu trữ một cách tối ưu, gần như là một Typed Array
.
Như tôi đã cố gắng làm rõ với sum
ví dụ về hàm của mình , (các) đối số được thông qua ảnh hưởng rất lớn đến những gì thực sự cần phải xảy ra và như vậy cách mã cụ thể đó được biên dịch thành mã máy. Truyền a String
cho sum
hàm không nên thay đổi chuỗi mà thay đổi cách hàm được biên dịch JIT! Truyền một mảng để sum
biên dịch một phiên bản khác (thậm chí có thể bổ sung cho loại này hoặc 'hình dạng' khi họ gọi nó, của đối tượng đã được thông qua) phiên bản mã máy.
Vì có vẻ hơi khó khăn khi chuyển đổi primes
Mảng giống như Typed_Array khi đang bay sang một cái gì đó trong khi trình biên dịch biết chức năng này thậm chí sẽ không sửa đổi nó!
Theo các giả định này để lại 2 tùy chọn:
- Biên dịch dưới dạng cruncher số giả sử không có giới hạn, chạy vào vấn đề ngoài giới hạn ở cuối, biên dịch lại và làm lại công việc (như đã nêu trong ví dụ lý thuyết trong chỉnh sửa 1 ở trên)
- Trình biên dịch đã phát hiện (hoặc nghi ngờ?) Từ phía trước bị ràng buộc và hàm được JIT-Compiled như thể đối số được truyền là một đối tượng thưa thớt dẫn đến mã máy chức năng chậm hơn (vì nó sẽ có nhiều kiểm tra / chuyển đổi / ép buộc hơn Vân vân.). Nói cách khác: hàm không bao giờ có thể đạt được đối với các tối ưu hóa nhất định, nó được biên dịch như thể nó nhận được một đối số 'mảng thưa thớt' (giống như).
Bây giờ tôi thực sự tự hỏi đó là 2 cái nào!
<=
và<
giống hệt nhau, cả về lý thuyết và thực hiện thực tế trong tất cả các bộ xử lý hiện đại (và phiên dịch).