Berikut adalah contoh dunia nyata: Titik tetap mengalikan pada kompiler lama.
Ini tidak hanya berguna pada perangkat tanpa floating point, mereka bersinar ketika datang ke presisi karena mereka memberi Anda 32 bit presisi dengan kesalahan yang dapat diprediksi (float hanya memiliki 23 bit dan lebih sulit untuk memprediksi kehilangan presisi). yaitu presisi absolut seragam pada seluruh rentang, bukannya presisi relatif dekat-seragam (float
).
Kompiler modern mengoptimalkan contoh titik tetap ini dengan baik, jadi untuk contoh lebih modern yang masih membutuhkan kode khusus penyusun, lihat
C tidak memiliki operator multiplikasi penuh (hasil 2N-bit dari input N-bit). Cara biasa untuk mengekspresikannya dalam C adalah dengan memasukkan input ke tipe yang lebih luas dan berharap kompiler mengetahui bahwa bit atas dari input tidak menarik:
// 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
}
Masalah dengan kode ini adalah bahwa kita melakukan sesuatu yang tidak dapat secara langsung diekspresikan dalam bahasa C. Kami ingin melipatgandakan dua angka 32 bit dan mendapatkan hasil 64 bit yang kami kembalikan menjadi bit 32 tengah. Namun, dalam C, perkalian ini tidak ada. Yang dapat Anda lakukan adalah mempromosikan integer ke 64 bit dan melakukan 64 * 64 = 64 multiply.
x86 (dan ARM, MIPS, dan lainnya) dapat melakukan kalikan dalam satu instruksi. Beberapa kompiler digunakan untuk mengabaikan fakta ini dan menghasilkan kode yang memanggil fungsi pustaka runtime untuk melakukan penggandaan. Pergeseran oleh 16 juga sering dilakukan oleh rutin perpustakaan (juga x86 dapat melakukan pergeseran tersebut).
Jadi kita pergi dengan satu atau dua panggilan perpustakaan hanya untuk penggandaan. Ini memiliki konsekuensi serius. Tidak hanya shiftnya yang lebih lambat, register harus dilestarikan di seluruh fungsi panggilan dan itu tidak membantu inlining dan membuka kode juga.
Jika Anda menulis ulang kode yang sama di assembler (inline) Anda dapat memperoleh peningkatan kecepatan yang signifikan.
Selain itu: menggunakan ASM bukan cara terbaik untuk menyelesaikan masalah. Sebagian besar kompiler memungkinkan Anda untuk menggunakan beberapa instruksi assembler dalam bentuk intrinsik jika Anda tidak dapat mengekspresikannya dalam C. Kompiler VS.NET2008 misalnya memperlihatkan 32 * 32 = 64 bit mul sebagai __emul dan pergeseran 64 bit sebagai __ll_rshift.
Menggunakan intrinsik Anda dapat menulis ulang fungsi dengan cara yang membuat kompiler C memiliki kesempatan untuk memahami apa yang terjadi. Ini memungkinkan kode untuk diuraikan, register dialokasikan, eliminasi subekspresi umum dan propagasi konstan dapat dilakukan juga. Anda akan mendapatkan peningkatan kinerja yang sangat besar dibandingkan kode assembler yang ditulis tangan dengan cara itu.
Untuk referensi: Hasil akhir untuk mul titik tetap untuk kompiler VS.NET adalah:
int inline FixedPointMul (int a, int b)
{
return (int) __ll_rshift(__emul(a,b),16);
}
Perbedaan kinerja pembagian titik tetap bahkan lebih besar. Saya memiliki peningkatan hingga faktor 10 untuk divisi kode titik tetap berat dengan menulis beberapa asm-lines.
Menggunakan Visual C ++ 2013 memberikan kode perakitan yang sama untuk kedua cara.
gcc4.1 dari 2007 juga mengoptimalkan versi C murni dengan baik. (Penjelajah kompiler Godbolt tidak memiliki versi gcc yang diinstal sebelumnya, tetapi mungkin versi GCC yang lebih lama dapat melakukan ini tanpa intrinsik.)
Lihat sumber + asm untuk x86 (32-bit) dan ARM pada explorer compiler Godbolt . (Sayangnya itu tidak memiliki kompiler yang cukup tua untuk menghasilkan kode buruk dari versi C murni sederhana.)
CPU modern dapat melakukan hal-hal yang tidak dimiliki operator C sama sekali , seperti popcnt
atau bit-scan untuk menemukan bit set pertama atau terakhir . (POSIX memiliki ffs()
fungsi, tetapi semantiknya tidak cocok dengan x86 bsf
/ bsr
. Lihat https://en.wikipedia.org/wiki/Find_first_set ).
Beberapa kompiler terkadang dapat mengenali loop yang menghitung jumlah bit yang ditetapkan dalam integer dan mengkompilasinya ke popcnt
instruksi (jika diaktifkan pada waktu kompilasi), tetapi jauh lebih dapat diandalkan untuk digunakan __builtin_popcnt
di GNU C, atau pada x86 jika Anda hanya menargetkan perangkat keras dengan SSE4.2: _mm_popcnt_u32
dari<immintrin.h>
.
Atau di C ++, tetapkan ke a std::bitset<32>
dan gunakan .count()
. (Ini adalah kasus di mana bahasa telah menemukan cara untuk mengekspos secara mudah implementasi popcount yang dioptimalkan melalui perpustakaan standar, dengan cara yang akan selalu dikompilasi ke sesuatu yang benar, dan dapat mengambil keuntungan dari apa pun yang didukung oleh target.) Lihat juga https : //en.wikipedia.org/wiki/Hamming_weight#Language_support .
Demikian pula, ntohl
dapat dikompilasi ke bswap
(x86 swap 32-bit untuk konversi endian) pada beberapa implementasi C yang memilikinya.
Bidang utama lain untuk intrinsik atau asm yang ditulis tangan adalah vektorisasi manual dengan instruksi SIMD. Kompiler tidak buruk dengan loop sederhana seperti dst[i] += src[i] * 10.0;
, tetapi sering melakukan buruk atau tidak melakukan auto-vektor sama sekali ketika keadaan menjadi lebih rumit. Misalnya, Anda tidak mungkin mendapatkan apa pun seperti Bagaimana menerapkan atoi menggunakan SIMD? dihasilkan secara otomatis oleh kompiler dari kode skalar.