Truy Cập Cấu Trúc Hóa Của Bảng Style - nói dối e blog

Truy Cập Cấu Trúc Hóa Của Bảng Style

Giao diện người dùng (UI) trong trò chơi của chúng tôi được xây dựng dựa trên một phiên bản fork của RmlUI, với nhiều cải tiến sâu rộng. Về cơ bản, hệ thống này hoạt động tương tự các công nghệ frontend web hiện đại, sử dụng CSS để định nghĩa bố cục giao diện. Do đó, phần lớn công việc nền tảng của chúng tôi tập trung vào việc xây dựng một engine UI hiệu quả dựa trên CSS.

Hơn một năm trước, tôi đã chia sẻ một bài blog về các tối ưu hóa đã thực hiện. Gần đây, trong quá trình phát triển trò chơi, chúng tôi phát hiện thêm một số điểm nghẽn hiệu năng và đang tiếp tục tối ưu hóa. Bài viết này sẽ ghi lại một trong những cải tiến quan trọng.

Theo cách trừu tượng hóa hiện tại của engine, mỗi style thực chất là một danh sách các thuộc tính (attrib). Mỗi attrib lại là một cặp khóa-giá trị (k/v). Mặc dù khóa được sử dụng như chuỗi ký tự, nhưng thực tế nó được chuyển đổi thành ID số trong khoảng [0,127]. Điều này có nghĩa engine chỉ hỗ trợ khoảng 100 khóa khác nhau. Tuy nhiên, với các quy tắc CSS hiện có của RmlUI, số lượng này là đủ dùng.

Về phía giá trị (value), đây cũng là một chuỗi ký tự, nhưng cấu trúc của nó phụ thuộc vào khóa tương ứng. Module stylecache không quan tâm đến cấu trúc này và xử lý tất cả như chuỗi. Tùy theo khóa, chuỗi có thể đại diện cho kiểu boolean, số, chuỗi ký tự, danh sách số hoặc thậm chí cấu trúc phức tạp như từ điển.

Vì RmlUI được viết bằng C++, khi trích xuất style từ chuỗi, chúng tôi chuyển đổi nó thành lớp đối tượng có thể truy cập dễ dàng trong C++. Trước đây, quá trình này được thực hiện thông qua cơ chế serial hóa và giải nén (deserialize). Khi lấy attrib từ style, hệ thống sẽ giải nén attrib thành đối tượng C++ để sử dụng thuận tiện.

Tuy nhiên, quy trình giải nén này đã bộc lộ vấn đề hiệu năng và cần được tối ưu. Chúng tôi tập trung vào hai hướng tiếp cận:

Thứ nhất: Ánh xạ trực tiếp đối tượng C++ thành khối nhớ liên tục, sau đó quản lý khối nhớ này như chuỗi trong stylecache. Cách này loại bỏ bước giải nén và không cần tạo đối tượng mới mỗi lần truy cập attrib (đối tượng đóng vai trò bộ truy cập dữ liệu). Hiện tại, chúng tôi đã hoàn thiện phần này và ghi nhận cải thiện đáng kể hiệu năng.

Thứ hai: Đối với cấu trúc phức tạp như từ điển, việc ánh xạ vào khối nhớ liên tục gặp khó khăn do cần thông tin chỉ mục bổ sung (thường là con trỏ trong C++). Con trỏ không dễ dàng sao chép hoặc di chuyển, nên không thể xử lý như chuỗi thông thường.

Do đó, chúng tôi chuyển sang hướng tiếp cận thứ hai: Sử dụng bộ đệm (cache) cho các đối tượng truy cập.

Một cấu trúc dữ liệu phức tạp có thể tồn tại ở hai dạng:

  1. Chuỗi ký tự
  2. Đối tượng C++

Hai dạng này hoàn toàn tương đương về mặt thông tin. Khi cả hai đều bất biến, đối tượng C++ đóng vai trò bộ truy cập cho chuỗi. Nếu đối tượng có thể được tạo từ chuỗi qua hàm giải nén và chuyển ngược thành chuỗi qua hàm serial hóa, hệ thống có thể tự do chuyển đổi giữa hai dạng. Điều này giúp đơn giản hóa quản lý vòng đời dữ liệu.

Chuỗi ký tự có ưu điểm dễ sao chép, tính hash, loại bỏ trùng lặp và so sánh. Tuy nhiên, việc truy cập các thành phần con bên trong lại phức tạp. Vì vậy, giao diện bên ngoài cung cấp attrib dưới dạng đối tượng C++ để người dùng dễ dàng thao tác cấu trúc dữ liệu. Trong khi đó, nội bộ engine sẽ lưu trữ attrib dưới dạng chuỗi serial hóa, đồng thời cache một handle (con trỏ gián tiếp) đến bộ truy cập C++. Handle có thể nhỏ gọn, ví dụ 16-bit cho phép cache tối đa 64K bộ truy cập. Khi cache失效 (hết hiệu lực), hệ thống có thể tái tạo bộ truy cập từ chuỗi.

Lưu ý về quản lý vòng đời bộ truy cập:
Người dùng cần cung cấp hàm hủy (release) để giải phóng bộ truy cập khi cache đầy. Ngoài ra, người dùng có thể thêm cơ chế đếm tham chiếu (reference counting) để giữ lại bộ truy cập. Khi lấy bộ truy cập từ interface, người dùng tăng đếm tham chiếu để giữ nó tồn tại, sau đó truyền lại cho stylecache (giảm đếm tham chiếu khi không dùng nữa). Việc giữ và tái sử dụng attrib cụ thể có chi phí O(1).

Cụ thể:

  • Khi nhận attrib mới từ bên ngoài, stylecache sẽ serial hóa thành chuỗi và giữ chuỗi này, tránh phụ thuộc vào vòng đời dữ liệu gốc (dữ liệu được sao chép một lần).
  • Khi lấy attrib từ stylecache, bộ truy cập trả về có vòng đời tồn tại đến lần gọi API tiếp theo. Người dùng không cần quản lý vòng đời, nhưng nếu lập tức truyền bộ truy cập này lại cho stylecache, hệ thống có thể tìm thấy trong cache và bỏ qua bước serial hóa (zero-copy).

Giao diện yêu cầu người dùng định nghĩa ba hàm:

1
2
3
4
typedef void * accessor_t; // Bộ truy cập, có thể là đối tượng C++
accessor_t (*create)(const char *, size_t); // Tạo bộ truy cập từ chuỗi
void (*release)(accessor_t); // Hủy bộ truy cập
size_t (*serialize)(accessor_t, char buf[], size_t buf_sz); // Serial hóa bộ truy cập thành chuỗi

Khi giao diện ở tầng style cần trao đổi dữ liệu attrib, tất cả đều sử dụng kiểu bộ truy cập. Quy ước:

  • Tham số đầu vào: Người gọi quản lý vòng đời bộ truy cập
  • Tham số đầu ra: Bộ truy cập trả về tồn tại đến lần gọi API tiếp theo

Cách tiếp cận này không làm tăng chi phí quản lý vòng đời, đồng thời cung cấp bộ truy cập C++ tiện lợi cho người dùng.

Tôi viết bài blog này vì cho rằng giải pháp trên có tính ứng dụng cao và đáng được ghi lại. Trong các giải pháp C++ truyền thống, nếu muốn xử lý đối tượng như kiểu cơ bản,

0%