Giảm Tải Chi Phí Thu Gom Rác Trong Lua - nói dối e blog

Giảm Tải Chi Phí Thu Gom Rác Trong Lua

Vào cuối tuần trước, một đồng nghiệp đã đặt câu hỏi với tôi về việc họ nghi ngờ quá trình thu gom rác (GC) của Lua đang gây ra chi phí hệ thống quá lớn trong hệ thống của họ. Đặc biệt, một phần chi phí này dường như không cần thiết.

Trong hệ thống của họ, có một lượng lớn dữ liệu cấu trúc chỉ đọc, không liên quan đến các dữ liệu khác - chủ yếu là các tham số logic trò chơi do策划设定 (cần dịch lại thành “nhóm thiết kế trò chơi” hoặc “nhóm策划” tùy ngữ cảnh). Những dữ liệu này có cấu trúc phức tạp, dẫn đến việc trình thu gom rác phải duyệt qua rất nhiều đối tượng trong quá trình hoạt động. Tuy nhiên, rõ ràng chúng ta đều biết rằng những dữ liệu này sẽ không bao giờ bị thu hồi, đồng thời cũng không ảnh hưởng đến kết quả của quá trình GC. Vậy liệu có cách nào để tối ưu hóa vấn đề này?

Đầu tiên, tôi cho rằng vấn đề này mới chỉ dừng ở giai đoạn phỏng đoán. Việc tồn tại của những dữ liệu cấu trúc này có thực sự ảnh hưởng đáng kể đến hiệu suất GC hay không vẫn cần được phân tích kỹ lưỡng. Tuy nhiên, giả sử rằng nhu cầu tối ưu hóa này tồn tại, tôi xin đề xuất một số giải pháp như sau:

Nguyên tắc cơ bản: Mọi giải pháp tối ưu đều đi kèm với sự đánh đổi. Việc giảm lượng dữ liệu cần duyệt qua trong quá trình GC chắc chắn sẽ làm tăng chi phí ở các khía cạnh khác.

Lua sử dụng cơ chế GC dựa trên đánh dấu và dọn dẹp (mark-and-sweep). Do đó, bất kỳ dữ liệu nào cần giữ lại đều phải được đánh dấu rõ ràng. Chúng ta không thể tránh khỏi việc đánh dấu, nhưng có thể tìm cách tăng tốc độ đánh dấu.

Giải pháp 1: Sử dụng nhiều trạng thái Lua (Lua States)
Một giải pháp đã được kiểm chứng là sử dụng kiến trúc Lua Rings hoặc thiết kế nhiều trạng thái Lua độc lập. Những dữ liệu độc lập (có thể là chỉ đọc hoặc có thể sửa đổi, nhưng quan trọng là không phụ thuộc lẫn nhau) sẽ được đặt trong một trạng thái Lua riêng biệt. Khi đó, trong trạng thái Lua chính (thường xuyên cập nhật logic trò chơi), những dữ liệu này sẽ được coi như một đối tượng duy nhất - chính là đối tượng trạng thái con. Quá trình đánh dấu chỉ cần thực hiện một lần duy nhất cho toàn bộ trạng thái con này.

Hệ quả:
Việc truyền dữ liệu giữa các trạng thái sẽ tốn kém hơn. Do đó, cần thiết kế một cơ chế truyền dẫn thông minh. Một giải pháp khả thi là xây dựng một hệ thống cache chung trong trạng thái mẹ, tránh việc sao chép dữ liệu từ trạng thái con mỗi lần sử dụng. Cache này có thể được thiết kế dưới dạng bảng yếu (weak table), cho phép hệ thống tự động xóa khi có chu kỳ GC mới diễn ra.

Giải thích bản chất:
Giải pháp này thực chất là chuyển gánh nặng duyệt qua cấu trúc dữ liệu phức tạp trong quá trình GC sang chi phí truy cập từng phần tử dữ liệu. Lập trình viên cần cân nhắc giữa hai lựa chọn này. Về mặt kiến trúc, việc sử dụng nhiều trạng thái Lua còn giúp tăng tính ổn định và phân tách rõ ràng các thành phần hệ thống.

Kinh nghiệm thực tiễn:
Trong hai năm gần đây, sau khi hoàn thiện module GC trong C và áp dụng vào dự án thực tế, tôi nhận ra rằng nếu lập trình viên nắm rõ toàn bộ các đối tượng tham gia vào quá trình GC, thì thuật toán GC đơn giản nhất lại có thể mang lại hiệu quả tốt nhất. Thuật toán này không cần cơ chế phân thế hệ (generational) hay quét dọn từng bước (incremental). Trong hệ thống của chúng tôi, số lượng đối tượng được quản lý bởi GC luôn duy trì ở mức hàng nghìn. Ở quy mô này, mọi thuật toán đều hoạt động hiệu quả (vì độ phức tạp thuật toán không còn là yếu tố quyết định khi n nhỏ).

Mở rộng thêm:
Một hướng tối ưu khác là sử dụng kỹ thuật “object pooling” (tạo sẵn các đối tượng để tái sử dụng) nhằm giảm tần suất kích hoạt GC. Đồng thời, với các dữ liệu chỉ đọc, có thể cân nhắc chuyển sang lưu trữ dưới dạng binary hoặc sử dụng các thư viện như MessagePack để giảm tải bộ nhớ heap của Lua.

0%