Dưới đây là một ví dụ thực tế: Nhân số điểm cố định trên các trình biên dịch cũ.
Chúng không chỉ hữu dụng trên các thiết bị không có điểm nổi, chúng tỏa sáng khi có độ chính xác vì chúng cung cấp cho bạn độ chính xác 32 bit với một lỗi có thể dự đoán được (float chỉ có 23 bit và khó dự đoán độ chính xác hơn). tức là đồng phục tuyệt đối độ chính xác trên toàn bộ phạm vi, thay vì độ chính xác tương đối gần đồng nhất ( float
).
Trình biên dịch hiện đại tối ưu hóa ví dụ điểm cố định này một cách độc đáo, vì vậy đối với các ví dụ hiện đại hơn vẫn cần mã dành riêng cho trình biên dịch, hãy xem
C không có toán tử nhân đầy đủ (kết quả 2N bit từ các đầu vào bit N). Cách thông thường để diễn đạt nó trong C là chuyển các đầu vào sang loại rộng hơn và hy vọng trình biên dịch nhận ra rằng các bit trên của các đầu vào không thú vị:
// on a 32-bit machine, int can hold 32-bit fixed-point integers.
int inline FixedPointMul (int a, int b)
{
long long a_long = a; // cast to 64 bit.
long long product = a_long * b; // perform multiplication
return (int) (product >> 16); // shift by the fixed point bias
}
Vấn đề với mã này là chúng tôi làm một cái gì đó không thể diễn đạt trực tiếp bằng ngôn ngữ C. Chúng tôi muốn nhân hai số 32 bit và nhận được kết quả 64 bit, trong đó chúng tôi trả về 32 bit giữa. Tuy nhiên, trong C nhân này không tồn tại. Tất cả những gì bạn có thể làm là quảng bá các số nguyên lên 64 bit và thực hiện phép nhân 64 * 64 = 64.
x86 (và ARM, MIPS và những người khác) tuy nhiên có thể thực hiện phép nhân trong một lệnh đơn. Một số trình biên dịch được sử dụng để bỏ qua thực tế này và tạo mã gọi hàm thư viện thời gian chạy để thực hiện phép nhân. Sự thay đổi của 16 cũng thường được thực hiện bởi một thói quen thư viện (cũng như x86 có thể thực hiện các ca như vậy).
Vì vậy, chúng tôi còn lại với một hoặc hai cuộc gọi thư viện chỉ để nhân lên. Điều này có hậu quả nghiêm trọng. Không chỉ là sự thay đổi chậm hơn, các thanh ghi phải được bảo toàn trong các lệnh gọi hàm và nó cũng không giúp nội tuyến và không kiểm soát mã.
Nếu bạn viết lại cùng một mã trong trình biên dịch (nội tuyến), bạn có thể tăng tốc đáng kể.
Thêm vào đó: sử dụng ASM không phải là cách tốt nhất để giải quyết vấn đề. Hầu hết các trình biên dịch cho phép bạn sử dụng một số hướng dẫn trình biên dịch ở dạng nội tại nếu bạn không thể diễn đạt chúng trong C. Trình biên dịch VS.NET2008 cho thấy 32 * 32 = 64 bit mul là __emul và dịch chuyển 64 bit là __ll_rshift.
Sử dụng nội tại, bạn có thể viết lại hàm theo cách mà trình biên dịch C có cơ hội hiểu những gì đang diễn ra. Điều này cho phép mã được nội tuyến, đăng ký được phân bổ, loại bỏ phổ biến phụ và lan truyền liên tục cũng có thể được thực hiện. Bạn sẽ nhận được rất nhiều cải tiến hiệu suất so với mã trình biên dịch viết tay theo cách đó.
Để tham khảo: Kết quả cuối cùng cho mul điểm cố định cho trình biên dịch VS.NET là:
int inline FixedPointMul (int a, int b)
{
return (int) __ll_rshift(__emul(a,b),16);
}
Sự khác biệt hiệu suất của phân chia điểm cố định thậm chí còn lớn hơn. Tôi đã cải thiện đến hệ số 10 để phân chia mã điểm cố định nặng bằng cách viết một vài dòng asm-lines.
Sử dụng Visual C ++ 2013 cho cùng một mã lắp ráp cho cả hai cách.
gcc4.1 từ năm 2007 cũng tối ưu hóa phiên bản C tinh khiết độc đáo. (Trình thám hiểm trình biên dịch Godbolt không cài đặt bất kỳ phiên bản gcc nào trước đó, nhưng có lẽ các phiên bản GCC cũ hơn có thể thực hiện việc này mà không cần nội tại.)
Xem nguồn + asm cho x86 (32-bit) và ARM trên trình khám phá trình biên dịch Godbolt . (Thật không may, nó không có trình biên dịch nào đủ cũ để tạo mã xấu từ phiên bản C thuần túy đơn giản.)
CPU hiện đại có thể làm những việc C không có nhà khai thác cho ở tất cả , giống như popcnt
hoặc bit quét để tìm các bit set đầu tiên hoặc cuối cùng . (POSIX có ffs()
chức năng, nhưng ngữ nghĩa của nó không khớp với x86 bsf
/ bsr
. Xem https://en.wikipedia.org/wiki/Find_first_set ).
Một số trình biên dịch đôi khi có thể nhận ra một vòng lặp mà đếm số lượng các thiết lập bit trong một số nguyên và biên dịch nó thành một popcnt
hướng dẫn (nếu được kích hoạt tại thời gian biên dịch), nhưng nó nhiều hơn đáng tin cậy để sử dụng __builtin_popcnt
trong GNU C, hoặc trên x86 nếu bạn chỉ phần cứng nhắm mục tiêu với SSE4.2: _mm_popcnt_u32
từ<immintrin.h>
.
Hoặc trong C ++, assign để một std::bitset<32>
và sử dụng .count()
. (Đây là một trường hợp ngôn ngữ đã tìm ra cách để portably phơi bày một thực hiện tối ưu hóa của popcount thông qua thư viện chuẩn, trong một cách mà sẽ luôn luôn biên dịch một cái gì đó đúng, và có thể tận dụng lợi thế của bất cứ sự hỗ trợ mục tiêu.) Xem thêm https : //en.wikipedia.org/wiki/ Hamming_ weight # L Language_support .
Tương tự, ntohl
có thể biên dịch thành bswap
(hoán đổi byte 32 bit để chuyển đổi cuối) trên một số triển khai C có nó.
Một lĩnh vực chính khác cho nội tại hoặc asm viết tay là vector hóa thủ công với các hướng dẫn SIMD. Trình biên dịch không tệ với các vòng lặp đơn giản như dst[i] += src[i] * 10.0;
, nhưng thường làm xấu hoặc không tự động vector hóa khi mọi thứ trở nên phức tạp hơn. Ví dụ: bạn không thể nhận được bất cứ điều gì như Cách triển khai atoi bằng SIMD? được tạo tự động bởi trình biên dịch từ mã vô hướng.