Trong trường hợp thứ hai +
là nhanh hơn bởi vì trong trường hợp đó V8 thực sự di chuyển nó ra khỏi vòng lặp điểm chuẩn - làm cho điểm chuẩn vòng lặp trống.
Điều này xảy ra do một số đặc thù của đường ống tối ưu hóa hiện tại. Nhưng trước khi chúng tôi nhận được các chi tiết đẫm máu, tôi muốn nhắc nhở cách Benchmark.js hoạt động.
Để đo test bạn đã viết nó mất Benchmark.prototype.setup
rằng bạn cũng được cung cấp và các trường hợp thử nghiệm bản thân và tự động tạo ra một chức năng trông khoảng như thế này (tôi bỏ qua một số chi tiết không liên quan):
function (n) {
var start = Date.now();
/* Benchmark.prototype.setup body here */
while (n--) {
/* test body here */
}
return Date.now() - start;
}
Khi chức năng được tạo Benchmark.js gọi nó để đo lường op của bạn cho một số lần lặp nhất định n
. Quá trình này được lặp lại nhiều lần: tạo ra một hàm mới, gọi hàm đó để thu thập một mẫu đo lường. Số lần lặp được điều chỉnh giữa các mẫu để đảm bảo rằng chức năng chạy đủ lâu để cung cấp cho phép đo có ý nghĩa.
Điều quan trọng cần lưu ý ở đây là
- cả trường hợp của bạn và
Benchmark.prototype.setup
là những bản văn inlined;
- có một vòng lặp xung quanh hoạt động bạn muốn đo lường;
Về cơ bản, chúng tôi thảo luận về lý do tại sao đoạn code dưới đây với một biến địa phương x
function f(n) {
var start = Date.now();
var x = "5555"
while (n--) {
var y = +x
}
return Date.now() - start;
}
chạy chậm hơn so với mã với biến toàn cầu x
function g(n) {
var start = Date.now();
x = "5555"
while (n--) {
var y = +x
}
return Date.now() - start;
}
(Lưu ý: trường hợp này được gọi là địa phương biến số trong câu hỏi, nhưng không phải như vậy, x
là global)
Điều gì sẽ xảy ra khi bạn thực hiện các chức năng này với một giá trị đủ lớn là n
, ví dụ f(1e6)
?
Đường ống tối ưu hóa hiện tại triển khai OSR theo cách đặc biệt. Thay vì tạo ra một phiên bản OSR cụ thể của mã được tối ưu hóa và loại bỏ nó sau này, nó tạo ra một phiên bản có thể được sử dụng cho cả OSR và mục nhập bình thường và thậm chí có thể được sử dụng lại nếu chúng ta cần thực hiện OSR tại cùng một vòng lặp. Điều này được thực hiện bằng cách tiêm một khối nhập đặc biệt OSR vào đúng vị trí trong biểu đồ luồng điều khiển.
OSR khối entry được tiêm khi SSA IR cho các chức năng được xây dựng và nó háo hức bản sao tất cả các biến địa phương ra khỏi tình trạng OSR đến. Kết quả là V8 không thấy rằng địa phương x
thực sự là một hằng số và thậm chí mất bất kỳ thông tin về loại hình của nó. Đối với các lần tối ưu hóa tiếp theo, x2
có vẻ như nó có thể là bất cứ thứ gì.
Như x2
có thể là bất kỳ biểu thức nào +x2
cũng có thể có các tác dụng phụ tùy ý (ví dụ: nó có thể là đối tượng có valueOf
được gắn với nó). Điều này ngăn chặn chuyển động mã biến đổi vòng lặp bất biến khi di chuyển +x2
ra khỏi vòng lặp.
Tại sao g
nhanh hơn? V8 kéo một thủ thuật ở đây. Nó theo dõi các biến toàn cục có chứa hằng số: ví dụ: trong tiêu chuẩn này, toàn cầu x
luôn chứa "5555"
vì vậy V8 chỉ thay thế truy cập x
với giá trị của nó và đánh dấu mã được tối ưu hóa này phụ thuộc vào giá trị của x
. Nếu ai đó thay thế x
giá trị với một cái gì đó khác với tất cả các mã phụ thuộc sẽ được deoptimized. Các biến toàn cầu cũng không phải là một phần của trạng thái OSR và không tham gia vào việc đổi tên SSA để V8 không bị nhầm lẫn bởi các hàm "hàm" tích hợp OSR và trạng thái nhập bình thường.Đó là lý do tại sao khi V8 tối ưu hóa g
nó kết thúc lên tạo IR sau trong thân vòng lặp (sọc đỏ trên bên trái cho thấy vòng lặp):
Lưu ý: +x
được biên soạn để x * 1
, nhưng điều này chỉ là một là chi tiết triển khai.
Sau đó LICM sẽ thực hiện thao tác này và di chuyển nó ra khỏi vòng lặp mà không có gì quan tâm trong chính vòng lặp đó. Điều này trở thành có thể vì bây giờ V8 biết rằng cả hai toán hạng của *
đều là nguyên thủy - vì vậy có thể có không có tác dụng phụ.
Và đó là lý do tại sao g
là nhanh hơn, vì vòng lặp trống là khá rõ ràng là nhanh hơn so với một không trống. Điều này cũng có nghĩa là phiên bản thứ hai của điểm chuẩn không thực sự đo lường những gì bạn muốn nó đo lường, và trong khi phiên bản đầu tiên đã thực sự nắm bắt một số khác biệt giữa hiệu suất parseInt(x)
và +x
được nhiều may mắn hơn: bạn nhấn một hạn chế trong đường ống tối ưu hóa hiện tại của V8 (Trục khuỷu) ngăn không cho nó ăn toàn bộ microbenchmark.
Bạn sẽ nhận được phản hồi tốt hơn ở đây nếu mã lõi ở đây, thay vì được giới thiệu bởi một liên kết (tôi đã cung cấp cho bạn một BTW +1, vì đây là một câu hỏi thú vị). – rfornal
Một câu hỏi thú vị, nhưng đó là nhiều hơn về jsperf và các trường hợp thử nghiệm hơn về 'parseInt' so với' + '. Tôi nghĩ tiêu đề của bạn hơi gây hiểu lầm. –
Tôi đã chỉnh sửa câu hỏi. Tôi hy vọng bây giờ nó tốt hơn –