Các phương pháp hay nhất để giảm hoạt động của Trình thu gom rác trong Javascript


94

Tôi có một ứng dụng Javascript khá phức tạp, có một vòng lặp chính được gọi là 60 lần mỗi giây. Có vẻ như có rất nhiều bộ sưu tập rác đang diễn ra (dựa trên đầu ra 'răng cưa' từ dòng thời gian Bộ nhớ trong các công cụ dành cho nhà phát triển Chrome) - và điều này thường ảnh hưởng đến hiệu suất của ứng dụng.

Vì vậy, tôi đang cố gắng nghiên cứu các phương pháp hay nhất để giảm bớt khối lượng công việc mà người thu gom rác phải làm. (Hầu hết thông tin tôi có thể tìm thấy trên web đều liên quan đến việc tránh rò rỉ bộ nhớ, đây là một câu hỏi hơi khác - bộ nhớ của tôi đang được giải phóng, chỉ là có quá nhiều rác đang diễn ra.) rằng điều này chủ yếu là để tái sử dụng các đồ vật càng nhiều càng tốt, nhưng tất nhiên ma quỷ nằm ở chi tiết.

Ứng dụng được cấu trúc theo 'lớp' dọc theo Dòng Kế thừa JavaScript đơn giản của John Resig .

Tôi nghĩ một vấn đề là một số hàm có thể được gọi hàng nghìn lần mỗi giây (vì chúng được sử dụng hàng trăm lần trong mỗi lần lặp lại của vòng lặp chính) và có lẽ các biến làm việc cục bộ trong các hàm này (chuỗi, mảng, v.v.) có thể là vấn đề.

Tôi biết về việc gộp đối tượng cho các đối tượng lớn hơn / nặng hơn (và chúng tôi sử dụng điều này ở một mức độ), nhưng tôi đang tìm kiếm các kỹ thuật có thể áp dụng trên diện rộng, đặc biệt là liên quan đến các hàm được gọi rất nhiều lần trong các vòng lặp chặt chẽ .

Tôi có thể sử dụng những kỹ thuật nào để giảm bớt khối lượng công việc mà người thu gom rác phải làm?

Và, có lẽ cũng vậy - những kỹ thuật nào có thể được sử dụng để xác định đối tượng nào đang được thu gom rác nhiều nhất? (Đó là một cơ sở mã lớn cực kỳ lớn, vì vậy việc so sánh các ảnh chụp nhanh của đống không được hiệu quả lắm)


2
Bạn có một ví dụ về mã của bạn, bạn có thể cho chúng tôi xem? Câu hỏi đặt ra sẽ dễ dàng hơn để câu trả lời sau đó (nhưng cũng có khả năng ít nói chung, vì vậy tôi không chắc chắn ở đây)
John Dvorak

2
Làm thế nào về các chức năng dừng chạy hàng nghìn lần mỗi giây? Đó có thực sự là cách duy nhất để tiếp cận điều này? Câu hỏi này có vẻ giống như một vấn đề XY. Bạn đang mô tả X nhưng những gì bạn đang thực sự tìm kiếm là một giải pháp cho Y.
Travis J

2
@TravisJ: Anh ấy chỉ chạy nó 60 lần mỗi giây, đây là tốc độ hoạt ảnh khá phổ biến. Anh ấy không yêu cầu làm ít công việc hơn, mà là làm thế nào để thu gom rác hiệu quả hơn.
Bergi

1
@Bergi - "một số hàm có thể được gọi hàng nghìn lần mỗi giây". Đó là một lần mỗi mili giây (có thể tệ hơn!). Điều đó không phổ biến chút nào. 60 lần mỗi giây không phải là một vấn đề. Câu hỏi này quá mơ hồ và chỉ đưa ra ý kiến ​​hoặc phỏng đoán.
Travis J

4
@TravisJ - Nó không phải là hiếm trong các khuôn khổ trò chơi.
UpTheCreek

Câu trả lời:


127

Rất nhiều điều bạn cần làm để giảm thiểu GC churn đi ngược lại những gì được coi là JS thành ngữ trong hầu hết các trường hợp khác, vì vậy hãy ghi nhớ bối cảnh khi đánh giá lời khuyên mà tôi đưa ra.

Phân bổ xảy ra trong các trình thông dịch hiện đại ở một số nơi:

  1. Khi bạn tạo một đối tượng thông qua newhoặc thông qua cú pháp nghĩa đen [...], hoặc {}.
  2. Khi bạn nối các chuỗi.
  3. Khi bạn nhập một phạm vi chứa khai báo hàm.
  4. Khi bạn thực hiện một hành động gây ra ngoại lệ.
  5. Khi bạn đánh giá một biểu hiện chức năng: (function (...) { ... }).
  6. Khi bạn thực hiện một thao tác buộc đối tượng như Object(myNumber)hoặcNumber.prototype.toString.call(42)
  7. Khi bạn gọi một nội trang thực hiện bất kỳ điều nào trong số này, chẳng hạn như Array.prototype.slice.
  8. Khi bạn sử dụng argumentsđể phản ánh qua danh sách tham số.
  9. Khi bạn tách một chuỗi hoặc so khớp với một biểu thức chính quy.

Tránh làm những việc đó, và gộp và tái sử dụng các đối tượng nếu có thể.

Cụ thể, hãy tìm kiếm các cơ hội để:

  1. Kéo các hàm bên trong không có hoặc ít phụ thuộc vào trạng thái đóng lại vào một phạm vi cao hơn, tồn tại lâu hơn. (Một số trình thu nhỏ mã như trình biên dịch Closure có thể nội tuyến các chức năng bên trong và có thể cải thiện hiệu suất GC của bạn.)
  2. Tránh sử dụng chuỗi để biểu thị dữ liệu có cấu trúc hoặc để định địa chỉ động. Đặc biệt tránh phân tích cú pháp lặp lại bằng cách sử dụng splithoặc đối sánh biểu thức chính quy vì mỗi đối tượng yêu cầu nhiều phân bổ đối tượng. Điều này thường xuyên xảy ra với các khóa trong bảng tra cứu và ID nút DOM động. Ví dụ: lookupTable['foo-' + x]document.getElementById('foo-' + x)cả hai đều liên quan đến một phân bổ vì có một nối chuỗi. Thường thì bạn có thể gắn khóa vào các vật thể tồn tại lâu dài thay vì nối lại. Tùy thuộc vào trình duyệt bạn cần hỗ trợ, bạn có thể sử dụng Mapđể sử dụng các đối tượng làm khóa trực tiếp.
  3. Tránh bắt các ngoại lệ trên các đường dẫn mã thông thường. Thay vì try { op(x) } catch (e) { ... }, hãy làm if (!opCouldFailOn(x)) { op(x); } else { ... }.
  4. Khi bạn không thể tránh việc tạo chuỗi, ví dụ: để chuyển thư đến máy chủ, hãy sử dụng nội trang giống như JSON.stringifysử dụng bộ đệm gốc bên trong để tích lũy nội dung thay vì phân bổ nhiều đối tượng.
  5. Tránh sử dụng lệnh gọi lại cho các sự kiện tần suất cao và nếu có thể, hãy chuyển dưới dạng lệnh gọi lại một hàm tồn tại lâu dài (xem 1) tạo lại trạng thái từ nội dung tin nhắn.
  6. Tránh sử dụng argumentsvì các hàm sử dụng phải tạo một đối tượng giống mảng khi được gọi.

Tôi đã đề xuất sử dụng JSON.stringifyđể tạo các tin nhắn mạng gửi đi. Việc phân tích cú pháp các thông điệp đầu vào bằng cách sử dụng JSON.parserõ ràng liên quan đến việc phân bổ, và rất nhiều trong số đó cho các thông điệp lớn. Nếu bạn có thể biểu diễn các thư đến của mình dưới dạng các mảng nguyên thủy, thì bạn có thể tiết kiệm được rất nhiều phân bổ. Nội trang duy nhất khác mà bạn có thể xây dựng trình phân tích cú pháp không phân bổ là String.prototype.charCodeAt. Một trình phân tích cú pháp cho một định dạng phức tạp chỉ sử dụng mà sẽ rất khó đọc.


Bạn không nghĩ rằng các JSON.parseđối tượng d phân bổ không gian ít hơn (hoặc bằng) so với chuỗi thông báo?
Bergi

@Bergi, Điều đó phụ thuộc vào việc tên thuộc tính có yêu cầu phân bổ riêng hay không, nhưng trình phân tích cú pháp tạo ra các sự kiện thay vì cây phân tích cú pháp không có allocaitons không liên quan.
Mike Samuel

Câu trả lời tuyệt vời, cảm ơn bạn! Nhiều lời xin lỗi đối với tiền thưởng hết hạn - Tôi đã đi du lịch vào thời điểm đó, và vì một lý do tôi không thể đăng nhập vào SO với tài khoản gmail của tôi trên điện thoại của tôi ....: /
UpTheCreek

Để bù đắp cho thời điểm tồi tệ của tôi với tiền thưởng, tôi đã thêm một khoản tiền bổ sung để nạp tiền (200 là mức tối thiểu tôi có thể đưa ra;) - Vì một số lý do, mặc dù nó yêu cầu tôi phải đợi 24 giờ trước khi trao thưởng (mặc dù Tôi đã chọn 'thưởng cho câu trả lời hiện có'). Sẽ là của bạn ngày mai ...
UpTheCreek

@UpTheCreek, đừng lo lắng. Tôi rất vui vì bạn thấy nó hữu ích.
Mike Samuel

13

Các công cụ dành cho nhà phát triển Chrome có một tính năng rất hay để theo dõi phân bổ bộ nhớ. Nó được gọi là Dòng thời gian bộ nhớ. Bài viết này mô tả một số chi tiết. Tôi cho rằng đây là những gì bạn đang nói về "răng cưa"? Đây là hành vi bình thường đối với hầu hết các thời gian chạy GC'ed. Việc phân bổ tiếp tục cho đến khi đạt đến ngưỡng sử dụng để kích hoạt một bộ sưu tập. Thông thường có nhiều loại tập hợp khác nhau ở các ngưỡng khác nhau.

Dòng thời gian bộ nhớ trong Chrome

Các bộ sưu tập rác được đưa vào danh sách sự kiện được liên kết với dấu vết cùng với thời lượng của chúng. Trên máy tính xách tay khá cũ của tôi, các bộ sưu tập tạm thời đang diễn ra ở khoảng 4Mb và mất 30ms. Đây là 2 trong số các lần lặp lại vòng lặp 60Hz của bạn. Nếu đây là một hình ảnh động, các bộ sưu tập 30ms có thể gây ra tình trạng giật hình. Bạn nên bắt đầu từ đây để xem điều gì đang xảy ra trong môi trường của mình: ngưỡng thu thập ở đâu và thời gian thu thập các bộ sưu tập của bạn. Điều này cung cấp cho bạn một điểm tham chiếu để đánh giá tối ưu hóa. Nhưng có lẽ bạn sẽ không làm gì tốt hơn là giảm tần suất nói lắp bằng cách làm chậm tốc độ phân bổ, kéo dài khoảng thời gian giữa các bộ sưu tập.

Bước tiếp theo là sử dụng Hồ sơ | Tính năng Phân bổ đống bản ghi để tạo danh mục phân bổ theo loại bản ghi. Thao tác này sẽ nhanh chóng hiển thị loại đối tượng nào đang sử dụng nhiều bộ nhớ nhất trong khoảng thời gian theo dõi, tương đương với tỷ lệ phân bổ. Tập trung vào những thứ này theo thứ tự tỷ lệ giảm dần.

Các kỹ thuật không phải là khoa học tên lửa. Tránh các đồ vật được đóng hộp khi bạn có thể làm với đồ vật không được đóng hộp. Sử dụng các biến toàn cục để giữ và sử dụng lại các đối tượng được đóng hộp đơn lẻ thay vì phân bổ các đối tượng mới trong mỗi lần lặp. Nhóm các loại đối tượng phổ biến trong danh sách miễn phí thay vì bỏ chúng. Kết quả nối chuỗi trong bộ nhớ cache có khả năng được sử dụng lại trong các lần lặp lại trong tương lai. Thay vào đó, tránh phân bổ chỉ để trả về kết quả hàm bằng cách đặt các biến trong một phạm vi bao quanh. Bạn sẽ phải xem xét từng loại đối tượng trong bối cảnh riêng của nó để tìm ra chiến lược tốt nhất. Nếu bạn cần trợ giúp về các chi tiết cụ thể, hãy đăng bản chỉnh sửa mô tả chi tiết về thử thách mà bạn đang xem xét.

Tôi khuyên bạn không nên thay đổi phong cách viết mã thông thường của bạn trong suốt một ứng dụng nhằm cố gắng tạo ra ít rác hơn. Đây cũng là lý do bạn không nên tối ưu hóa tốc độ quá sớm. Hầu hết nỗ lực của bạn cộng với phần lớn sự phức tạp và không rõ ràng của mã sẽ trở nên vô nghĩa.


Đúng vậy, ý tôi là cái răng cưa. Tôi biết sẽ luôn có một kiểu răng cưa nào đó, nhưng mối quan tâm của tôi là với ứng dụng của tôi, tần suất răng cưa và 'vách đá' khá cao. Điều thú vị là, GC sự kiện không hiển thị trên dòng thời gian của tôi - sự kiện duy nhất mà xuất hiện trong cửa sổ 'hồ sơ' (một trong những trung) là: request animation frame, animation frame fired, và composite layers. Tôi không biết tại sao tôi không thấy GC Eventgiống như bạn (đây là trên phiên bản chrome mới nhất và cả canary).
UpTheCreek,

4
Tôi đã thử sử dụng hồ sơ với 'phân bổ đống hồ sơ' nhưng cho đến nay vẫn chưa thấy nó rất hữu ích. Có lẽ là do mình chưa biết cách sử dụng hợp lý. Nó dường như chứa đầy các tham chiếu không có ý nghĩa gì đối với tôi, chẳng hạn như @342342code relocation info.
UpTheCreek

9

Theo nguyên tắc chung, bạn muốn lưu vào bộ nhớ cache càng nhiều càng tốt và thực hiện ít tạo và hủy cho mỗi lần chạy vòng lặp của bạn.

Điều đầu tiên nảy ra trong đầu tôi là giảm việc sử dụng các hàm ẩn danh (nếu bạn có) bên trong vòng lặp chính của bạn. Ngoài ra, rất dễ rơi vào cái bẫy của việc tạo và phá hủy các đối tượng được chuyển vào các chức năng khác. Tôi không phải là một chuyên gia javascript, nhưng tôi sẽ tưởng tượng rằng điều này:

var options = {var1: value1, var2: value2, ChangingVariable: value3};
function loopfunc()
{
    //do something
}

while(true)
{
    $.each(listofthings, loopfunc);

    options.ChangingVariable = newvalue;
    someOtherFunction(options);
}

sẽ chạy nhanh hơn nhiều so với điều này:

while(true)
{
    $.each(listofthings, function(){
        //do something on the list
    });

    someOtherFunction({
        var1: value1,
        var2: value2,
        ChangingVariable: newvalue
    });
}

Có bao giờ chương trình của bạn ngừng hoạt động không? Có thể bạn cần nó chạy trơn tru trong một hoặc hai giây (ví dụ: đối với hoạt ảnh) và sau đó nó có nhiều thời gian hơn để xử lý? Nếu trường hợp này xảy ra, tôi có thể thấy việc lấy các đối tượng thường là rác được thu thập trong suốt hoạt ảnh và giữ một tham chiếu đến chúng trong một số đối tượng toàn cục. Sau đó, khi hoạt ảnh kết thúc, bạn có thể xóa tất cả các tham chiếu và để bộ thu gom rác làm việc.

Xin lỗi nếu tất cả điều này là một chút nhỏ so với những gì bạn đã thử và nghĩ đến.


Điều này. Thêm vào đó, các chức năng được đề cập bên trong các chức năng khác (không phải là IIFE) cũng là việc lạm dụng phổ biến gây đốt cháy nhiều bộ nhớ và dễ bỏ sót.
Esailija

Cảm ơn Chris! Tôi không có bất kỳ thời gian chết không may: /
UpTheCreek

4

Tôi muốn tạo một hoặc một vài đối tượng trong global scope(nơi tôi chắc chắn rằng bộ thu gom rác không được phép chạm vào chúng), sau đó tôi sẽ cố gắng cấu trúc lại giải pháp của mình để sử dụng các đối tượng đó để hoàn thành công việc, thay vì sử dụng các biến cục bộ .

Tất nhiên nó không thể được thực hiện ở mọi nơi trong mã, nhưng nói chung đó là cách của tôi để tránh trình thu gom rác.

PS Nó có thể làm cho phần mã cụ thể đó ít khả năng bảo trì hơn một chút.


GC loại bỏ các biến phạm vi toàn cầu của tôi một cách nhất quán.
VectorVortec
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.