Nhà phát triển V8 tại đây. Với số lượng quan tâm trong câu hỏi này và thiếu câu trả lời khác, tôi có thể đưa ra câu hỏi này; Tôi sợ rằng nó sẽ không phải là câu trả lời mà bạn đã hy vọng mặc dù.
Có một bộ hướng dẫn nào đó về cách lập trình trong khi ở trong thế giới của các mảng SMI được đóng gói (ví dụ) không?
Câu trả lời ngắn: nó ở ngay đây : const guidelines = ["keep your integers small enough"]
.
Câu trả lời dài hơn: đưa ra một bộ hướng dẫn toàn diện là khó khăn vì nhiều lý do. Nói chung, ý kiến của chúng tôi là các nhà phát triển JavaScript nên viết mã có ý nghĩa với họ và trường hợp sử dụng của họ và các nhà phát triển công cụ JavaScript nên tìm ra cách chạy mã đó nhanh trên các công cụ của họ. Mặt khác, rõ ràng có một số hạn chế đối với lý tưởng đó, theo nghĩa là một số mẫu mã hóa sẽ luôn có chi phí hiệu năng cao hơn các loại khác, bất kể lựa chọn thực hiện động cơ và nỗ lực tối ưu hóa.
Khi chúng tôi nói về lời khuyên về hiệu suất, chúng tôi cố gắng ghi nhớ điều đó và ước tính cẩn thận những đề xuất nào có khả năng cao còn hiệu lực trên nhiều động cơ và trong nhiều năm, và cũng khá hợp lý / không xâm phạm.
Quay trở lại ví dụ trong tay: sử dụng Smis trong nội bộ được coi là một chi tiết triển khai mà mã người dùng không cần phải biết. Nó sẽ làm cho một số trường hợp hiệu quả hơn, và không nên làm tổn thương trong các trường hợp khác. Không phải tất cả các công cụ đều sử dụng Smis (ví dụ, AFAIK Firefox / Spidermonkey trong lịch sử không có; tôi đã nghe nói rằng đối với một số trường hợp họ sử dụng Smis ngày nay; nhưng tôi không biết bất kỳ chi tiết nào và không thể nói với bất kỳ cơ quan nào về vấn đề). Trong V8, kích thước của Smis là một chi tiết bên trong và thực sự đã thay đổi theo thời gian và qua các phiên bản. Trên các nền tảng 32 bit, từng là trường hợp sử dụng đa số, Smis luôn là số nguyên có chữ ký 31 bit; trên nền tảng 64 bit, chúng từng là số nguyên có chữ ký 32 bit, gần đây có vẻ như là trường hợp phổ biến nhất, cho đến khi trong Chrome 80, chúng tôi đã chuyển "nén con trỏ" đối với kiến trúc 64 bit, yêu cầu giảm kích thước Smi xuống 31 bit được biết đến từ các nền tảng 32 bit. Nếu bạn tình cờ dựa trên một triển khai dựa trên giả định rằng Smis thường là 32 bit, bạn sẽ gặp những tình huống không may nhưnày .
Rất may, như bạn đã lưu ý, mảng kép vẫn rất nhanh. Đối với mã nặng về số, có thể có ý nghĩa khi giả định / nhắm mục tiêu mảng kép. Với tỷ lệ tăng gấp đôi trong JavaScript, thật hợp lý khi giả định rằng tất cả các công cụ đều hỗ trợ tốt cho các mảng kép và kép.
Có thể thực hiện lập trình hiệu suất cao chung trong Javascript mà không cần sử dụng một cái gì đó như hệ thống macro để nội tuyến những thứ như vec.add () vào các cuộc gọi?
"Chung" thường mâu thuẫn với "hiệu suất cao". Điều này không liên quan đến JavaScript hoặc các triển khai công cụ cụ thể.
Mã "chung" có nghĩa là các quyết định phải được đưa ra trong thời gian chạy. Mỗi khi bạn thực thi một hàm, mã phải chạy để xác định, "có phải là x
số nguyên không? Nếu vậy, hãy lấy đường dẫn mã đó. Có phải là x
một chuỗi không? Sau đó nhảy qua đây. Nó có phải là một đối tượng không .valueOf
? có lẽ .toString()
? Có lẽ trên chuỗi nguyên mẫu của nó? Gọi đó và khởi động lại từ đầu với kết quả của nó ". Mã tối ưu hóa "hiệu suất cao" về cơ bản được xây dựng trên ý tưởng để loại bỏ tất cả các kiểm tra động này; Điều đó chỉ có thể khi công cụ / trình biên dịch có một số cách để suy ra các loại trước thời hạn: nếu nó có thể chứng minh (hoặc giả sử với xác suất đủ cao) x
luôn luôn là một số nguyên, thì nó chỉ cần tạo mã cho trường hợp đó ( được bảo vệ bởi một kiểm tra loại nếu các giả định chưa được chứng minh có liên quan).
Nội tuyến là trực giao cho tất cả điều này. Hàm "chung" vẫn có thể được nội tuyến. Trong một số trường hợp, trình biên dịch có thể truyền thông tin kiểu vào hàm được nội tuyến để giảm tính đa hình ở đó.
(Để so sánh: C ++, là ngôn ngữ được biên dịch tĩnh, có các mẫu để giải quyết vấn đề liên quan. Tóm lại, họ để lập trình viên chỉ thị rõ ràng cho trình biên dịch tạo các bản sao chuyên biệt của các hàm (hoặc toàn bộ các lớp), được tham số hóa trên các loại đã cho. Giải pháp tốt cho một số trường hợp, nhưng không phải không có nhược điểm riêng, ví dụ thời gian biên dịch dài và nhị phân lớn. JavaScript, tất nhiên, không có thứ gì giống như mẫu. Bạn có thể sử dụng eval
để xây dựng một hệ thống có phần giống nhau, nhưng sau đó bạn Bạn sẽ gặp phải những hạn chế tương tự: bạn phải thực hiện tương đương với công việc của trình biên dịch C ++ khi chạy và bạn phải lo lắng về số lượng mã bạn đang tạo.)
Làm thế nào để một mô-đun mã hiệu suất cao thành các libaries trong bối cảnh của những thứ như các trang web cuộc gọi siêu mô hình và deoptimisations? Chẳng hạn, nếu tôi vui vẻ sử dụng gói Đại số tuyến tính A ở tốc độ cao, sau đó tôi nhập gói B phụ thuộc vào A, nhưng B gọi nó với các loại khác và phát hiện ra nó, đột nhiên (không thay đổi mã của tôi) mã của tôi chạy chậm hơn .
Vâng, đó là một vấn đề chung với JavaScript. V8 được sử dụng để triển khai một số nội dung nhất định (giống như Array.sort
) trong JavaScript và vấn đề này (mà chúng tôi gọi là "ô nhiễm phản hồi kiểu") là một trong những lý do chính khiến chúng tôi hoàn toàn tránh xa kỹ thuật đó.
Điều đó nói rằng, đối với mã số, không có nhiều loại (chỉ có Smis và nhân đôi) và như bạn lưu ý rằng chúng nên có hiệu suất tương tự trong thực tế, vì vậy trong khi ô nhiễm phản hồi thực sự là mối quan tâm về mặt lý thuyết, và trong một số trường hợp có thể có tác động đáng kể, cũng có khả năng là trong các kịch bản đại số tuyến tính, bạn sẽ không thấy sự khác biệt có thể đo lường được.
Ngoài ra, bên trong động cơ còn có nhiều tình huống hơn "một loại == nhanh" và "nhiều hơn một loại == chậm". Nếu một thao tác nhất định đã thấy cả Smis và nhân đôi, điều đó hoàn toàn tốt. Tải các phần tử từ hai loại mảng cũng tốt. Chúng tôi sử dụng thuật ngữ "megamorphic" cho tình huống khi tải đã thấy rất nhiều loại khác nhau mà từ bỏ theo dõi chúng riêng lẻ và thay vào đó sử dụng một cơ chế chung hơn để chia tỷ lệ tốt hơn cho số lượng lớn các loại - một chức năng chứa các tải như vậy có thể vẫn được tối ưu hóa. "Deoptimization" là hành động rất cụ thể của việc phải loại bỏ mã được tối ưu hóa cho một hàm vì một loại mới được nhìn thấy chưa từng thấy trước đó và do đó mã được tối ưu hóa không được trang bị để xử lý. Nhưng ngay cả điều đó cũng tốt: chỉ cần quay lại mã chưa được tối ưu hóa để thu thập thêm phản hồi loại và tối ưu hóa lại sau. Nếu điều này xảy ra một vài lần, thì không có gì phải lo lắng; nó chỉ trở thành một vấn đề trong các trường hợp xấu về mặt bệnh lý.
Vì vậy, tóm tắt của tất cả đó là: đừng lo lắng về nó . Chỉ cần viết mã hợp lý, để cho động cơ đối phó với nó. Và theo "hợp lý", ý tôi là: điều gì hợp lý cho trường hợp sử dụng của bạn, có thể đọc được, có thể bảo trì, sử dụng thuật toán hiệu quả, không chứa các lỗi như đọc vượt quá độ dài của mảng. Lý tưởng nhất, đó là tất cả những gì có, và bạn không cần phải làm gì khác. Nếu việc này khiến bạn cảm thấy tốt hơn khi làm điều gì đó và / hoặc nếu bạn thực sự quan sát các vấn đề về hiệu suất, tôi có thể đưa ra hai ý tưởng:
Sử dụng TypeScript có thể giúp đỡ. Cảnh báo lớn về chất béo: Các loại của TypeScript nhắm đến năng suất của nhà phát triển, chứ không phải hiệu suất thực thi (và hóa ra, hai quan điểm đó có các yêu cầu rất khác nhau từ một hệ thống loại). Điều đó nói rằng, có một số trùng lặp: ví dụ: nếu bạn luôn chú thích mọi thứ như number
, thì trình biên dịch TS sẽ cảnh báo bạn nếu bạn vô tình đưa null
vào một mảng hoặc hàm được cho là chỉ chứa / hoạt động trên các số. Tất nhiên, kỷ luật vẫn được yêu cầu: một number_func(random_object as number)
lối thoát duy nhất có thể âm thầm phá hoại mọi thứ, bởi vì tính chính xác của các chú thích loại không được thi hành ở bất cứ đâu.
Sử dụng TypedArrays cũng có thể giúp đỡ. Chúng có chi phí hoạt động cao hơn một chút (mức tiêu thụ bộ nhớ và tốc độ phân bổ) cho mỗi mảng so với mảng JavaScript thông thường (vì vậy nếu bạn cần nhiều mảng nhỏ, thì mảng thông thường có thể hiệu quả hơn) và chúng kém linh hoạt hơn vì chúng không thể phát triển hoặc thu hẹp sau khi phân bổ, nhưng chúng đảm bảo rằng tất cả các yếu tố có chính xác một loại.
Có công cụ đo lường nào dễ sử dụng để kiểm tra xem công cụ Javascript đang làm gì với các loại không?
Không, và đó là cố ý. Như đã giải thích ở trên, chúng tôi không muốn bạn điều chỉnh cụ thể mã của mình theo bất kỳ mẫu nào V8 có thể tối ưu hóa đặc biệt tốt hiện nay và chúng tôi không tin rằng bạn thực sự muốn làm điều đó. Tập hợp các thứ đó có thể thay đổi theo một trong hai hướng: nếu có một mẫu bạn muốn sử dụng, chúng tôi có thể tối ưu hóa cho phiên bản đó trong tương lai (trước đây chúng tôi đã từng có ý tưởng lưu trữ các số nguyên 32 bit không được lưu trữ dưới dạng các phần tử mảng .. . nhưng làm việc trên đó chưa bắt đầu, vì vậy không có lời hứa); và đôi khi nếu có một mẫu mà chúng ta đã sử dụng để tối ưu hóa trong quá khứ, chúng ta có thể quyết định bỏ nó nếu nó theo cách tối ưu hóa khác, quan trọng hơn / có tác động hơn. Ngoài ra, những thứ như heuristic nội tuyến rất khó để có được đúng, do đó, đưa ra quyết định nội tuyến đúng vào đúng thời điểm là một lĩnh vực nghiên cứu đang diễn ra và những thay đổi tương ứng đối với hành vi của công cụ / trình biên dịch; Điều này làm cho một trường hợp khác mà nó sẽ không may cho tất cả mọi người (bạnvà chúng tôi) nếu bạn dành nhiều thời gian để chỉnh sửa mã của mình cho đến khi một số phiên bản trình duyệt hiện tại thực hiện xấp xỉ các quyết định nội tuyến mà bạn nghĩ (hoặc biết?) là tốt nhất, chỉ quay lại nửa năm sau để nhận ra rằng các trình duyệt hiện tại đã thay đổi heuristic của họ.
Tất nhiên, bạn luôn có thể đo lường hiệu suất của toàn bộ ứng dụng của mình - đó là điều quan trọng cuối cùng, không phải là lựa chọn cụ thể nào mà động cơ thực hiện trong nội bộ. Coi chừng các dấu hiệu vi mô, vì chúng gây hiểu nhầm: nếu bạn chỉ trích xuất hai dòng mã và điểm chuẩn, thì có khả năng kịch bản sẽ đủ khác nhau (ví dụ: phản hồi loại khác nhau) mà động cơ sẽ đưa ra các quyết định rất khác nhau.