Giới thiệu
Vì vậy, bạn nhận được một email cho biết trò chơi/ứng dụng web của bạn đang hoạt động kém hiệu quả sau một khoảng thời gian nhất định, bạn tìm hiểu kỹ mã của mình, không thấy bất kỳ điều gì nổi bật cho đến khi bạn 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 cười khúc khích vì họ nhận ra rằng bạn đang 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 một vấn đề tiềm ẩn nghiêm trọng về hiệu suất. 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 biểu đồ như thế này, bạn có thể thấy có nhiều sự kiện Thu gom rác đang diễn ra, điều này có thể gây hại cho hiệu suất của ứ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.
Thu gom rác và chi phí 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ệ gọi là Trình dọn 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ớ khỏi 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ẽ quản lý tác vụ này thay cho lập trình viên, 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, mà là vào thời điểm sau đó 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 quyết định này yêu cầu GC thực hiện 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 cần một khoảng thời gian để thực hiện.
Quá trình dọn rác thường được mô tả là trái ngược với việc quản lý bộ nhớ thủ công, yêu cầu lập trình viên chỉ định đối tượng nào cần 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í, quá trình này 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; bên cạnh đó, chính hệ thống sẽ đưa ra quyết định thời điểm chạy. Bạn không có quyền kiểm soát hành động này, xung GC có thể xảy ra bất cứ lúc nào trong quá trình thực thi mã, điều này sẽ chặn việc thực thi mã cho đến khi hoàn tất. Bạn thường không biết thời lượng của xung này; 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 bất kỳ thời điểm nào.
Các ứng dụng hiệu suất cao dựa vào ranh giới hiệu suất nhất quán để đảm bảo 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ì hệ thống này có thể chạy vào những 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ần thiết để ứng dụng đạt được mục tiêu về hiệu suất.
Giảm tình trạng nhồi nhét bộ nhớ, giảm thuế thu gom rác
Như đã lưu ý, xung GC sẽ xảy ra 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 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 thường xuyên này đượ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 thời gian hoạt động của ứng dụng, bạn cũng sẽ giảm được 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à huỷ, đồng thời bạn phải ngừng phân bổ bộ nhớ.
Quá trình này sẽ di chuyển biểu đồ kỷ niệm của bạn từ :
thành:
Trong mô hình này, bạn có thể thấy biểu đồ không còn có dạng răng cưa nữa, mà tăng trưởng rất nhiều ở giai đoạn đầu, sau đó tăng dần theo thời gian. Nếu bạn gặp phải vấn đề về hiệu suất do tình trạng nhồi nhét bộ nhớ, thì đây là loại biểu đồ bạn nên tạo.
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 cho vòng đời của ứng dụng và quản lý bộ nhớ đó trong quá trình thực thi khi không cần đến các đối tượng nữa. Chúng ta có thể đạt được mục tiêu này qua một vài bước đơn giản:
- Đo lường ứng dụng để xác định số lượng tối đa các đối tượng bộ nhớ hoạt động cần thiết (theo từng loại) cho một loạt tình huống sử dụng
- Triển khai lại mã để phân bổ trước dung lượng tối đa đó, sau đó tìm nạp/giải phóng chúng theo cách thủ công thay vì chuyển đến bộ nhớ chính.
Trên thực tế, để hoàn thành mục tiêu số 1, chúng ta cần thực hiện một chút mục tiêu số 2, vì vậy, hãy bắt đầu từ đó.
Nhóm đố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 loại. Khi cần một đối tượng mới cho mã, 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 sẽ tái sử dụng một trong các đối tượng không dùng đến trong vùng nhớ. Sau khi mã bên ngoài hoàn tất với đố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ờ được huỷ tham chiếu (hay 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 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 không đồng nhất các loại đối tượng mà ứng dụng duy trì, nên để sử dụng đúng nhóm đối tượng, bạn phải có một nhóm cho mỗi loại có tỷ lệ rời bỏ 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 đến một mức độ nhất định về việc cần phân bổ các đối tượng mới. Sau nhiều lần chạy ứng dụng, bạn sẽ có thể nắm 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 gộp đối tượng vào dự án sẽ giúp bạn có được số lượng đối tượng tối đa theo lý thuyết cần thiết trong thời gian chạy của ứng dụng. Sau khi chạy trang web của mình thông qua nhiều tình huống thử nghiệm, bạn có thể nắm rõ các loại yêu cầu về bộ nhớ cần thiết, đồng thời có thể lập danh mục dữ liệu đó ở một nơi nào đó và phân tích dữ liệu đó để hiểu 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 để tự động điền trước tất cả các nhóm đối tượng theo một lượng cụ thể. Hành động này sẽ đẩy tất cả quá trình khởi chạy đối tượng lên đầu ứng dụng và giảm số lượng phân bổ xảy ra 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ố lượng 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 thành thạo.
Không phải là giải pháp tối ưu
Có một loại ứng dụng mà trong đó, các mẫu tăng trưởng bộ nhớ tĩnh có thể mang lại lợi ích. Tuy nhiên, như Renato Mangini, một thành viên khác trong nhóm Quan hệ nhà phát triển Chrome, đã chỉ ra, có một vài điểm 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 quy tắc hạn chế về cú pháp và khả năng xử lý các vấn đề về bộ nhớ thay cho bạn. Bạn có thể viết mã và để Gemini xử lý những việc phức tạp. 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 của 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 bớt gánh nặng này cho tốc độ khung hình và dành thời gian đó cho 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 với một cách triển khai khác. Thay vào đó, tôi sẽ hướng dẫn bạn về các phương thức này, mỗi phương thức đều có các sắc thái triển khai cụ thể; điều này rất quan trọng, vì mỗi cách sử dụng ứng dụng có thể có các nhu cầu triển khai cụ thể.
- Nhóm đối tượng của Gamecore.js
- Nhóm đối tượng của Beej
- Nhóm đối tượng siêu đơn giản của Emehrkay
- Nhóm đối tượng tập trung vào trò chơi của Steven Lambert
- Thiết lập objectPool của RenderEngine