U Language: Quản lý bộ nhớ không cần garbage collection, truy cập cực nhanh và an toàn

Công nghệ25 tháng 9, 2026·9 phút đọc

Ngôn ngữ lập trình U giới thiệu phương pháp quản lý bộ nhớ mới dựa trên mô hình sở hữu DAG, loại bỏ hoàn toàn garbage collection. Cách tiếp cận này kết hợp bộ cấp phát slab chain, kỹ thuật NaN-boxing và hệ thống resolver phân thế hệ để đạt hiệu năng cao trong khi vẫn đảm bảo an toàn bộ nhớ.

U Language vừa công bố một phương pháp quản lý bộ nhớ hoàn toàn mới, hứa hẹn loại bỏ garbage collection mà vẫn đảm bảo truy cập nhanh và an toàn. Đây là bước tiến đáng chú ý trong bối cảnh các ngôn ngữ hiện đại như Rust, Go hay Swift vẫn đang vật lộn với bài toán cân bằng giữa hiệu năng và an toàn bộ nhớ.

Không có garbage collector — sở hữu là DAG

Điểm cốt lõi trong thiết kế của U là không có garbage collector. Thay vào đó, quyền sở hữu (ownership) được tổ chức như một đồ thị có hướng không chu trình (DAG) — các tham chiếu mạnh chỉ trỏ từ cha xuống con, không bao giờ trỏ ngược lên hoặc tạo chu trình.

Khi số đếm tham chiếu của một chủ sở hữu về không, toàn bộ cây con của nó cũng bị giải phóng. Không có tracing, không mark-sweep, không dừng chương trình (pause). Chi phí giải phóng tỷ lệ thuận với lượng bộ nhớ mà chủ sở hữu đó cấp phát, chứ không phụ thuộc vào tổng kích thước heap.

Các tham chiếu ngược được đánh dấu bằng cú pháp +R(parent). Trình biên dịch coi chúng là tham chiếu yếu — không đóng góp vào refcount và tự động trả về none khi đối tượng đích bị giải phóng. Một công cụ lint sử dụng thuật toán Tarjan SCC trên đồ thị kiểu tham chiếu để đảm bảo cấu trúc DAG: mọi chu trình phải có ít nhất một cạnh +R(parent), nếu không chương trình sẽ bị từ chối biên dịch.

Đây là điểm khác biệt căn bản so với Rust — nơi borrow checker cũng đảm bảo an toàn bộ nhớ nhưng không cho phép cấu trúc dữ liệu chia sẻ phức tạp mà không cần Rc/RefCell.

Bộ cấp phát slab chain

Mỗi chủ sở hữu có một chuỗi slab riêng. Cấp phát chỉ đơn giản là dịch con trỏ trong slab hiện tại. Khi slab đầy, một slab mới có kích thước gấp đôi được nối vào chuỗi.

  • Cấp phát là thao tác bump pointer: tăng con trỏ, so sánh với điểm cuối
  • Không cần duyệt free-list hay metadata cấp phát tổng quát
  • Giải phóng chỉ cần duyệt chuỗi và trả từng slab về hệ thống
  • Chi phí là O(log n) cho n lần giải phóng slab, thực tế gần như hằng số

Với các cấp phát kích thước thông thường, chỉ cần một vài lần giải phóng. Đây là lợi thế lớn khi nhiều đối tượng có cùng vòng đời — ví dụ dữ liệu request trong một ứng dụng web.

NaN-boxing — giá trị động chỉ chiếm một word

Giá trị động trong List, Map và Tree của U sử dụng biểu diễn gắn thẻ 8 byte thông qua NaN-boxing. Điều này có nghĩa là:

  • Số thực double → biểu diễn IEEE thô (NaN được chuẩn hóa)
  • Số nguyên nhỏ → payload được gắn thẻ
  • Con trỏ → payload con trỏ được gắn thẻ
  • true, false, none, tombstone → các mẫu thẻ dành riêng

Hệ quả quan trọng: giá trị lá không cần cấp phát heap riêng. Một giá trị động của U chỉ chiếm một word máy nếu nó vừa với biểu diễn gắn thẻ. Thẻ và payload được trích xuất bằng mask, shift và so sánh — cực kỳ nhanh.

Danh sách với slab lũy thừa hai

List trong U dùng slab lũy thừa hai ổn định. Phần tử không di chuyển chỉ vì List tăng kích thước. Khi một phần tử nhận chỉ số logic, việc append sau đó không làm thay đổi chỉ số hay vị trí của nó.

Truy cập ngẫu nhiên đạt O(1) nhờ xác định slab bằng bit có ý nghĩa cao nhất, clz, shift và số học. Với chỉ số k, slab được tính bằng công thức slab_index = 31 - clz((k >> 2) + 1).

Tối ưu Map: tránh tra cứu ngược

Điểm đột phá trong thiết kế Map của U là nguyên tắc đầu tiên: tránh tra cứu ngược. Bảng băm truyền thống giả định key → hash → vị trí lưu trữ là cơ bản. U thì không.

Nhiều mẫu lập trình phổ biến trong PHP, JavaScript, JSON, routing, cấu hình đã bộc lộ sẵn chỉ số ổn định cần thiết thông qua:

  • Vòng lặp (iterator provenance)
  • Khóa hằng số từ trình biên dịch
  • Shape chia sẻ
  • Kết quả phân giải trước đó
  • Luồng dữ liệu của trình biên dịch

Iterator provenance

Ví dụ điển hình trong PHP:

foreach ($a as $b => $c) {
    use($a[$b]);
}

Iterator đã biết $b, $c và chỉ số ổn định hiện tại bi. Do đó $a[$b] trở thành a.values[bi] — không có tra cứu ngược. Trình biên dịch giữ lại chỉ số ổn định của vòng lặp ngoài và trong, biến $a[$b][$d] thành a.values[bi].values[di].

Nguyên tắc tương tự áp dụng cho JavaScript: khóa xuất phát từ vòng lặp mang theo provenance chỉ số ổn định ẩn. Nếu khóa chỉ dùng để quay lại Map gốc, trình biên dịch có thể không cần materialize nó.

Khóa hằng số và Symbol

Các khóa literal như $user["id"], $user["name"] không nên thực thi lại ngữ nghĩa khóa mỗi lần chạy. U chuyển chúng thành Symbol — nhưng Symbol không phải chỉ số Map phổ quát:

  • Map A: Symbol("foo") → index 7
  • Map B: Symbol("foo") → index 19
  • Map C: Symbol("foo") → vắng mặt

Pipeline là: hằng số K → Symbol chuẩn tắc → phân giải theo từng Map/shape → chỉ số ổn định i → cache i.

Shape chia sẻ

Các Map có cùng lịch sử chèn có thể chia sẻ metadata symbol-to-index. Điều này đặc biệt hữu ích cho record, JSON object, dòng cơ sở dữ liệu, header, cấu trúc cấu hình và payload API.

Resolver phân thế hệ

Thay vì di chuyển toàn bộ entry resolver khi Map tăng kích thước, U chia reverse index thành các thế hệ: G0 | G1 | G2 | G3 | ... | current.

Mỗi thế hệ có kích thước mục tiêu và thuật toán riêng:

Kích thướcResolver mặc địnhLý do
1–8Unrolled / SIMD scanMetadata tốn hơn tra cứu
9–64SIMD fingerprintsThường nằm trong L1 cache
65–4KSwissTable-styleTra cứu động mạnh mẽ
4K–64KSwiss hoặc radix nénChọn theo loại khóa
64K+ chuỗiART / Patricia nénThứ bậc bộ nhớ chi phối
Đã sealResolver đóng băngTối đa mật độ và locality

Khi một thế hệ đạt kích thước mục tiêu, nó được seal — đóng băng, có thể nén và tạo membership summary bất biến. Các thế hệ đã seal không bao giờ cần ghi lại chỉ vì thế hệ sau tồn tại.

Điều này giải quyết vấn đề rehashing — vốn đáng lo khi hash table chính là Map, nhưng ít nghiêm trọng hơn nhiều khi hashing chỉ là gia tốc reverse-index.

Bộ lọc membership phủ định

Cấu trúc kiểu Bloom không phải chỉ số Map có thẩm quyền. Chúng là bộ lọc phủ định nén tùy chọn đặt trước các thế hệ resolver:

  • Kết quả âm → chắc chắn không có trong thế hệ này
  • Kết quả dương → chỉ có thể có, cần probe resolver thật và kiểm tra bằng chính xác khóa

Điều này đặc biệt giá trị khi các thế hệ resolver lớn và lạnh. Một vài summary nén có thể nằm trong cache trong khi SwissTable hoặc ART thật thì không.

Tra cứu song song và pipeline

Các thế hệ resolver độc lập nhau. Các probe không có phụ thuộc tuần tự,所以 có thể phát lệnh load đồng thời thay vì chờ thế hệ này miss rồi mới chạm thế hệ tiếp theo.

Với vòng lặp chứa nhiều tra cứu động, U có thể pipeline theo cả hai chiều: nhiều khóa × nhiều thế hệ ứng viên. Chỉ số đo lường vòng lặp nóng trở thành số khóa được phân giải mỗi chu kỳ, không chỉ độ trễ tra cứu lạnh.

Hệ thống sở hữu, capability và xác định

U mặc định các hàm là -E-D — không có hiệu ứng (effect-free) và xác định (deterministic). Điều này giúp việc thực thi, memoize, fold và chuyên biệt hóa trở nên dễ dàng hơn.

Các thao tác có hiệu ứng yêu cầu capability tiếp cận được thông qua tham số. Filesystem, network, crypto, database là capability chứ không phải biến toàn cục. Trên tham chiếu, +E là góc nhìn capability tại thời điểm biên dịch. Phép gán có thể thu hẹp capability nhưng không bao giờ mở rộng.

Các tham chiếu ngược +R(parent) không tham gia sở hữu mạnh, không thể mutate qua tham chiếu ngược, và có thể mang capability sự kiện.

Ý nghĩa với lập trình viên Việt Nam

Đối với cộng đồng phát triển phần mềm Việt Nam — đặc biệt các nhóm đang xây dựng backend PHP, Node.js hoặc API hiệu năng cao — cách tiếp cận của U mang lại góc nhìn thú vị:

  • Ứng dụng web: dữ liệu request có vòng đời rõ ràng, phù hợp với mô hình owner-scoped allocation
  • Xử lý JSON/API: shape chia sẻ giúp phân giải khóa nhanh cho payload có cấu trúc ổn định
  • Hệ thống nhúng/IoT: không có GC pause đồng nghĩa độ trễ có thể dự đoán được
  • Transpilation từ PHP/JavaScript: các mẫu vòng lặp và truy cập literal được tối ưu gần như miễn phí

Triết lý cốt lõi rất đáng học hỏi: khi trình biên dịch đã biết vị trí, resolver nhanh nhất là không dùng resolver nào cả. Việc tối ưu một thao tác tổng quát là vô nghĩa khi cấu trúc chương trình đã khiến thao tác đó trở nên không cần thiết.

Dự án U hiện đang thảo luận sôi nổi trên Hacker News, thu hút sự quan tâm của các kỹ sư hệ thống và nhà thiết kế ngôn ngữ lập trình. Đây là một trong những nỗ lực đáng chú ý nhằm giải quyết bài toán cân bằng giữa hiệu năng, an toàn và tính biểu đạt — vốn là thách thức lâu năm của ngành công nghiệp phần mềm.

Chia sẻ:FacebookX
Nội dung tổng hợp bằng AI, mang tính tham khảo. Xem bài gốc ↗