JavaScript bộ nhớ tĩnh với Nhóm đối tượng

Colt McAnlis
Colt McAnlis

Giới thiệu

Vì vậy, bạn sẽ nhận được email thông báo rằng trò chơi web / ứng dụng web của bạn hoạt động kém hiệu quả sau một khoảng thời gian nhất định, bạn đào sâu mã của mình và không thấy bất cứ điều gì nổi bật, cho đến khi bạn mở các công cụ cải thiện hiệu suất bộ nhớ của Chrome và xem nội dung sau:

Ảnh chụp nhanh từ dòng thời gian kỷ niệm của bạn

Một đồng nghiệp 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 hình răng cưa này cho thấy rất rõ một vấn đề hiệu suất nghiêm trọng tiềm ẩn. Khi mức sử dụng bộ nhớ tăng lên, bạn sẽ thấy khu vực trên biểu đồ cũng tăng lên trong ảnh chụp theo dòng thời gian. Khi biểu đồ sụt giảm đột ngột thì đó là trường hợp Công cụ thu gom rác đã chạy và dọn dẹp các đối tượng bộ nhớ được tham chiếu đến của bạn.

Ý nghĩa của răng cưa

Trong biểu đồ như thế này, bạn có thể thấy rằng có rất nhiều sự kiện Thu gom rác đang diễn ra, có thể gây ảnh hưởng xấu đến hiệu suất của ứng dụng web. Bài viết này sẽ trình bày 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í hoạt động và thu gom rác

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à Garbage Collector. Trong nhiều ngôn ngữ, lập trình viên trực tiếp chịu trách nhiệm phân bổ và giải phóng bộ nhớ khỏi Vùng nhớ khối xếp bộ nhớ của hệ thống. Tuy nhiên, hệ thống Garbage Collector sẽ thay mặt lập trình viên quản lý nhiệm vụ này, nghĩa là các đối tượng sẽ không trực tiếp giải phóng khỏi bộ nhớ khi người lập trình tham chiếu đến nó, mà vào một thời điểm sau này khi phỏng đoán của GC quyết định rằng sẽ có lợi cho việc này. Quá trình quyết định này đòi hỏi GC phải thực thi một số phân tích thống kê về đối tượng hoạt động và không hoạt động, việc này mất một khoảng thời gian để thực hiện.

Việc thu gom rác thường được mô tả là ngược lại với quản lý bộ nhớ thủ công, yêu cầu người lập trình chỉ định đối tượng nào cần phân bổ và trả lại hệ thống bộ nhớ

Quá trình mà trong đó GC thu hồi bộ nhớ không còn trống, thường làm giảm hiệu suất hiện có bằng cách dành một khoảng thời gian để thực hiện công việc của mình; song song với đó, hệ thống sẽ tự 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ã. Xung này sẽ chặn quá trình thực thi mã cho đến khi hoàn tất. Thường thì bạn 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 một thời điểm cho trước.

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. Các hệ thống thu gom rác có thể làm ngắn mạch mục tiêu này, vì chúng có thể chạy vào các thời điểm ngẫu nhiên trong khoảng thời gian ngẫu nhiên, ăn vào thời gian có sẵn mà ứng dụng cần để đáp ứng mục tiêu 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 mà một 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 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 vòng đời ứng dụng, bạn cũng sẽ giảm được khoảng thời gian GC cần từ quá trình 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ỷ, một cách 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ừ :

Ảnh chụp nhanh từ dòng thời gian kỷ niệm của bạn

tới đây:

JavaScript bộ nhớ tĩnh

Trong mô hình này, bạn có thể thấy biểu đồ không còn có răng cưa như mẫu nữa, mà phát triển rất nhiều lúc ban đầu, rồi tăng dần theo thời gian. Nếu gặp phải các vấn đề về hiệu suất do tình trạng nhồi nhét bộ nhớ, đây là loại biểu đồ bạn cần tạo.

Chuyển sang JavaScript bộ nhớ tĩnh

JavaScript bộ nhớ tĩnh là một kỹ thuật có liên quan đến việc phân bổ trước, khi khởi động ứng dụng, tất cả bộ nhớ cần thiết cho toàn bộ thời gian hoạt độ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 qua một vài bước đơn giản:

  1. Đo lường ứng dụng của bạn để xác định số lượng đối tượng bộ nhớ trực tiếp cần thiết tối đa (mỗi loại) cho nhiều tình huống sử dụng
  2. Triển khai lại mã để phân bổ trước lượng dữ liệu 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.

Trong thực tế, việc hoàn thành mục tiêu số 1 đòi hỏi chúng ta phải làm được bước thứ 2, vì vậy hãy bắt đầu từ đó.

Nhóm đối tượng

Nói một cách đơn giản, nhóm đố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ó chung một kiểu. 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ừ hệ thống Memory Heap, bạn sẽ tái chế một trong các đối tượng không dùng đến từ nhóm. Sau khi hoàn tất mã bên ngoài với đối tượng, thay vì giải phóng mã vào bộ nhớ chính, mã bên ngoài sẽ được trả về nhóm. Vì đối tượng không bao giờ bị huỷ tham chiếu (còn gọi là bị xoá) khỏi mã, nên đối tượng này sẽ không bị thu thập rác. Khi sử dụng các nhóm đối tượng, lập trình viên sẽ lại có quyền kiểm soát bộ nhớ, giảm mức độ ảnh hưởng của bộ 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 việc sử dụng đúng cách các nhóm đối tượng yêu cầu bạn phải có một nhóm cho mỗi loại có khả năng xảy ra tình trạng 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 các ứng dụng, cuối cùng bạn sẽ đạt đến một mức độ nào đó liên quan đến nhu cầu phân bổ các đối tượng mới. Qua nhiều lần chạy ứng dụng, bạn sẽ hiểu được giới hạn trên là gì 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 việc gộp đối tượng vào dự án sẽ cung cấp cho bạn mức tối đa theo lý thuyết về số lượng đối tượng cần thiết trong thời gian chạy ứng dụng. Sau khi chạy trang web qua nhiều tình huống kiểm thử, bạn có thể hiểu 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 đó, đồng thời 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 là gì.

Sau đó, trong phiên bản vận chuyển của ứng dụng, bạn có thể thiết lập giai đoạn khởi chạy để điền sẵn tất cả các nhóm đối tượng với số lượng đã chỉ định. Thao tác này sẽ đẩy toàn bộ quá trình khởi tạo đối tượng lên nền trước của ứng dụng, đồng thời giảm lượng phân bổ diễn 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 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ể khiến bạn tạo ra mức bộ nhớ nhỏ hơn đối với những người không dùng nhiều.

Cách xa một viên đạn bạc

Có rất nhiều phân loại ứng dụng mà mô hình tăng trưởng bộ nhớ tĩnh có thể mang lại lợi ích. Tuy nhiên, như Renato Mangini, thành viên Chrome DevRel, thành viên đồng nghiệp chỉ ra, có một số hạn chế.

Kết luận

Một trong những lý do khiến JavaScript trở nên lý tưởng cho web dựa trên thực tế rằng đó là một ngôn ngữ nhanh, thú vị và dễ dàng để bắt đầu. Điều này chủ yếu là do rào cản thấp về hạn chế cú pháp và khả năng xử lý các vấn đề về bộ nhớ thay mặt bạn. Bạn có thể lập trình và để công việc này xử lý công việc sửa đổi. Tuy nhiên, đối với các ứng dụng web có hiệu suất cao (như trò chơi HTML5), GC thường có thể tiêu tốn tốc độ khung hình cực kỳ cần thiết, làm giảm trải nghiệm của người dùng cuối. Với một số khả năng đo lường và sử dụng nhóm đối tượng cẩn thận, bạn có thể giảm bớt gánh nặng này đối với tốc độ khung hình và có thể lấy lại thời gian đó để thực hiện những tác vụ tuyệt vời hơn.

Mã nguồn

Có rất nhiều cách triển khai nhóm đối tượng trôi nổi trên web, vì vậy tôi sẽ không nói gì với bạn thêm nữa. Thay vào đó, tôi sẽ hướng dẫn bạn đến các phần này, mỗi phương pháp có những sắc thái triển khai cụ thể; điều quan trọng là phải xét đến việc mỗi cách sử dụng ứng dụng có thể có những nhu cầu triển khai cụ thể.

Tài liệu tham khảo