Giới thiệu
Bạn nhận được email cho biết trò chơi/ứng dụng web của mình đang hoạt động kém sau một khoảng thời gian nhất định, bạn tìm hiểu mã của mình nhưng không thấy gì nổi bật, cho đến khi mở công cụ hiệu suất bộ nhớ của Chrome và thấy thông tin sau:
Một trong những đồng nghiệp của bạn bật cười vì họ nhận ra rằng bạn gặp vấn đề về hiệu suất liên quan đến bộ nhớ.
Trong chế độ xem biểu đồ bộ nhớ, mẫu răng cưa này cho biết rất rõ về một vấn đề về hiệu suất có thể nghiêm trọng. Khi mức sử dụng bộ nhớ tăng lên, bạn sẽ thấy vùng biểu đồ cũng tăng lên trong bản ghi dòng thời gian. Khi biểu đồ giảm đột ngột, đó là trường hợp Trình thu gom rác đã chạy và dọn dẹp các đối tượng bộ nhớ được tham chiếu.
Trong một biểu đồ như thế này, bạn có thể thấy rằng có nhiều sự kiện Thu gom rác đang diễn ra. Điều này có thể ảnh hưởng xấu đến hiệu suất của các ứng dụng web. Bài viết này sẽ nói về cách kiểm soát mức sử dụng bộ nhớ, giảm tác động đến hiệu suất.
Chi phí thu gom rác và hiệu suất
Mô hình bộ nhớ của JavaScript được xây dựng dựa trên một công nghệ có tên là Trình thu gom rác. Trong nhiều ngôn ngữ, lập trình viên chịu trách nhiệm trực tiếp về việc phân bổ và giải phóng bộ nhớ từ Vùng nhớ khối xếp của hệ thống. Tuy nhiên, hệ thống Trình thu gom rác sẽ thay mặt lập trình viên quản lý tác vụ này, nghĩa là các đối tượng không được giải phóng trực tiếp khỏi bộ nhớ khi lập trình viên huỷ tham chiếu đối tượng, mà là vào lúc khác khi phương pháp phỏng đoán của GC quyết định rằng việc này sẽ có lợi. Quá trình ra quyết định này yêu cầu GC thực thi một số phân tích thống kê về các đối tượng đang hoạt động và không hoạt động, việc này sẽ mất một khoảng thời gian để thực hiện.
Thu gom rác thường được mô tả là đối lập với việc quản lý bộ nhớ thủ công, đòi hỏi lập trình viên phải chỉ định đối tượng nào sẽ được giải phóng và trả về hệ thống bộ nhớ
Quá trình GC thu hồi bộ nhớ không phải là miễn phí, thường làm giảm hiệu suất hiện có của bạn bằng cách dành một khoảng thời gian để thực hiện công việc; cùng với đó, chính hệ thống sẽ đưa ra quyết định về thời điểm chạy. Bạn không có quyền kiểm soát hành động này và xung GC có thể xảy ra bất cứ lúc nào trong quá trình thực thi mã. Xung này sẽ chặn quá trình thực thi mã cho đến khi hoàn tất. Thời lượng của xung này thường không xác định được với bạn; sẽ mất một khoảng thời gian để chạy, tuỳ thuộc vào cách chương trình của bạn sử dụng bộ nhớ tại một thời điểm cụ thể bất kỳ.
Các ứng dụng hiệu suất cao dựa vào các giới hạn hiệu suất nhất quán để đảm bảo mang lại trải nghiệm mượt mà cho người dùng. Hệ thống thu gom rác có thể làm gián đoạn mục tiêu này vì chúng có thể chạy vào thời điểm ngẫu nhiên trong khoảng thời gian ngẫu nhiên, làm giảm thời gian có sẵn mà ứng dụng cần để đáp ứng các mục tiêu về hiệu suất.
Giảm tỷ lệ thay đổi bộ nhớ, giảm thuế thu gom rác
Như đã lưu ý, một xung GC sẽ xảy ra sau khi một tập hợp các phương pháp phỏng đoán xác định rằng có đủ đối tượng không hoạt động để xung sẽ có lợi. Do đó, chìa khoá để giảm thời gian mà Trình thu gom rác lấy từ ứng dụng của bạn nằm ở việc loại bỏ càng nhiều trường hợp tạo và giải phóng đối tượng quá mức càng tốt. Quá trình tạo/giải phóng đối tượng này thường được gọi là "nhồi nhét bộ nhớ". Nếu có thể giảm tình trạng nhồi nhét bộ nhớ trong suốt vòng đời của ứng dụng, bạn cũng sẽ giảm được lượng thời gian GC cần để thực thi. Điều này có nghĩa là bạn cần xoá/giảm số lượng đối tượng được tạo và bị huỷ, hiệu quả là bạn phải ngừng phân bổ bộ nhớ.
Quá trình này sẽ di chuyển biểu đồ bộ nhớ của bạn từ:
thành:
Trong mô hình này, bạn có thể thấy rằng biểu đồ không còn có mẫu giống như hình răng cưa mà thay vào đó sẽ phát triển rất nhiều ở ban đầu, sau đó tăng dần theo thời gian. Nếu đang gặp vấn đề về hiệu suất do sự cố về bộ nhớ, bạn nên tạo loại biểu đồ này.
Chuyển sang JavaScript bộ nhớ tĩnh
JavaScript bộ nhớ tĩnh là một kỹ thuật liên quan đến việc phân bổ trước, khi bắt đầu ứng dụng, tất cả bộ nhớ cần thiết trong suốt thời gian hoạt động của ứng dụng và quản lý bộ nhớ đó trong quá trình thực thi khi các đối tượng không còn cần thiết nữa. Chúng ta có thể tiếp cận mục tiêu này theo một vài bước đơn giản:
- Đo lường ứng dụng của bạn để xác định số lượng tối đa đối tượng bộ nhớ đang hoạt động bắt buộc (theo loại) cho một loạt các trường hợp sử dụng
- Triển khai lại mã của bạn để phân bổ trước số lượng tối đa đó, sau đó tìm nạp/giải phóng các đối tượng đó theo cách thủ công thay vì chuyển sang bộ nhớ chính.
Trong thực tế, để hoàn thành mục tiêu #1, chúng ta cần thực hiện một chút mục tiêu #2, vì vậy, hãy bắt đầu từ đó.
Vùng chứa đối tượng
Nói một cách đơn giản, gộp đối tượng là quá trình giữ lại một tập hợp các đối tượng không dùng đến có cùng một loại. Khi cần một đối tượng mới cho mã của mình, thay vì phân bổ một đối tượng mới từ Vùng nhớ khối xếp của hệ thống, bạn nên tái chế một trong các đối tượng không dùng đến trong nhóm. Sau khi mã bên ngoài hoàn tất đối tượng, thay vì giải phóng đối tượng đó vào bộ nhớ chính, đối tượng đó sẽ được trả về nhóm. Vì đối tượng không bao giờ bị gỡ tham chiếu (còn gọi là bị xoá) khỏi mã nên đối tượng sẽ không được thu gom rác. Việc sử dụng nhóm đối tượng sẽ giúp lập trình viên kiểm soát bộ nhớ, giảm ảnh hưởng của trình thu gom rác đối với hiệu suất.
Vì có một tập hợp các loại đối tượng không đồng nhất mà ứng dụng duy trì, nên để sử dụng đúng cách các nhóm đối tượng, bạn cần có một nhóm cho mỗi loại có tỷ lệ thay đổi cao trong thời gian chạy của ứng dụng.
var newEntity = gEntityObjectPool.allocate();
newEntity.pos = {x: 215, y: 88};
//..... do some stuff with the object that we need to do
gEntityObjectPool.free(newEntity); //free the object when we're done
newEntity = null; //free this object reference
Đối với phần lớn ứng dụng, cuối cùng bạn sẽ đạt được một mức độ ổn định về việc cần phân bổ các đối tượng mới. Trong nhiều lần chạy ứng dụng, bạn sẽ có thể hiểu rõ giới hạn trên này và có thể phân bổ trước số lượng đối tượng đó khi bắt đầu ứng dụng.
Phân bổ trước đối tượng
Việc triển khai tính năng gộp đối tượng vào dự án sẽ cung cấp cho bạn số lượng đối tượng tối đa theo lý thuyết trong thời gian chạy của ứng dụng. Sau khi chạy trang web qua các tình huống kiểm thử khác nhau, bạn có thể nắm rõ các loại yêu cầu về bộ nhớ sẽ cần đến, đồng thời có thể lập danh mục dữ liệu đó ở nơi nào đó và phân tích dữ liệu để nắm được giới hạn trên của yêu cầu về bộ nhớ đối với ứng dụng của bạn.
Sau đó, trong phiên bản phát hành của ứng dụng, bạn có thể đặt giai đoạn khởi chạy để điền sẵn tất cả các nhóm đối tượng theo số lượng đã chỉ định. Hành động này sẽ đẩy tất cả các hoạt động khởi tạo đối tượng lên đầu ứng dụng và giảm số lượng lượt phân bổ xảy ra một cách linh động trong quá trình thực thi.
function init() {
//preallocate all our pools.
//Note that we keep each pool homogeneous wrt object types
gEntityObjectPool.preAllocate(256);
gDomObjectPool.preAllocate(888);
}
Số tiền bạn chọn có liên quan rất nhiều đến hành vi của ứng dụng; đôi khi mức tối đa theo lý thuyết không phải là lựa chọn tốt nhất. Ví dụ: việc chọn mức tối đa trung bình có thể giúp bạn giảm mức sử dụng bộ nhớ cho những người dùng không phải là người dùng chuyên sâu.
Không phải là giải pháp toàn diện
Có một toàn bộ phân loại ứng dụng trong đó các mẫu tăng trưởng bộ nhớ tĩnh có thể là một chiến thắng. Tuy nhiên, như đồng nghiệp Renato Mangini của Chrome DevRel chỉ ra, có một vài hạn chế.
Kết luận
Một trong những lý do khiến JavaScript trở thành ngôn ngữ lý tưởng cho web là vì đây là một ngôn ngữ nhanh, thú vị và dễ bắt đầu. Điều này chủ yếu là do rào cản thấp đối với các hạn chế về cú pháp và cách Google xử lý các vấn đề về bộ nhớ thay cho bạn. Bạn có thể lập trình và để thư viện này xử lý công việc khó khăn. Tuy nhiên, đối với các ứng dụng web hiệu suất cao, chẳng hạn như trò chơi HTML5, GC thường có thể làm giảm tốc độ khung hình cần thiết, làm giảm trải nghiệm cho người dùng cuối. Bằng cách đo lường cẩn thận và sử dụng các nhóm đối tượng, bạn có thể giảm gánh nặng này đối với tốc độ khung hình và lấy lại thời gian đó để làm những việc thú vị hơn.
Mã nguồn
Có rất nhiều cách triển khai nhóm đối tượng trên web, vì vậy, tôi sẽ không làm bạn chán bằng một cách triển khai khác. Thay vào đó, tôi sẽ hướng dẫn bạn về từng cách triển khai, mỗi cách sẽ có những điểm riêng biệt về cách triển khai. Điều này rất quan trọng vì mỗi cách sử dụng ứng dụng có thể có nhu cầu triển khai cụ thể.
- Bể đối tượng của Gamecore.js
- Bể đối tượng của Beej
- Bể đối tượng siêu đơn giản của Emehrkay
- Vùng nhớ khối tập trung vào trò chơi của Steven Lambert
- Thiết lập objectPool của RenderEngine