Cập nhật (ngày 2 tháng 3 năm 2020)
Hóa ra, mã hóa trong ví dụ của tôi ở đây được cấu trúc theo đúng cách để rơi ra khỏi một vách đá hiệu suất đã biết trong công cụ JavaScript V8 ...
Xem các cuộc thảo luận trên bug.chromium.org để biết chi tiết. Lỗi này hiện đang được xử lý và sẽ được sửa trong tương lai gần.
Cập nhật (ngày 9 tháng 1 năm 2020)
Tôi đã cố gắng cô lập mã hóa hoạt động theo cách được mô tả dưới đây vào một ứng dụng Web duy nhất, nhưng khi làm như vậy, hành vi đó đã biến mất (??). Tuy nhiên, hành vi được mô tả dưới đây vẫn tồn tại trong bối cảnh của ứng dụng đầy đủ.
Điều đó nói rằng, tôi đã tối ưu hóa mã hóa tính toán fractal và vấn đề này không còn là vấn đề trong phiên bản trực tiếp. Nếu bất cứ ai quan tâm, mô-đun JavaScript biểu hiện vấn đề này vẫn có sẵn ở đây
Tổng quat
Tôi vừa hoàn thành một ứng dụng dựa trên Web nhỏ để so sánh hiệu suất của JavaScript dựa trên trình duyệt với Web hội. Ứng dụng này tính toán một hình ảnh Mandelbrot Set, sau đó khi bạn di chuyển con trỏ chuột lên hình ảnh đó, Julia Set tương ứng được tính toán động và thời gian tính toán được hiển thị.
Bạn có thể chuyển đổi giữa sử dụng JavaScript (nhấn 'j') hoặc WebAssugging (nhấn 'w') để thực hiện tính toán và so sánh thời gian chạy.
Nhấn vào đây để xem ứng dụng làm việc
Tuy nhiên, khi viết mã này, tôi phát hiện ra một số hành vi hiệu suất JavaScript lạ bất ngờ ...
Tóm tắt sự cố
Vấn đề này dường như là cụ thể đối với công cụ JavaScript V8 được sử dụng trong Chrome và Brave. Vấn đề này không xuất hiện trong các trình duyệt sử dụng SpiderMonkey (Firefox) hoặc JavaScriptCore (Safari). Tôi chưa thể kiểm tra điều này trong trình duyệt bằng công cụ Chakra
Tất cả mã JavaScript cho ứng dụng Web này đã được viết dưới dạng Mô-đun ES6
Tôi đã thử viết lại tất cả các hàm bằng
function
cú pháp truyền thống thay vì cú pháp mũi tên ES6 mới. Thật không may, điều này không làm cho bất kỳ sự khác biệt đáng kể
Vấn đề về hiệu năng dường như liên quan đến phạm vi mà hàm JavaScript được tạo. Trong ứng dụng này, tôi gọi hai hàm một phần, mỗi hàm cung cấp cho tôi một hàm khác. Sau đó tôi chuyển các hàm được tạo này làm đối số cho một hàm khác được gọi bên trong một for
vòng lặp lồng nhau .
Liên quan đến chức năng trong đó nó thực thi, có vẻ như một for
vòng lặp tạo ra một cái gì đó giống với phạm vi của chính nó (mặc dù không chắc chắn đó là một phạm vi toàn diện). Sau đó, việc truyền các hàm được tạo qua ranh giới phạm vi (?) Này rất tốn kém.
Cấu trúc mã hóa cơ bản
Mỗi hàm một phần nhận giá trị X hoặc Y của vị trí con trỏ chuột trên hình ảnh Bộ Mandelbrot và trả về hàm được lặp khi tính toán bộ Julia tương ứng:
const makeJuliaXStepFn = mandelXCoord => (x, y) => mandelXCoord + diffOfSquares(x, y)
const makeJuliaYStepFn = mandelYCoord => (x, y) => mandelYCoord + (2 * x * y)
Các hàm này được gọi trong logic sau:
- Người dùng di chuyển con trỏ chuột lên hình ảnh của Bộ Mandelbrot kích hoạt
mousemove
sự kiện Vị trí hiện tại của con trỏ chuột được dịch sang không gian tọa độ của bộ Mandelbrot và tọa độ (X, Y) được truyền cho hàm
juliaCalcJS
để tính toán Bộ Julia tương ứng.Khi tạo bất kỳ Bộ Julia cụ thể nào, hai hàm một phần ở trên được gọi để tạo các hàm được lặp khi tạo Bộ Julia
Một
for
vòng lặp lồng nhau sau đó gọi hàmjuliaIter
để tính màu của mỗi pixel trong tập Julia. Mã hóa đầy đủ có thể được nhìn thấy ở đây , nhưng logic cơ bản là như sau:const juliaCalcJS = (cvs, juliaSpace) => { // Snip - initialise canvas and create a new image array // Generate functions for calculating the current Julia Set let juliaXStepFn = makeJuliaXStepFn(juliaSpace.mandelXCoord) let juliaYStepFn = makeJuliaYStepFn(juliaSpace.mandelYCoord) // For each pixel in the canvas... for (let iy = 0; iy < cvs.height; ++iy) { for (let ix = 0; ix < cvs.width; ++ix) { // Translate pixel values to coordinate space of Julia Set let x_coord = juliaSpace.xMin + (juliaSpace.xMax - juliaSpace.xMin) * ix / (cvs.width - 1) let y_coord = juliaSpace.yMin + (juliaSpace.yMax - juliaSpace.yMin) * iy / (cvs.height - 1) // Calculate colour of the current pixel let thisColour = juliaIter(x_coord, y_coord, juliaXStepFn, juliaYStepFn) // Snip - Write pixel value to image array } } // Snip - write image array to canvas }
Như bạn có thể thấy, các hàm được trả về bằng cách gọi
makeJuliaXStepFn
vàmakeJuliaYStepFn
bên ngoàifor
vòng lặp được truyền tớijuliaIter
, sau đó thực hiện tất cả công việc khó khăn để tính toán màu của pixel hiện tại
Khi tôi nhìn vào cấu trúc mã này, ban đầu tôi nghĩ "Cái này tốt, tất cả đều hoạt động tốt, vì vậy không có gì sai ở đây"
Ngoại trừ có. Hiệu suất chậm hơn nhiều so với dự kiến ...
Giải pháp bất ngờ
Nhiều cái đầu gãi và nghịch ngợm theo sau ...
Sau một thời gian, tôi phát hiện ra rằng nếu tôi di chuyển việc tạo các hàm juliaXStepFn
và juliaYStepFn
bên trong for
các vòng lặp bên ngoài hoặc bên trong , thì hiệu suất sẽ được cải thiện theo hệ số từ 2 đến 3 ...
WHAAAAAAT!?
Vì vậy, mã bây giờ trông như thế này
const juliaCalcJS =
(cvs, juliaSpace) => {
// Snip - initialise canvas and create a new image array
// For each pixel in the canvas...
for (let iy = 0; iy < cvs.height; ++iy) {
// Generate functions for calculating the current Julia Set
let juliaXStepFn = makeJuliaXStepFn(juliaSpace.mandelXCoord)
let juliaYStepFn = makeJuliaYStepFn(juliaSpace.mandelYCoord)
for (let ix = 0; ix < cvs.width; ++ix) {
// Translate pixel values to coordinate space of Julia Set
let x_coord = juliaSpace.xMin + (juliaSpace.xMax - juliaSpace.xMin) * ix / (cvs.width - 1)
let y_coord = juliaSpace.yMin + (juliaSpace.yMax - juliaSpace.yMin) * iy / (cvs.height - 1)
// Calculate colour of the current pixel
let thisColour = juliaIter(x_coord, y_coord, juliaXStepFn, juliaYStepFn)
// Snip - Write pixel value to image array
}
}
// Snip - write image array to canvas
}
Tôi đã dự kiến sự thay đổi dường như không đáng kể này sẽ kém hiệu quả hơn một chút, bởi vì một cặp hàm không cần thay đổi đang được tạo lại mỗi khi chúng ta lặp lại for
vòng lặp. Tuy nhiên, bằng cách di chuyển các khai báo hàm bên trong for
vòng lặp, mã này thực thi nhanh hơn từ 2 đến 3 lần!
Bất cứ ai có thể giải thích hành vi này?
Cảm ơn