GitHub đã công bố một bước đột phá trong kỹ thuật xử lý văn bản quy mô lớn: thuật toán case-folding (gấp chữ hoa/thường) mới có thể xử lý hơn 45 gigabyte dữ liệu mỗi giây trên một lõi CPU duy nhất. Thành tựu này là chìa khóa cho công cụ tìm kiếm mã nguồn Blackbird của GitHub, khi phải index hơn 480 terabyte mã nguồn từ 180 triệu repository. Bài viết phân tích cách GitHub đạt được tốc độ gần bằng băng thông bộ nhớ thông qua một cách tiếp cận phản trực giác: loại bỏ tối ưu hóa ‘dừng sớm’ và áp dụng vòng lặp không nhánh (branch-free loop).

Case-folding là gì và tại sao nó quan trọng với GitHub?

Case-folding là quá trình chuyển đổi văn bản về một dạng chuẩn hóa để so sánh, xóa bỏ sự khác biệt về chữ hoa và chữ thường. Điều này giúp một tìm kiếm với từ khóa ‘café’ có thể khớp với mã nguồn chứa ‘CAFÉ’, hoặc ‘straße’ khớp với ‘STRASSE’. Đây là một thao tác cơ bản nhưng cực kỳ quan trọng đối với các hệ thống như công cụ tìm kiếm, cờ regex (?i), hay việc kiểm tra tên người dùng và tên máy chủ không phân biệt hoa/thường.

Tại GitHub, mọi byte mã nguồn trong hơn 180 triệu repository đều được case-fold trước khi trích xuất n-gram và xây dựng chỉ mục cho Blackbird. Với mỗi kết quả tìm kiếm tiềm năng, một thao tác case-folding khác (ẩn hoặc hiện) lại được thực thi để định vị các kết quả khớp. Ở quy mô này, tốc độ của một thao tác cơ bản cũng trở thành yếu tố then chốt ảnh hưởng đến hiệu suất tổng thể.

case-folding – hình ảnh 2
case-folding – Ảnh từ GitHub Blog.

Sự khác biệt giữa case-folding và lowercasing

Nhiều người có thể nhầm lẫn case-folding với việc chuyển đổi sang chữ thường (lowercasing), nhưng hai thao tác này có mục đích hoàn toàn khác biệt:

  • Lowercasing dành cho mục đích hiển thị. Nó nhạy cảm với ngữ cảnh và ngôn ngữ (locale). Ví dụ, chữ sigma cuối trong tiếng Hy Lạp (ς) chỉ chuyển thành chữ thường ở cuối từ, trong khi chữ ‘I’ trong tiếng Thổ Nhĩ Kỳ chuyển đổi khác với tiếng Anh.
  • Case-folding dành cho mục đích so sánh. Nó được thiết kế một cách có chủ đích để độc lập với ngữ cảnh và ngôn ngữ. Mục tiêu là tạo ra một quan hệ ổn định và đối xứng: nếu A được fold để khớp với B, thì B cũng phải được fold để khớp với A trong bất kỳ ngôn ngữ nào.

Cơ sở dữ liệu ký tự Unicode (Unicode Character Database) cung cấp một tệp CaseFolding.txt riêng biệt cho mục đích này. Sự khác biệt thể hiện rõ trên các ký tự thực tế như ‘ß’, ‘İ’, hay sigma cuối. Việc sử dụng lowercasing thay thế cho case-folding sẽ dẫn đến các kết quả khớp sai lệch một cách thầm lặng. Thư viện casefold của GitHub chỉ triển khai các phép fold đơn giản (1-1) – tương ứng với trạng thái C và S trong CaseFolding.txt – và không bao gồm các phép fold đa ký tự (như ‘ß’ → ‘ss’) hay các phép fold theo ngôn ngữ Thổ Nhĩ Kỳ. Đây không phải là lựa chọn bất thường; các công cụ phổ biến như ripgrep cũng áp dụng hạn chế tương tự để đảm bảo tính nhất quán.

case-folding – hình ảnh 3
case-folding – Ảnh từ GitHub Blog.

Lõi phản trực giác: Đừng dừng sớm

Vì GitHub chủ yếu xử lý mã nguồn, phần lớn văn bản là ASCII. Do đó, tối ưu hóa đường dẫn ASCII để chạy ở tốc độ bộ nhớ là ưu tiên hàng đầu. Phép fold của một chữ cái ASCII rất đơn giản: ‘A’ đến ‘Z’ được ánh xạ thành ‘a’ đến ‘z’, mọi thứ khác giữ nguyên. Một cách tiếp cận ngây thơ có thể là duyệt buffer và dừng lại ngay khi gặp byte không phải ASCII để chuyển sang đường dẫn Unicode phức tạp hơn.

Tuy nhiên, các kỹ sư GitHub phát hiện ra rằng chính việc ‘dừng sớm’ này lại là rào cản cho việc tối ưu hóa. Một vòng lặp có lệnh thoát sớm phụ thuộc vào dữ liệu sẽ ngăn trình biên dịch vector hóa mã. Giải pháp là xóa bỏ mọi nhánh (branch) trong vòng lặp:

  1. Loại bỏ điều kiện dừng sớm (if b >= 0x80 { break }).
  2. Thay thế kiểm tra phạm vi ‘A’..=’Z’ bằng phép toán số học không nhánh: b.wrapping_sub(b'A') < 26.
  3. Thay thế ghi có điều kiện bằng một phép ghi luôn được thực thi, sử dụng mặt nạ bit: *b |= u8::from(is_upper) << 5 (thiết lập bit 5 để chuyển chữ hoa thành chữ thường).

Kết quả là một vòng lặp không có luồng điều khiển phụ thuộc vào dữ liệu, cho phép LLVM phát ra các lệnh NEON xử lý 16 byte cùng lúc, đạt tốc độ >45 GiB/s – gần như bằng băng thông bộ nhớ. Đồng thời, vòng lặp vẫn tích lũy thông tin về sự hiện diện của byte không phải ASCII thông qua một biến tích lũy (high_bit_acc), để quyết định có cần xử lý tiếp bằng đường dẫn Unicode hay không.

Bảng so sánh hiệu suất các bước tối ưu

Phiên bản Thông lượng (Trên ASCII thuần túy) Được vector hóa?
Ngây thơ (có break + kiểm tra nhánh) ~3.1 GiB/s Không
Kiểm tra/ghi không nhánh, vẫn giữ break ~2.6 GiB/s Không
Bỏ lệnh thoát sớm (break) ~7.6 GiB/s Một phần
Kiểm tra & ghi không nhánh (vòng lặp hoàn chỉnh) >45 GiB/s Hoàn toàn

Một bài học quan trọng là: mã không nhánh chỉ có lợi khi nó cho phép vector hóa. Trong mã vô hướng (scalar code), nó thậm chí có thể làm chậm hơn do luôn thực hiện ghi vào bộ nhớ, thay vì chỉ ghi khi cần thiết như trong mã có nhánh được dự đoán tốt.

case-folding – hình ảnh 4
case-folding – Ảnh từ GitHub Blog.

Tránh cấp phát bộ nhớ không cần thiết

Để đạt được 45 GiB/s, việc tránh cấp phát bộ nhớ heap không cần thiết là yếu tố sống còn. Hàm simple_fold nhận một String làm đầu vào, sở hữu buffer heap của nó để có thể biến đổi và trả về. Nếu văn bản là ASCII thuần túy, nó đã được fold ngay tại chỗ và cùng một vùng cấp phát được trả về thẳng, không cần buffer thứ hai hay sao chép.

Nếu có ký tự không phải ASCII, thuật toán sẽ quét từ byte đó trở đi. Buffer đầu ra chỉ được cấp phát khi gặp một ký tự mà việc fold làm thay đổi độ dài byte. Hai ký tự duy nhất trong Unicode 16.0 làm tăng độ dài khi fold là U+023A (Ⱥ) và U+023E (Ɀ) (từ 2 byte lên 3 byte). Dựa trên điều này, thuật toán cấp phát trước một buffer với dung lượng tối đa là 1.5 lần độ dài đầu vào, đảm bảo đủ chỗ cho trường hợp xấu nhất mà không cần kiểm tra hay tái cấp phát lại trong quá trình xử lý.

Tối ưu hóa đường dẫn Unicode

Mặc dù hiếm khi xảy ra trong mã nguồn, đường dẫn xử lý Unicode cũng được tối ưu hóa triệt để. Unicode 16.0 có 1484 ánh xạ fold đơn giản, nhưng chúng phân bố rất thưa thớt và có cấu trúc. GitHub đã nén chúng xuống chỉ còn 1776 byte bằng cách:

  1. Sử dụng bitmap trang (page bitmap): Không gian mã được chia thành các ‘trang’ 64 mã điểm. Chỉ 59 trong số ~1960 trang là chứa các ký tự có thể fold. Một bitmap một-bit-một-trang cho phép từ chối nhanh chóng các ký tự không fold (bit bằng 0) mà không cần giải mã.
  2. Nén theo đoạn (run compression): Các mã điểm có thể fold thường xuất hiện thành cụm liền kề với cùng một delta (độ chênh lệch) khi fold. Thay vì lưu từng ánh xạ, thuật toán lưu các đoạn (run) với điểm bắt đầu, kết thúc, bước nhảy và delta. Kỹ thuật này giảm ~1484 fold riêng lẻ xuống chỉ còn 238 đoạn trên 59 trang.
  3. Tìm kiếm rộng (wide search): Trong một trang, việc tìm kiếm đoạn phù hợp được thực hiện bằng cách so sánh đồng thời 8 khóa (key) sử dụng kỹ thuật SWAR (SIMD Within A Register), thay vì lặp tuần tự, giảm thiểu số lần rẽ nhánh.
  4. Fold như một phép cộng byte: Trên kiến trúc little-endian, việc fold một ký tự có thể được thực hiện bằng cách cộng một hằng số vào một từ (word) 4-byte đại diện cho mã UTF-8 gốc.

Tác động với cộng đồng lập trình viên Việt Nam

Công trình này của GitHub minh họa một nguyên lý tối ưu hóa sâu sắc có thể áp dụng rộng rãi: đôi khi, việc loại bỏ một tối ưu hóa cục bộ (như thoát vòng lặp sớm) lại mở đường cho một tối ưu hóa tổng thể mạnh mẽ hơn nhiều (vector hóa toàn bộ). Điều này đặc biệt có giá trị khi xử lý khối lượng dữ liệu văn bản lớn, không chỉ trong công cụ tìm kiếm mã nguồn mà còn trong các hệ thống xử lý ngôn ngữ tự nhiên, phân tích log, hay index cơ sở dữ liệu.

Việc GitHub công bố thư viện casefold dưới dạng mã nguồn mở (Rust crate) cũng tạo cơ hội cho các kỹ sư phần mềm Việt Nam học hỏi các kỹ thuật low-level tinh vi, từ số học byte không nhánh, tối ưu hóa bộ nhớ đệm, đến thiết kế cấu trúc dữ liệu chuyên biệt cho bài toán cụ thể. Hiểu biết này có thể giúp cải thiện hiệu suất cho các sản phẩm công nghệ nội địa khi chúng phát triển đến quy mô cần xử lý dữ liệu lớn.

Câu hỏi thường gặp (FAQ)

Case-folding khác gì với việc chuyển sang chữ thường (to_lowercase)?

Case-folding dùng để so sánh và không phụ thuộc ngôn ngữ, trong khi to_lowercase dùng để hiển thị và phụ thuộc vào ngữ cảnh và ngôn ngữ. Sử dụng to_lowercase thay cho case-folding có thể dẫn đến kết quả so khớp sai với các ký tự đặc biệt như ‘ß’.

Tại sao việc ‘dừng sớm’ lại làm chậm quá trình?

Lệnh thoát vòng lặp sớm dựa trên dữ liệu (data-dependent early exit) tạo ra một điểm rẽ nhánh trong mỗi lần lặp. Điều này ngăn cản trình biên dịch áp dụng các kỹ thuật vector hóa, vốn yêu cầu vòng lặp có luồng điều khiển cố định và dự đoán được.

Thuật toán này có xử lý được tiếng Việt có dấu không?

Có. Các ký tự Unicode của tiếng Việt (nằm trong khối Latin Extended) đều được hỗ trợ bởi thuật toán case-folding này. Tuy nhiên, do mã nguồn chủ yếu là ASCII, các ký tự tiếng Việt sẽ được xử lý qua đường dẫn Unicode được tối ưu hóa riêng.

Thư viện casefold có thể sử dụng trong dự án của tôi không?

Có. GitHub đã công bố thư viện này như một Rust crate mã nguồn mở. Bạn có thể tích hợp nó vào các dự án Rust của mình để thực hiện case-folding hiệu suất cao. Cần kiểm tra trang chính thức của crate để biết điều kiện sử dụng và hướng dẫn tích hợp cụ thể.

GitHub Blog

Nguồn: GitHub Blog.