Tối ưu hoá theo cấu hình (profile)

Tối ưu hoá theo cấu hình (PGO) là một kỹ thuật tối ưu hoá trình biên dịch nổi tiếng. Trong PGO, các cấu hình thời gian chạy từ quá trình thực thi của một chương trình sẽ được trình biên dịch sử dụng để đưa ra lựa chọn tối ưu về cách chèn cùng dòng và bố cục mã. Điều này giúp cải thiện hiệu quả hoạt động và giảm kích thước mã.

Bạn có thể triển khai PGO trên ứng dụng hoặc thư viện theo các bước sau: 1. Xác định khối lượng công việc tiêu biểu. 2. Thu thập cấu hình. 3. Sử dụng các cấu hình trong Bản phát hành.

Bước 1: Xác định khối lượng công việc tiêu biểu

Trước tiên, hãy xác định điểm chuẩn tiêu biểu hoặc khối lượng công việc tiêu biểu cho ứng dụng. Đây là một bước quan trọng vì cấu hình được thu thập từ khối lượng công việc sẽ xác định các vùng nóng (hot) và nguội (cold) trong mã. Khi sử dụng các cấu hình này, trình biên dịch sẽ thực hiện tối ưu hoá linh hoạt và chèn cùng dòng tại các vùng nóng. Trình biên dịch cũng có thể chọn giảm kích thước mã của các vùng lạnh trong khi đánh đổi hiệu suất.

Việc xác định được khối lượng công việc phù hợp cũng giúp ích cho việc theo dõi hiệu suất nói chung.

Bước 2: Thu thập cấu hình

Việc thu thập cấu hình gồm 3 bước: – tạo mã gốc bằng khả năng đo lường, – chạy ứng dụng được đo lường trên thiết bị và tạo cấu hình, và – hợp nhất/xử lý hậu kỳ các cấu hình trên máy chủ.

Tạo bản dựng được đo lường

Các cấu hình được thu thập bằng cách chạy khối lượng công việc từ bước 1 trên bản dựng được đo lường của ứng dụng. Để tạo một bản dựng được đo lường, hãy thêm -fprofile-generate vào cờ trình biên dịch và trình liên kết. Cờ này phải được kiểm soát bởi một biến thể xây dựng riêng biệt vì đây là cờ không cần thiết trong quá trình xây dựng mặc định.

Tạo cấu hình

Tiếp theo, hãy chạy ứng dụng được đo lường trên thiết bị và tạo cấu hình. Các cấu hình được thu thập trong bộ nhớ khi tệp nhị phân được đo lường đang chạy và được ghi vào tệp khi thoát. Tuy nhiên, các chức năng được đăng ký bằng atexit không được gọi trong ứng dụng Android – ứng dụng sẽ bị huỷ.

Ứng dụng/khối lượng công việc phải thực hiện thêm công việc để đặt đường dẫn cho tệp cấu hình, sau đó kích hoạt việc ghi cấu hình một cách rõ ràng.

  • Để đặt đường dẫn tệp cấu hình, hãy gọi __llvm_profile_set_filename(PROFILE_DIR "/default-%m.profraw. %m rất hữu ích khi có nhiều thư viện dùng chung. %m` mở rộng thành một chữ ký mô-đun duy nhất cho thư viện đó, dẫn đến mỗi thư viện có một cấu hình riêng. Xem tại đây để biết các thông số mẫu hữu ích khác. PROFILE_DIR là một thư mục có thể ghi từ ứng dụng. Xem bản minh hoạ để biết cách phát hiện thư mục này trong thời gian chạy.
  • Để kích hoạt quá trình ghi cấu hình một cách rõ ràng, hãy gọi hàm __llvm_profile_write_file.
extern "C" {
extern int __llvm_profile_set_filename(const char*);
extern int __llvm_profile_write_file(void);
}

#define PROFILE_DIR "<location-writable-from-app>"
void workload() {
  // ...
  // run workload
  // ...

  // set path and write profiles after workload execution
  __llvm_profile_set_filename(PROFILE_DIR "/default-%m.profraw");
  __llvm_profile_write_file();
  return;
}

NB: Việc tạo tệp cấu hình sẽ đơn giản hơn nếu khối lượng công việc là một tệp nhị phân độc lập – chỉ cần đặt biến môi trường LLVM_PROFILE_FILE thành %t/default-%m.profraw trước khi chạy tệp nhị phân đó.

Xử lý hậu kỳ cho cấu hình

Các tệp cấu hình có định dạng .profraw. Trước tiên, bạn phải tìm nạp các tệp này từ thiết bị bằng cách sử dụng adb pull. Sau khi tìm nạp, hãy sử dụng tiện ích llvm-profdata trong NDK để chuyển đổi từ .profraw thành .profdata – tệp có thể được truyền đến trình biên dịch sau đó.

$NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-profdata \
    merge --output=pgo_profile.profdata \
    <list-of-profraw-files>

Sử dụng llvm-profdataclang từ cùng một bản phát hành NDK để tránh tình trạng không khớp phiên bản của định dạng tệp cấu hình.

Bước 3 Sử dụng cấu hình để tạo ứng dụng

Sử dụng cấu hình từ bước trước đó trong bản phát hành ứng dụng bằng cách truyền -fprofile-use=<>.profdata cho trình biên dịch và trình liên kết. Bạn có thể sử dụng cấu hình ngay cả khi mã phát triển – trình biên dịch Clang có thể chấp nhận thông tin không khớp không đáng kể giữa nguồn và cấu hình.

NB: Nói chung, đối với hầu hết thư viện, các cấu hình này phổ biến trên tất cả các cấu trúc. Ví dụ: Bạn có thể sử dụng cấu hình tạo từ bản dựng arm64 của thư viện cho tất cả các cấu trúc. Lưu ý là nếu có đường dẫn mã dành riêng cho từng cấu trúc trong thư viện (arm so với x86 hoặc 32 bit so với 64 bit), thì bạn nên sử dụng các cấu hình (profile) riêng cho từng cấu hình.

Kết hợp kiến thức đã học

https://github.com/DanAlbert/ndk-samples/tree/pgo/pgo giới thiệu bản minh hoạ từ đầu đến cuối về cách sử dụng PGO từ một ứng dụng. Tài liệu này cung cấp thêm thông tin chi tiết được trình bày lướt qua trong tài liệu này.

  • Quy tắc xây dựng CMake cho biết cách thiết lập biến CMake tạo mã gốc có khả năng đo lường. Khi bạn không đặt biến thể xây dựng, mã gốc sẽ được tối ưu hoá bằng cách sử dụng các cấu hình PGO đã tạo trước đó.
  • Trong một bản dựng được đo lường, pgodemo.cpp sẽ ghi các cấu hình là quá trình thực thi khối lượng công việc.
  • Vị trí có thể ghi cho các cấu hình được thu thập trong thời gian chạy trong MainActivity.kt bằng cách sử dụng applicationContext.cacheDir.toString().
  • Để lấy cấu hình từ thiết bị mà không cần adb root, hãy sử dụng công thức adb tại đây.