Saya telah menggali beberapa bagian dari kernel Linux, dan menemukan panggilan seperti ini:
if (unlikely(fd < 0))
{
/* Do something */
}
atau
if (likely(!err))
{
/* Do something */
}
Saya telah menemukan definisi mereka:
#define likely(x) __builtin_expect((x),1)
#define unlikely(x) __builtin_expect((x),0)
Saya tahu itu untuk optimasi, tetapi bagaimana cara kerjanya? Dan berapa banyak penurunan kinerja / ukuran yang dapat diharapkan dari menggunakannya? Dan apakah itu sepadan dengan kerumitan (dan mungkin kehilangan portabilitas) setidaknya dalam kode bottleneck (di userspace, tentu saja).
BOOST_LIKELY
__builtin_expect
pada pertanyaan lain.#define likely(x) (x)
dan#define unlikely(x) (x)
pada platform yang tidak mendukung petunjuk semacam ini.Jawaban:
Mereka memberi petunjuk kepada kompiler untuk memancarkan instruksi yang akan menyebabkan prediksi cabang lebih menyukai sisi "kemungkinan" dari instruksi lompatan. Ini bisa menjadi kemenangan besar, jika prediksi itu benar itu berarti bahwa instruksi lompatan pada dasarnya gratis dan akan mengambil nol siklus. Di sisi lain jika prediksi salah, maka itu berarti pipa prosesor perlu memerah dan dapat menghabiskan beberapa siklus. Selama prediksi itu benar sebagian besar waktu, ini akan cenderung bagus untuk kinerja.
Seperti semua optimisasi kinerja seperti itu, Anda hanya harus melakukannya setelah profil yang luas untuk memastikan kode benar-benar tersendat, dan mungkin diberi sifat mikro, yang sedang dijalankan dalam loop yang ketat. Umumnya pengembang Linux cukup berpengalaman sehingga saya akan membayangkan mereka akan melakukan itu. Mereka tidak terlalu peduli tentang portabilitas karena mereka hanya menargetkan gcc, dan mereka memiliki ide yang sangat dekat tentang perakitan yang mereka ingin hasilkan.
sumber
"[...]that it is being run in a tight loop"
, banyak CPU memiliki prediktor cabang , sehingga menggunakan makro ini hanya membantu kode waktu pertama dieksekusi atau ketika tabel sejarah ditimpa oleh cabang yang berbeda dengan indeks yang sama ke dalam tabel percabangan. Dalam loop ketat, dan dengan asumsi cabang berjalan satu arah sebagian besar waktu, prediktor cabang kemungkinan akan mulai menebak cabang yang benar dengan sangat cepat. - Temanmu dalam ilmu kesantunan.Mari kita dekompilasi untuk melihat apa yang dilakukan GCC 4.8 dengan itu
Tanpa
__builtin_expect
Kompilasi dan dekompilasi dengan GCC 4.8.2 x86_64 Linux:
Keluaran:
Urutan instruksi dalam memori tidak berubah: pertama
printf
dan kemudianputs
danretq
kembali.Dengan
__builtin_expect
Sekarang ganti
if (i)
dengan:dan kami mendapatkan:
The
printf
(dikompilasi untuk__printf_chk
) dipindahkan ke akhir fungsi, setelahputs
dan kembali untuk meningkatkan prediksi cabang seperti yang disebutkan oleh jawaban lainnya.Jadi pada dasarnya sama dengan:
Optimasi ini tidak dilakukan dengan
-O0
.Tapi semoga berhasil menulis contoh yang berjalan lebih cepat dengan
__builtin_expect
tanpa, CPU benar-benar pintar hari ini . Upaya naif saya ada di sini .C ++ 20
[[likely]]
dan[[unlikely]]
C ++ 20 telah membuat standar C ++ built-in: Bagaimana cara menggunakan atribut C ++ 20 yang kemungkinan / tidak mungkin dalam pernyataan if-else Mereka kemungkinan besar (pun!) Akan melakukan hal yang sama.
sumber
Ini adalah makro yang memberikan petunjuk kepada kompiler tentang ke mana cabang bisa pergi. Makro diperluas ke ekstensi spesifik GCC, jika tersedia.
GCC menggunakan ini untuk mengoptimalkan prediksi cabang. Misalnya, jika Anda memiliki sesuatu seperti berikut ini
Kemudian dapat merestrukturisasi kode ini menjadi sesuatu yang lebih seperti:
Keuntungannya adalah ketika prosesor mengambil cabang pertama kali, ada overhead yang signifikan, karena mungkin telah memuat dan mengeksekusi kode secara spekulatif lebih jauh ke depan. Ketika menentukan akan mengambil cabang, maka itu harus membatalkan itu, dan mulai pada target cabang.
Sebagian besar prosesor modern sekarang memiliki semacam prediksi cabang, tetapi itu hanya membantu ketika Anda telah melalui cabang sebelumnya, dan cabang tersebut masih dalam cache prediksi cabang.
Ada sejumlah strategi lain yang dapat digunakan oleh kompiler dan prosesor dalam skenario ini. Anda dapat menemukan detail lebih lanjut tentang cara kerja peramal cabang di Wikipedia: http://en.wikipedia.org/wiki/Branch_predictor
sumber
goto
s tanpa mengulangireturn x
: stackoverflow.com/a/31133787/895245Mereka menyebabkan kompiler untuk memancarkan petunjuk cabang yang sesuai di mana perangkat keras mendukungnya. Ini biasanya hanya berarti memutar-mutar beberapa bit dalam opcode instruksi, sehingga ukuran kode tidak akan berubah. CPU akan mulai mengambil instruksi dari lokasi yang diprediksi, dan menyiram pipa dan memulai kembali jika ternyata salah ketika cabang tercapai; dalam hal petunjuknya benar, ini akan membuat cabang lebih cepat - tepatnya seberapa cepat akan tergantung pada perangkat keras; dan seberapa banyak hal ini mempengaruhi kinerja kode akan tergantung pada proporsi waktu yang tepat.
Sebagai contoh, pada CPU PowerPC, cabang yang tidak disentuh mungkin memerlukan 16 siklus, yang salah mengisyaratkan 8 dan yang salah mengisyaratkan 24. Dalam loop terdalam, petunjuk yang baik dapat membuat perbedaan besar.
Portabilitas sebenarnya bukan masalah - mungkin definisinya ada di header per-platform; Anda dapat dengan mudah mendefinisikan "kemungkinan" dan "tidak mungkin" untuk platform yang tidak mendukung petunjuk cabang statis.
sumber
Konstruk ini memberi tahu kompilator bahwa ekspresi EXP kemungkinan besar akan memiliki nilai C. Nilai baliknya adalah EXP. __builtin_expect dimaksudkan untuk digunakan dalam ekspresi kondisional. Dalam hampir semua kasus, ini akan digunakan dalam konteks ekspresi boolean di mana dalam kasus ini lebih mudah untuk mendefinisikan dua makro pembantu:
Makro ini kemudian dapat digunakan seperti pada
Referensi: https://www.akkadia.org/drepper/cpumemory.pdf
sumber
__builtin_expect(!!(expr),0)
bukan hanya__builtin_expect((expr),0)
?!!
sama dengan melemparkan sesuatu kebool
. Beberapa orang suka menulis seperti ini.(komentar umum - jawaban lain mencakup detail)
Tidak ada alasan Anda kehilangan portabilitas dengan menggunakannya.
Anda selalu memiliki opsi untuk membuat "inline" atau makro efek nil-efek sederhana yang memungkinkan Anda untuk mengompilasi pada platform lain dengan kompiler lain.
Anda tidak akan mendapatkan manfaat dari optimasi jika Anda menggunakan platform lain.
sumber
Sesuai komentar oleh Cody , ini tidak ada hubungannya dengan Linux, tetapi merupakan petunjuk bagi kompiler. Apa yang terjadi tergantung pada versi arsitektur dan kompiler.
Fitur khusus ini di Linux agak salah digunakan dalam driver. Sebagai osgx poin di semantik atribut panas , setiap
hot
ataucold
fungsi yang disebut dengan di blok otomatis dapat mengisyaratkan bahwa kondisi ini kemungkinan atau tidak. Misalnya,dump_stack()
ditandaicold
jadi ini berlebihan,Versi masa depan
gcc
mungkin selektif inline fungsi berdasarkan petunjuk ini. Ada juga saran bahwa itu bukanboolean
, tetapi skor seperti dalam kemungkinan besar , dll. Secara umum, itu harus dipilih untuk menggunakan beberapa mekanisme alternatif seperticold
. Tidak ada alasan untuk menggunakannya di sembarang tempat kecuali jalur panas. Apa yang dilakukan oleh kompiler pada satu arsitektur dapat sangat berbeda pada arsitektur lainnya.sumber
Dalam banyak rilis linux, Anda dapat menemukan complier.h di / usr / linux /, Anda dapat memasukkannya untuk digunakan secara sederhana. Dan pendapat lain, tidak mungkin () lebih bermanfaat daripada kemungkinan (), karena
itu dapat dioptimalkan juga di banyak kompiler.
Ngomong-ngomong, jika Anda ingin mengamati perilaku detail kode, Anda dapat melakukannya dengan mengikuti:
Kemudian, buka obj.s, Anda dapat menemukan jawabannya.
sumber
Mereka adalah petunjuk ke kompiler untuk menghasilkan awalan petunjuk di cabang. Pada x86 / x64, mereka membutuhkan satu byte, jadi Anda akan mendapatkan paling banyak peningkatan satu byte untuk setiap cabang. Adapun kinerja, itu sepenuhnya tergantung pada aplikasi - dalam kebanyakan kasus, prediktor cabang pada prosesor akan mengabaikan mereka, hari ini.
Sunting: Lupa tentang satu tempat yang sebenarnya bisa mereka bantu. Itu dapat memungkinkan kompiler untuk menyusun ulang grafik aliran kontrol untuk mengurangi jumlah cabang yang diambil untuk jalur 'kemungkinan'. Ini dapat memiliki peningkatan yang ditandai dalam loop di mana Anda memeriksa beberapa kasus keluar.
sumber
Ini adalah fungsi GCC untuk programmer untuk memberikan petunjuk kepada kompiler tentang apa kondisi cabang yang paling mungkin dalam ekspresi yang diberikan. Ini memungkinkan kompiler untuk membangun instruksi cabang sehingga kasus yang paling umum mengambil jumlah instruksi yang paling sedikit untuk dieksekusi.
Cara instruksi cabang dibangun bergantung pada arsitektur prosesor.
sumber