Tối Ưu Hóa _Ftol: Hành Trình Cải Thiện Hiệu Năng Chuyển Đổi Kiểu Số Thực Sang Nguyên - nói dối e blog

Tối Ưu Hóa _Ftol: Hành Trình Cải Thiện Hiệu Năng Chuyển Đổi Kiểu Số Thực Sang Nguyên

Trong quá trình lập trình C, mỗi khi bạn thực hiện phép chuyển đổi kiểu dữ liệu từ float sang int bằng cú pháp (int)float_v, trình biên dịch sẽ tự động sinh ra lời gọi tới hàm CRT có tên _ftol. Câu chuyện tối ưu hóa hàm hệ thống này bắt đầu từ thập niên 90, khi một người bạn làm về đồ họa 3D chia sẻ rằng phiên bản _ftol được triển khai bằng lệnh x87 có hiệu năng chậm đáng kể, nên ưu tiên sử dụng các phép toán nguyên để thay thế.

Khoảng năm 2000, khi phát triển ứng dụng trên nền tảng RISC (cụ thể là tập lệnh ARM), tôi đã từng tự xây dựng các hàm mô phỏng phép toán dấu phẩy động bằng toán nguyên. Dù mã nguồn gốc giờ đã thất truyền, nhưng kiến thức về chuẩn IEEE 754 cho số thực đơn độ chính xác vẫn còn nguyên giá trị. Gần đây, khi gặp lại vấn đề liên quan đến xử lý số thực, tôi quyết định chia sẻ kinh nghiệm này, dù biết trước có thể nhận “gạch đá” như hồi bài viết về độ chính xác số thực.

Giải pháp thay thế hiệu quả

Thay vì viết lại phiên bản toán nguyên cho _ftol từ đầu, tôi tìm kiếm trên mạng và phát hiện tài liệu từ Flipcode 1 giới thiệu hàm ftol sau:

1
2
3
4
5
6
7
8
9
int ftol(float f)
{ 
  int a = *(int*)(&f);
  int sign = (a >> 31); 
  int mantissa = (a & ((1 << 23) - 1)) | (1 << 23);
  int exponent = ((a & 0x7fffffff) >> 23) - 127;
  int r = ((unsigned int)(mantissa) << 8) >> (31 - exponent);
  return ((r ^ sign) - sign ) & ~(exponent >> 31);    
}

Công thức này vận dụng trực tiếp cấu trúc bit của chuẩn IEEE 754:

  • Dấu hiệu (sign bit): Xác định giá trị âm/dương
  • Mũ (exponent): Lưu theo dạng biased 127
  • Định trị (mantissa): Phần phân số với 23 bit

Đối chiếu hiệu năng thực tế

Để kiểm chứng, tôi xây dựng chương trình thử nghiệm trên hệ thống Intel Core 2 Duo mới mua:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include "stdio.h"
#define RDTSC _asm _emit 0x0f _asm _emit 0x31
#pragma warning (push)
#pragma warning (disable: 4035)
inline unsigned __int64 timestamp()
{
	__asm RDTSC
}
#pragma warning (pop)

// Hàm ftol từ Flipcode
int int_chop(float f) { ... }

// Test case 1: Dùng ftol tùy chỉnh
int test1(float f) { return int_chop(f); }

// Test case 2: Ép kiểu trực tiếp
int test2(float f) { return (int)f; }

// Test case 3: Dùng lệnh x87 FPU
int test3(float x)
{
	int t;
	__asm fld x 
	__asm fistp t
	return t;
}

void test(int t, int (*f)(float))
{
	// Vòng lặp đo thời gian thực thi
	...
}

Kết quả thu được (đơn vị tick CPU):

Bài thử ftol tùy chỉnh (int)f x87 FPU
Lần 1 4,449,676 4,583,873 1,491,980
Lần 2 6,097,315 4,603,592 1,662,360
Lần 3 2,427,691 4,532,759 1,445,269

Phân tích kết quả

  1. Hàm _ftol do biên dịch sinh ra không tối ưu bằng phiên bản toán nguyên (~2 lần chậm hơn)
  2. Phiên bản x87 nhanh nhất nhưng phụ thuộc chế độ làm tròn (rounding mode) của FPU
  3. Giải pháp toán nguyên đảm bảo đúng ngữ nghĩa C trong khi duy trì hiệu năng hợp lý

Mở rộng cho kiểu double

Với kiểu dữ liệu double (64-bit), kỹ thuật tương tự được áp dụng nhưng phức tạp hơn do cấu trúc IEEE 754 cho số thực kép độ chính xác:

  • Độ dài mantissa tăng từ 23 lên 52 bit
  • Độ lệch mũ (bias) thay đổi từ 127 thành 1023

Chi tiết triển khai có thể tham khảo bài viết chuyên sâu về chuyển đổi double sang int.

Kết luận

Việc hiểu rõ kiến trúc phần cứng và chuẩn biểu diễn số thực giúp chúng ta tối ưu hóa những thành phần tưởng chừng như “ngầm định” trong ngôn ngữ lập trình cấp cao. Dù các trình biên dịch hiện đại đã cải thiện đáng kể hiệu năng _ftol, việc nắm bắt cơ chế底层 vẫn mang lại lợi thế trong các hệ thống nhúng hoặc ứng dụng đồ họa đòi hỏi hiệu năng cao.


  1.  ↩︎
0%