Khám Phá Mã Nguồn Go: Bảng Băm Và Quản Lý Ngăn Xếp - nói dối e blog

Khám Phá Mã Nguồn Go: Bảng Băm Và Quản Lý Ngăn Xếp

Trong quá trình tìm hiểu mã nguồn Go, tôi đặc biệt quan tâm đến cơ chế bảng băm trong thư viện runtime. Mục tiêu ban đầu là so sánh cách triển khai bảng băm của Go với hệ thống bảng (table) trong Lua.

Kết luận rút ra là bảng băm của Go phức tạp hơn nhiều so với phiên bản Lua. Nếu như mã nguồn ltable.c của Lua chỉ khoảng dưới 600 dòng thì tệp hashmap.c của Go lại vượt quá 1500 dòng. Điểm khác biệt lớn nằm ở định hướng tối ưu không gian của Go. Bảng băm trong Go được thiết kế theo kiểu có kiểu dữ liệu cụ thể - cả khóa (key) và giá trị (value) đều có kiểu cố định được lưu trữ trong cấu trúc mô tả kiểu. Mặc dù kích thước khóa/giá trị không xác định được ở thời điểm biên dịch, nhưng đã được xác định rõ ràng khi khởi tạo.

Giá trị băm trong Go là kiểu uintptr (8 byte trên hệ thống 64-bit). Thay vì lưu toàn bộ giá trị băm như các giải pháp thông thường, Go chỉ lưu trữ một byte đại diện để tiết kiệm bộ nhớ. Điều này tương tự cách triển khai trong Lua, nơi giá trị băm cũng không được lưu trữ đầy đủ do số lượng kiểu dữ liệu hạn chế và cố định.

Điểm đặc biệt là Go cho phép người dùng tự định nghĩa kiểu khóa. Để tối ưu hiệu năng, Go chỉ lưu một byte giá trị băm trong bảng, đồng thời sử dụng cấu trúc Bucket để nhóm 8 cặp khóa-giá trị. Các khóa trong cùng một Bucket có cùng giá trị AND giữa giá trị băm và kích thước bảng (luôn là lũy thừa của 2), đồng thời dùng byte cao hơn của giá trị băm để phân biệt chính xác. Khi số phần tử vượt quá 8, hệ thống sẽ mở rộng bằng cơ chế danh sách liên kết.

Cần lưu ý là giá trị 0 được dành riêng để đánh dấu vị trí trống trong Bucket. Nếu byte cao của giá trị băm thực sự bằng 0, hệ thống sẽ tự động thay thế thành 1 để tránh nhầm lẫn.

Khi tải trọng (LOAD) đạt ngưỡng 6.5 lần (giá trị được xác định qua thực nghiệm), bảng băm sẽ được mở rộng gấp đôi kích thước. Mặc dù tương tự cơ chế của Lua, nhưng Go sử dụng chiến lược phân tán mở (open hashing) thay vì đóng (closed hashing), ưu điểm là phù hợp hơn cho xử lý đa luồng.

Một điểm thú vị khác là Go không cho phép ghi đồng thời vào map nhưng lại hỗ trợ đọc song song. Hệ thống iterator của Go được thiết kế chuyên dụng nên hiệu quả hơn so với cơ chế không trạng thái của Lua. Tuy nhiên, khi bảng băm được mở rộng, iterator có thể đang tham chiếu đến mảng cũ. Giải pháp của Go là giữ nguyên cả mảng mới và cũ cho đến khi iterator bị hủy, đồng thời sử dụng cơ chế CAS (Compare-And-Swap) để đảm bảo an toàn đa luồng.

Trước khi nghiên cứu mã nguồn, tôi từng thắc mắc cách Go quản lý hàng triệu goroutine mà không bị quá tải bộ nhớ. Khác với Lua sử dụng cơ chế stack mềm trên heap (mỗi coroutine chỉ tiêu tốn 208 byte), Go cần tương tác với C nên phải tuân theo mô hình stack truyền thống. Giải pháp Go áp dụng là công nghệ “ngăn xếp phân đoạn” (segmented stacks) - cho phép stack có thể mở rộng động mà không cần liên tục. Khi phát hiện stack gần đầy, hệ thống sẽ cấp phát khối nhớ mới và chuyển con trỏ stack sang, phục hồi lại khi hàm trả về.

Điều này đòi hỏi sự hỗ trợ đặc biệt từ trình biên dịch và liên kết. Tương tự như giải pháp tôi từng áp dụng cho hệ thống coroutine trong trò chơi Tây Du Ký với giới hạn 4KB mỗi coroutine, Go cũng cho phép các hàm truyền thống chia sẻ không gian stack hệ thống do không bao giờ chứa điểm yield.

Một phát hiện bất ngờ khác là Go có trình biên dịch C riêng để xử lý mã C. Trình biên dịch này được mở rộng để hỗ trợ không gian tên thông qua ký hiệu đặc biệt “·” thay vì “::” như C++. Dù là ký hiệu Unicode, nhưng người dùng tiếng Việt có thể dễ dàng nhập bằng tổ hợp phím “@” trong chế độ gõ tiếng Việt.

0%