Tại sao <= chậm hơn <sử dụng đoạn mã này trong V8?


166

Tôi đang đọc các slide Phá vỡ giới hạn tốc độ Javascript bằng V8 và có một ví dụ như mã bên dưới. Tôi không thể hiểu tại sao <=chậm hơn <trong trường hợp này, có ai có thể giải thích điều đó không? Bất kỳ ý kiến ​​được đánh giá cao.

Chậm:

this.isPrimeDivisible = function(candidate) {
    for (var i = 1; i <= this.prime_count; ++i) {
        if (candidate % this.primes[i] == 0) return true;
    }
    return false;
} 

(Gợi ý: số nguyên tố là một mảng có độ dài Prime_count)

Nhanh hơn:

this.isPrimeDivisible = function(candidate) {
    for (var i = 1; i < this.prime_count; ++i) {
        if (candidate % this.primes[i] == 0) return true;
    }
    return false;
} 

[Thông tin thêm] cải thiện tốc độ là đáng kể, trong thử nghiệm môi trường địa phương của tôi, kết quả như sau:

V8 version 7.3.0 (candidate) 

Chậm:

 time d8 prime.js
 287107
 12.71 user 
 0.05 system 
 0:12.84 elapsed 

Nhanh hơn:

time d8 prime.js
287107
1.82 user 
0.01 system 
0:01.84 elapsed

10
@DacreDenny Khó khăn tính toán của <=<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).
TypeIA

1
Tôi đã đọc tài liệu, có một mainmã gọi hàm đó trong một vòng lặp chạy 25000thời gian, vì vậy bạn đang thực hiện rất nhiều lần lặp ít hơn để thực hiện thay đổi đó. Ngoài ra, nếu một mảng có chiều dài bằng 5, cố gắng đạt được array[5]sẽ vượt ra ngoài giới hạn của anh ta, đưa ra một undefinedgiá trị vì các mảng bắt đầu lập chỉ mục 0.
Shidersz

1
Sẽ rất hữu ích nếu câu hỏi này giải thích mức độ cải thiện tốc độ đạt được (ví dụ, nhanh hơn 5 lần) để mọi người không bị loại bỏ bởi phép lặp thêm. Tôi đã cố gắng tìm kiếm nhanh như thế nào trong các slide nhưng có rất nhiều và tôi gặp khó khăn khi tìm nó, nếu không tôi sẽ tự chỉnh sửa nó.
Thuyền trưởng Man

@CaptainMan Bạn nói đúng, cải thiện tốc độ chính xác rất khó để lượm lặt từ các slide vì chúng bao gồm nhiều vấn đề khác nhau cùng một lúc. Nhưng trong cuộc trò chuyện của tôi với diễn giả sau cuộc nói chuyện này, anh ấy đã xác nhận rằng đó không chỉ là một phần trăm nhỏ như bạn mong đợi từ một lần lặp thêm trong trường hợp thử nghiệm này, mà là một sự khác biệt lớn: nhanh hơn nhiều lần, có thể là một đơn đặt hàng độ lớn trở lên. Và lý do cho điều đó là V8 rơi trở lại (hoặc giảm trở lại trong những ngày đó) về định dạng mảng không được tối ưu hóa khi bạn cố gắng đọc bên ngoài giới hạn mảng.
Michael Geary

3
Có thể hữu ích khi so sánh một phiên bản sử dụng <=nhưng hành động giống hệt với <phiên bản bằng cách thực hiện i <= this.prime_count - 1. Điều này giải quyết cả vấn đề "lặp lại thêm" và vấn đề "một lần qua cuối mảng".
TheHansinator

Câu trả lời:


132

Tôi làm việc trên V8 tại Google và muốn cung cấp một số thông tin chi tiết bổ sung về các câu trả lời và nhận xét hiện có.

Để tham khảo, đây là ví dụ mã đầy đủ từ các slide :

var iterations = 25000;

function Primes() {
  this.prime_count = 0;
  this.primes = new Array(iterations);
  this.getPrimeCount = function() { return this.prime_count; }
  this.getPrime = function(i) { return this.primes[i]; }
  this.addPrime = function(i) {
    this.primes[this.prime_count++] = i;
  }
  this.isPrimeDivisible = function(candidate) {
    for (var i = 1; i <= this.prime_count; ++i) {
      if ((candidate % this.primes[i]) == 0) return true;
    }
    return false;
  }
};

function main() {
  var p = new Primes();
  var c = 1;
  while (p.getPrimeCount() < iterations) {
    if (!p.isPrimeDivisible(c)) {
      p.addPrime(c);
    }
    c++;
  }
  console.log(p.getPrime(p.getPrimeCount() - 1));
}

main();

Đầu tiên và quan trọng nhất, sự khác biệt hiệu suất không liên quan gì đến trực tiếp <và các <=nhà khai thác. Vì vậy, vui lòng không nhảy qua các vòng chỉ để tránh <=trong mã của bạn vì bạn đã đọc trên Stack Overflow rằng nó chậm --- không phải vậy!


Thứ hai, mọi người chỉ ra rằng mảng là "holey". Điều này không rõ ràng từ đoạn mã trong bài đăng của OP, nhưng rõ ràng khi bạn nhìn vào mã khởi tạo this.primes:

this.primes = new Array(iterations);

Điều này dẫn đến một mảng với một HOLEYloại phần tử trong V8, ngay cả khi mảng kết thúc hoàn toàn được đóng gói / đóng gói / tiếp giáp. Nói chung, các hoạt động trên mảng holey chậm hơn so với hoạt động trên các mảng được đóng gói, nhưng trong trường hợp này, sự khác biệt là không đáng kể: nó kiểm tra thêm 1 lần kiểm tra Smi ( số nguyên nhỏ ) (để bảo vệ chống lại các lỗ) mỗi lần chúng tôi chạm this.primes[i]vào vòng lặp isPrimeDivisible. Không sao đâu!

TL; DR Mảng HOLEYkhông phải là vấn đề ở đây.


Những người khác chỉ ra rằng mã đọc ra ngoài giới hạn. Nói chung, nên tránh đọc vượt quá độ dài của mảng và trong trường hợp này thực sự sẽ tránh được hiệu suất giảm mạnh. Nhưng tại sao mặc dù? V8 có thể xử lý một số tình huống ngoài luồng này chỉ với một tác động hiệu suất nhỏ. Vậy thì có gì đặc biệt về trường hợp đặc biệt này vậy?

Các out-of-bounds đọc kết quả trong this.primes[i]undefinedtrên dòng này:

if ((candidate % this.primes[i]) == 0) return true;

Và điều đó đưa chúng ta đến vấn đề thực sự : %toán tử hiện đang được sử dụng với các toán hạng không nguyên!

  • integer % someOtherIntegercó thể được tính toán rất hiệu quả; Công cụ JavaScript có thể tạo mã máy được tối ưu hóa cao cho trường hợp này.

  • integer % undefinedmặt khác, một cách kém hiệu quả hơn Float64Mod, vì undefinedđược biểu diễn dưới dạng gấp đôi.

Đoạn mã thực sự có thể được cải thiện bằng cách thay đổi <=thành <trên dòng này:

for (var i = 1; i <= this.prime_count; ++i) {

... không phải vì <=bằng cách nào đó là một nhà điều hành vượt trội hơn <, mà chỉ vì điều này tránh được giới hạn đọc trong trường hợp cụ thể này.


1
Bình luận không dành cho thảo luận mở rộng; cuộc trò chuyện này đã được chuyển sang trò chuyện .
Samuel Liew

1
Để hoàn thành 100%, IC tải có khóa cho this.primes [i] trong isPrimeDivisible bất ngờ chuyển thành megamorphic trong V8. Điều đó có vẻ giống như một lỗi: bug.chromium.org/p/v8/issues/detail?id=8561
Mathias Bynens

226

Các câu trả lời và nhận xét khác đề cập rằng sự khác biệt giữa hai vòng lặp là vòng thứ nhất thực hiện một lần lặp nhiều hơn vòng lặp thứ hai. Điều này là đúng, nhưng trong một mảng tăng lên 25.000 phần tử, một lần lặp ít nhiều sẽ chỉ tạo ra sự khác biệt rất nhỏ. Theo dự đoán của sân bóng, nếu chúng ta giả sử chiều dài trung bình khi nó tăng lên là 12.500, thì mức chênh lệch chúng ta có thể mong đợi sẽ vào khoảng 1 / 12.500, hoặc chỉ 0,008%.

Sự khác biệt hiệu suất ở đây lớn hơn nhiều so với giải thích bằng một lần lặp thêm đó và vấn đề được giải thích ở gần cuối bài thuyết trình.

this.primes là một mảng liền kề (mọi phần tử giữ một giá trị) và các phần tử là tất cả các số.

Một công cụ JavaScript có thể tối ưu hóa một mảng như vậy thành một mảng đơn giản của các số thực, thay vì một mảng các đối tượng xảy ra để chứa các số nhưng có thể chứa các giá trị khác hoặc không có giá trị. Định dạng đầu tiên truy cập nhanh hơn nhiều: mất ít mã hơn và mảng nhỏ hơn nhiều nên sẽ phù hợp hơn trong bộ đệm. Nhưng có một số điều kiện có thể ngăn định dạng tối ưu hóa này được sử dụng.

Một điều kiện sẽ là nếu một số phần tử mảng bị thiếu. Ví dụ:

let array = [];
a[0] = 10;
a[2] = 20;

Bây giờ giá trị của là a[1]gì? Nó không có giá trị . (Thậm chí không đúng khi nói nó có giá trị undefined- một phần tử mảng chứa undefinedgiá trị khác với phần tử mảng bị thiếu hoàn toàn.)

Không có cách nào để biểu thị điều này chỉ bằng số, vì vậy công cụ JavaScript buộc phải sử dụng định dạng ít tối ưu hóa hơn. Nếu a[1]chứa một giá trị số như hai phần tử khác, mảng có khả năng chỉ được tối ưu hóa thành một mảng số.

Một lý do khác để một mảng bị buộc vào định dạng đã được tối ưu hóa có thể là nếu bạn cố gắng truy cập một phần tử bên ngoài giới hạn của mảng, như được thảo luận trong phần trình bày.

Vòng lặp đầu tiên với <=nỗ lực đọc một phần tử qua cuối mảng. Thuật toán vẫn hoạt động chính xác, bởi vì trong lần lặp thêm cuối cùng:

  • this.primes[i]đánh giá undefinedvì đã iqua cuối mảng.
  • candidate % undefined(cho bất kỳ giá trị nào candidate) đánh giá NaN.
  • NaN == 0đánh giá để false.
  • Do đó, return truekhông được thực hiện.

Vì vậy, nó như thể việc lặp lại thêm không bao giờ xảy ra - nó không có tác dụng đối với phần còn lại của logic. Mã tạo ra kết quả giống như nó sẽ không có phép lặp thêm.

Nhưng để đạt được điều đó, nó đã cố đọc một phần tử không tồn tại qua phần cuối của mảng. Điều này buộc mảng không được tối ưu hóa - hoặc ít nhất là đã làm tại thời điểm nói chuyện này.

Vòng lặp thứ hai <chỉ đọc các phần tử tồn tại trong mảng, vì vậy nó cho phép một mảng và mã được tối ưu hóa.

Vấn đề được mô tả trong các trang 90-91 của bài nói chuyện, với các cuộc thảo luận liên quan trong các trang trước và sau đó.

Tôi tình cờ tham dự buổi thuyết trình I / O rất Google này và nói chuyện với diễn giả (một trong những tác giả V8) sau đó. Tôi đã sử dụng một kỹ thuật trong mã riêng của mình liên quan đến việc đọc qua phần cuối của một mảng như một nỗ lực sai lầm (trong nhận thức muộn) để tối ưu hóa một tình huống cụ thể. Ông xác nhận rằng nếu bạn cố gắng đọc hết phần cuối của một mảng, nó sẽ ngăn định dạng tối ưu hóa đơn giản được sử dụng.

Nếu những gì tác giả V8 nói vẫn đúng, thì việc đọc qua phần cuối của mảng sẽ khiến nó không được tối ưu hóa và nó sẽ phải quay lại định dạng chậm hơn.

Bây giờ, có thể V8 đã được cải thiện trong thời gian này để xử lý hiệu quả trường hợp này hoặc các công cụ JavaScript khác xử lý khác. Tôi không biết cách này hay cách khác về vấn đề đó, nhưng sự mở rộng này là những gì bài thuyết trình đã nói.


1
Tôi khá chắc chắn rằng mảng vẫn còn liền kề - không có lý do gì để thay đổi bố cục bộ nhớ. Tuy nhiên, vấn đề quan trọng là kiểm tra ngoài chỉ mục trong truy cập thuộc tính không thể được tối ưu hóa và mã đôi khi được cung cấp undefinedthay vì một số dẫn đến một phép tính khác.
Bergi

1
@Bergi Tôi không phải là chuyên gia về JS / V8, nhưng các đối tượng trong các ngôn ngữ GC hầu như luôn được tham chiếu đến các đối tượng thực tế. Các đối tượng thực tế đó có phân bổ độc lập, ngay cả khi các tham chiếu gần nhau, bởi vì tuổi thọ của đối tượng GC không được gắn với nhau. Trình tối ưu hóa có thể đóng gói các phân bổ độc lập đó thành liền kề, nhưng (a) bộ nhớ sử dụng skyrockets và (b) bạn có hai khối liền kề được lặp lại (các tham chiếu và dữ liệu được đề cập) thay vì một. Tôi cho rằng một trình tối ưu hóa điên rồ có thể xen kẽ các tham chiếu và dữ liệu được đề cập và có một mảng sở hữu các sọc bộ nhớ ...
Yakk - Adam Nevraumont 6/12/18

1
@Bergi Mảng vẫn có thể tiếp giáp trong trường hợp không được tối ưu hóa, nhưng các phần tử mảng không cùng loại như trong trường hợp được tối ưu hóa. Phiên bản tối ưu hóa là một dãy số đơn giản không có lông tơ bổ sung. Phiên bản không được tối ưu hóa là một mảng các đối tượng (định dạng đối tượng bên trong, không phải JavaScript Object), vì nó phải hỗ trợ bất kỳ kết hợp các loại dữ liệu nào trong mảng. Như tôi đã đề cập ở trên, mã trong vòng lặp được cung cấp undefinedkhông ảnh hưởng đến tính chính xác của thuật toán - nó hoàn toàn không thay đổi phép tính (như thể việc lặp lại thêm không bao giờ xảy ra).
Michael Geary

3
@Bergi Tác giả V8, người đã nói chuyện này nói rằng việc đọc cố gắng bên ngoài giới hạn mảng khiến cho mảng được xử lý như thể nó có một loại hỗn hợp: thay vì định dạng chỉ được tối ưu hóa số, nó tối ưu hóa mảng trở lại định dạng chung. Trong trường hợp được tối ưu hóa, đó là một dãy số đơn giản như bạn có thể sử dụng trong chương trình C. Trong trường hợp không tối ưu hóa, nó là một mảng các Valueđối tượng có thể chứa các tham chiếu đến các giá trị thuộc bất kỳ loại nào. (Tôi đã tạo nên tên Value, nhưng vấn đề là các phần tử mảng không chỉ là các số đơn giản mà là các đối tượng bao bọc các số hoặc các loại khác.)
Michael Geary

3
Tôi làm việc trên V8. Mảng trong câu hỏi sẽ được đánh dấu là HOLEYvì nó được tạo bằng cách sử dụng new Array(n)(mặc dù phần mã này không hiển thị trong OP). HOLEYmảng vẫn HOLEYtồn tại mãi mãi trong V8 , ngay cả khi chúng được lấp đầy sau đó. Điều đó nói rằng, mảng là holey không phải là lý do cho vấn đề hoàn hảo trong trường hợp này; điều đó chỉ có nghĩa là chúng ta phải thực hiện một kiểm tra Smi bổ sung trên mỗi lần lặp (để bảo vệ chống lại các lỗ hổng), điều này không phải là vấn đề lớn.
Mathias Bynens

19

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ỉ 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):

  1. 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.)
  2. 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- 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ề undefinedgiá 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.primeslà 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 primeschiề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 primesrấ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 sumví 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 Stringcho sumhà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 để sumbiê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 primesMả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:

  1. 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)
  2. 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!


2
Một cuộc thảo luận tốt về một số vấn đề cơ bản - tuy nhiên bạn hầu như không giải thích câu trả lời nào cả (trong câu cuối cùng). Có thể thêm một tl; dr vào đầu trang? ví dụ: "Vòng lặp chậm hơn là do vượt quá giới hạn, điều này buộc động cơ phải đánh giá lại vòng lặp mà không tối ưu hóa. Đọc tiếp để tìm hiểu lý do tại sao."
brichin

@brichins: cảm ơn, và cảm ơn những gợi ý, mà tôi đã reworded một chút trong bối cảnh thêm chỉnh sửa thứ hai của tôi, bởi vì hơn bây giờ tôi nghĩ về nó, rằng tuyên bố trên dường như thực sự chính xác cũng
GitaarLAB

6

Để thêm tính khoa học cho nó, đây là một jsperf

https://jsperf.com/ints-values-in-out-of-array-bound

Nó kiểm tra trường hợp điều khiển của một mảng chứa đầy số nguyên và lặp thực hiện số học mô-đun trong khi vẫn ở trong giới hạn. Nó có 5 trường hợp thử nghiệm:

  • 1. Vòng ra khỏi giới hạn
  • 2. mảng Holey
  • 3. Số học mô đun chống lại NaN
  • 4. Các giá trị hoàn toàn không xác định
  • 5. Sử dụng một new Array()

Nó cho thấy rằng 4 trường hợp đầu tiên thực sự xấu cho hiệu suất. Vòng ra khỏi giới hạn tốt hơn một chút so với 3 người còn lại, nhưng cả 4 đều chậm hơn khoảng 98% so với trường hợp tốt nhất.
Các new Array()trường hợp là gần như tốt như mảng thô, chỉ chậm hơn một vài phần trăm.

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.