Tối Ưu Hóa Phân Rã Ma Trận Trong Engine Game - nói dối e blog

Tối Ưu Hóa Phân Rã Ma Trận Trong Engine Game

Trong engine game của chúng tôi, một chức năng quan trọng là phân rã ma trận 4x4 thành ba thành phần cơ bản: T (dịch chuyển), R (xuyên suốt), và S (tỷ lệ). Trong đó, T có thể thu được trực tiếp từ hàng cuối cùng của ma trận. Tuy nhiên, việc phân tách SR lại đòi hỏi chi phí tính toán cao hơn, đặc biệt là R phụ thuộc vào kết quả trích xuất S.

Bối cảnh và động lực tối ưu

Qua phân tích dữ liệu, chúng tôi nhận thấy đa số ma trận trong game không chứa thành phần tỷ lệ (nghĩa là S ≈ (1,1,1)). Điều này mở ra cơ hội để tối ưu hóa quy trình xử lý: Nếu xác nhận S gần bằng 1, chúng ta có thể bỏ qua các phép tính phức tạp khi trích xuất R.

Giải pháp chi tiết

  1. Tính toán S truyền thống:
    S được tính bằng độ dài của ba vector hàng đầu tiên trong ma trận:

    1
    2
    3
    
    float scale_x = sqrt(dot(row0, row0));  
    float scale_y = sqrt(dot(row1, row1));  
    float scale_z = sqrt(dot(row2, row2));  
    

    Tuy nhiên, khi S gần bằng 1, việc tính sqrt() trở nên thừa thãi.

  2. Phương pháp kiểm tra mới:
    Chúng tôi thêm bước kiểm tra giá trị dot product trước khi tính sqrt(). Nếu giá trị gần 1 ± ε, bỏ qua sqrt().

So sánh các kỹ thuật kiểm tra gần bằng 1

Để tối ưu hóa phép so sánh f ≈ 1.0f, ba phương pháp được thử nghiệm:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// Phiên bản số học nhị phân (IEEE 754)  
static inline int equal_one_v1(float f) {  
    union { float f; uint32_t n; } u = {f};  
    return ((u.n + 0x1f) & ~0x3f) == 0x3f800000; // 0x3f800000 = 1.0f  
}  

// So sánh với EPSILON (dùng fabs)  
#define EPSILON 0.000001f  
static inline int equal_one_v2(float f) {  
    return fabs(f - 1.0f) < EPSILON;  
}  

// So sánh với EPSILON (hai điều kiện)  
static inline int equal_one_v3(float f) {  
    float delta = f - 1.0f;  
    return delta > -EPSILON && delta < EPSILON;  
}  

Kết quả benchmark trên PC:

  • equal_one_v1equal_one_v2 có hiệu năng tương đương (~1.2s cho 10^8 lần gọi).
  • equal_one_v3 chậm hơn 3 lần (~3.6s) do rẽ nhánh (branch penalty).

Phân tích ưu/nhược điểm

  • equal_one_v1 (IEEE 754):

    • Ưu: Không dùng phép toán FP → phù hợp cho nền tảng yếu về FPU.
    • Nhược: Phụ thuộc vào định dạng float (không dùng trực tiếp cho double).
  • equal_one_v2 (fabs):

    • Ưu: Dễ đọc, có thể được tối ưu bởi trình biên dịch thành lệnh AND bit.
    • Nhược: Phụ thuộc vào độ chính xác của EPSILON.
  • equal_one_v3 (hai điều kiện):

    • Ưu: Rõ ràng về logic.
    • Nhược: Gây branch misprediction → hiệu năng kém trên CPU hiện đại.

Đề xuất cho nền tảng ARM

Trên CPU ARM không hỗ trợ NEON:

  • equal_one_v1 là lựa chọn tối ưu vì phép toán nguyên nhanh hơn các phép toán FP được giả lập phần mềm.
  • Nếu hỗ trợ NEON, có thể dùng vmaxnm.f32/vminnm.f32 để tối ưu hóa.

Hướng phát triển

  • Mở rộng sang double cho phiên bản equal_one_v1 bằng cách thay đổi masks bit.
  • Thử nghiệm kỹ thuật “early exit” khi phát hiện S gần bằng 1 cho cả ba trục.
  • Tích hợp kiểm tra này vào pipeline phân rã ma trận toàn cục.

Kết luận: Việc áp dụng kỹ thuật kiểm tra giá trị gần đúng đã giảm 15-20% thời gian phân rã ma trận trong kịch bản game thực tế của chúng tôi.

0%