Phân Tích Mã Nguồn Lua GC (Phần 1)
Gần đây, trong quá trình vận hành môi trường Lua với lượng dữ liệu lớn, tôi nhận thấy hoạt động thu gom rác (GC) đang chiếm dụng tới khoảng 20% tổng thời gian CPU. Điều này khiến tôi quyết tâm tối ưu hóa vấn đề này. Để làm được điều đó, trước tiên phải hiểu rõ bản chất thuật toán và cơ chế triển khai GC của Lua. Dù đã từng nghiên cứu mã nguồn Lua trước đây, nhưng do sự thay đổi giữa các phiên bản mã nguồn, tôi buộc phải thực hiện lại toàn bộ quá trình này. Lần này, tôi đã tập trung phân tích kỹ lưỡng mã nguồn phiên bản Lua 5.1.4. Từ hôm nay, tôi sẽ ghi chép chi tiết quá trình nghiên cứu này thành chuỗi bài viết. Việc đọc mã nguồn đã chiếm trọn 1 ngày làm việc của tôi, nhưng việc biên soạn thành bài viết có thể còn tốn nhiều thời gian hơn nữa. Tôi sẽ chia sẻ nội dung này thành nhiều kỳ trên blog cá nhân.
Tổng quan về hệ thống GC trong Lua
Lua sử dụng hệ thống GC đơn giản dựa trên thuật toán đánh dấu và dọn dẹp (mark-sweep). Trong môi trường Lua, có tổng cộng 9 loại kiểu dữ liệu: nil
, boolean
, lightuserdata
, number
, string
, table
, function
, userdata
và thread
. Trong số này, chỉ có 4 kiểu dữ liệu string
, table
, function
, thread
là được chia sẻ dưới dạng tham chiếu trong máy ảo và cần được quản lý bởi GC. Các kiểu dữ liệu còn lại đều được lưu trữ dưới dạng giá trị trực tiếp.
Tuy nhiên, trong quá trình triển khai thực tế, Lua còn quản lý thêm 2 đối tượng khác qua GC:
- Proto (có thể hiểu là hàm chưa gắn
upvalue
) - Upvalue (nhiều
upvalue
có thể trỏ đến cùng một giá trị)
Cơ chế lưu trữ giá trị trong Lua
Lua sử dụng cấu trúc union kèm tag kiểu dữ liệu để lưu trữ giá trị. Cấu trúc này được định nghĩa trong tệp lobject.h
(dòng 56-75):
|
|
Như bạn thấy, trường value
trong TValue
được định nghĩa dưới dạng union
. Nếu giá trị cần được quản lý bởi GC, nó sẽ được lưu dưới dạng con trỏ GCObject*
. Ngược lại, giá trị sẽ được lưu trực tiếp. Trong toàn bộ mã nguồn, các giá trị đều được biểu diễn dưới dạng TValue
thay vì Value
để đảm bảo có thêm trường tag kiểu dữ liệu tt
. Trên hệ thống điển hình, mỗi TValue
chiếm 12 byte bộ nhớ.
Chú ý: Trong tài liệu chính thức The implementation of Lua 5.0, các tác giả đã thảo luận về việc tại sao không sử dụng kỹ thuật nén tag kiểu dữ liệu vào 8 byte đầu tiên trên hệ thống 32-bit.
Cấu trúc chung của các đối tượng GC
Tất cả các đối tượng được quản lý bởi GC đều bắt đầu với một phần tiêu đề chung gọi là CommonHeader
, được định nghĩa dưới dạng macro trong lobject.h
(dòng 43):
|
|
Từ đây, ta thấy:
- Tất cả các đối tượng GC được kết nối với nhau dưới dạng danh sách liên kết đơn
- Trường
tt
xác định kiểu dữ liệu của đối tượng - Trường
marked
dùng để đánh dấu trạng thái trong quá trình thu gom rác
Đặc điểm của chuỗi (string) trong GC
Mặc dù hầu hết các đối tượng GC đều được quản lý qua danh sách liên kết rootgc
, nhưng kiểu string
lại có cách xử lý đặc biệt. Tất cả các chuỗi được lưu trữ trong một bảng băm lớn để đảm bảo không có hai chuỗi trùng nhau tồn tại trong hệ thống. Do đó, các đối tượng kiểu TString
(đại diện cho chuỗi) không được đưa vào danh sách chung rootgc
, mà được quản lý riêng biệt thông qua cấu trúc stringtable
.
Cấu trúc lua_State
- Trạng thái của máy ảo Lua
lua_State
là cấu trúc dữ liệu quan trọng nhất khi làm việc với giao diện C-Lua. Về cơ bản, nó đại diện cho một luồng thực thi (thread
) cùng các thông tin liên quan như ngăn xếp (stack), môi trường thực thi, v.v. Cấu trúc này được định nghĩa trong lstate.h
(dòng 97):
|
|
值得注意的是,lua_State
tự nó cũng là một đối tượng GC với kiểu thread
.
Quản lý toàn cục qua global_State
Mỗi máy ảo Lua có thể chứa nhiều lua_State
(nhiều luồng). Tất cả các thông tin chia sẻ giữa các luồng được lưu trữ trong cấu trúc global_State
, bao gồm:
- Danh sách liên kết
rootgc
chứa hầu hết các đối tượng GC - Bảng băm
stringtable
chứa các chuỗi - Danh sách
tmudata
chứa các đối tượnguserdata
cần gọi hàm__gc
Hàm luaC_link
- Kết nối đối tượng GC vào hệ thống
Khi tạo mới một đối tượng GC, nó sẽ được kết nối vào danh sách rootgc
thông qua hàm luaC_link
(định nghĩa trong lgc.c
dòng 687):
|
|
Hàm này thực hiện các công việc chính:
- Thiết lập liên kết đơn hướng đến đầu danh sách
rootgc
- Đặt trạng thái ban đầu của đối tượng là “trắng” (chưa được đánh dấu)
- Gán kiểu dữ liệu cho đối tượng
Xử lý đặc biệt với upvalue
Upvalue
là trường hợp đặc biệt vì chúng là các tham chiếu gián tiếp đến giá trị đã tồn tại. Hàm luaC_linkupval
xử lý riêng trường hợp này để đảm bảo tính nhất quán khi GC hoạt động theo cơ chế phân đoạn (incremental). Khi GC đang ở giai đoạn lan truyền đánh dấu (`GCSpropagate