Ada banyak tebakan yang salah (sedikit atau seluruhnya) dalam komentar tentang beberapa detail / latar belakang untuk ini.
Anda sedang melihat implementasi C fallback dioptimalkan glibc yang dioptimalkan. (Untuk SPA yang tidak memiliki implementasi asm yang ditulis tangan) . Atau versi lama dari kode itu, yang masih di pohon sumber glibc. https://code.woboq.org/userspace/glibc/string/strlen.c.html adalah kode-peramban berdasarkan pohon glibc git saat ini. Tampaknya masih digunakan oleh beberapa target glibc utama, termasuk MIPS. (Terima kasih @zwol).
Pada ISA populer seperti x86 dan ARM, glibc menggunakan asm yang ditulis tangan
Jadi insentif untuk mengubah apa pun tentang kode ini lebih rendah dari yang Anda kira.
Kode bithack ini ( https://graphics.stanford.edu/~seander/bithacks.html#ZeroInWord ) bukan yang sebenarnya berjalan di server / desktop / laptop / smartphone Anda. Ini lebih baik daripada loop byte-at-a-time yang naif, tetapi bahkan bithack ini cukup buruk dibandingkan dengan asm efisien untuk CPU modern (terutama x86 di mana AVX2 SIMD memungkinkan memeriksa 32 byte dengan beberapa instruksi, memungkinkan 32 hingga 64 byte per jam siklus di loop utama jika data panas di cache L1d pada CPU modern dengan 2 / jam beban vektor dan throughput ALU. yaitu untuk string berukuran sedang di mana overhead startup tidak mendominasi.)
glibc menggunakan trik tautan dinamis untuk menyelesaikan strlen
ke versi optimal untuk CPU Anda, sehingga bahkan dalam x86 ada versi SSE2 (vektor 16-byte, garis dasar untuk x86-64) dan versi AVX2 (vektor 32-byte).
x86 memiliki transfer data yang efisien antara register vektor dan keperluan umum, yang membuatnya unik (?) baik untuk menggunakan SIMD untuk mempercepat fungsi pada string panjang implisit di mana kontrol loop bergantung pada data. pcmpeqb
/ pmovmskb
memungkinkan untuk menguji 16 byte terpisah sekaligus.
glibc memiliki versi AArch64 seperti itu yang menggunakan AdvSIMD , dan versi untuk CPU AArch64 di mana register vektor-> GP menghentikan jalur pipa, sehingga ia benar - benar menggunakan bithack ini . Tetapi menggunakan count-leading-zero untuk menemukan byte-dalam-register begitu mendapat hit, dan mengambil keuntungan dari akses yang tidak selaras efisien AArch64 setelah memeriksa untuk lintas halaman.
Juga terkait: Mengapa kode ini 6.5x lebih lambat dengan optimisasi diaktifkan? memiliki beberapa perincian lebih lanjut tentang apa yang cepat vs. lambat dalam as86 x86 strlen
dengan dengan buffer besar dan implementasi asm sederhana yang mungkin baik bagi gcc untuk mengetahui cara melakukan inline. (Beberapa versi gcc secara tidak bijaksana sebaris rep scasb
yang sangat lambat, atau bithack 4-byte-at-a-time seperti ini. Jadi resep inline-strlen GCC perlu diperbarui atau dinonaktifkan.)
ASM tidak memiliki "perilaku tidak terdefinisi" gaya C ; aman untuk mengakses byte di memori sesuka Anda, dan pemuatan selaras yang menyertakan byte yang valid tidak dapat kesalahan. Perlindungan memori terjadi dengan rincian halaman selaras; akses yang selaras lebih sempit dari itu tidak dapat melintasi batas halaman. Apakah aman membaca melewati akhir buffer dalam halaman yang sama di x86 dan x64? Alasan yang sama berlaku untuk kode mesin yang membuat peretasan C ini dibuat untuk membuat implementasi mandiri dari fungsi ini.
Ketika kompiler memancarkan kode untuk memanggil fungsi non-inline yang tidak diketahui, ia harus mengasumsikan bahwa fungsi memodifikasi setiap / semua variabel global dan memori apa pun yang mungkin memiliki pointer. yaitu segala sesuatu kecuali penduduk setempat yang tidak memiliki alamat pelarian mereka harus disinkronkan dalam memori di seluruh panggilan. Ini berlaku untuk fungsi yang ditulis dalam asm, jelas, tetapi juga untuk fungsi perpustakaan. Jika Anda tidak mengaktifkan optimasi waktu-tautan, itu bahkan berlaku untuk unit terjemahan yang terpisah (file sumber).
Mengapa ini aman sebagai bagian dari glibc tetapi tidak sebaliknya.
Faktor yang paling penting adalah bahwa ini strlen
tidak bisa sejalan dengan hal lain. Tidak aman untuk itu; itu berisi UB alias ketat (membaca char
data melalui unsigned long*
). char*
diizinkan untuk alias apa pun tetapi kebalikannya tidak benar .
Ini adalah fungsi perpustakaan untuk perpustakaan yang dikompilasi sebelumnya (glibc). Itu tidak akan disejajarkan dengan optimasi tautan waktu ke penelepon. Ini berarti hanya perlu mengkompilasi ke kode mesin yang aman untuk versi yang berdiri sendiri strlen
. Tidak harus portabel / aman C.
Pustaka GNU C hanya perlu dikompilasi dengan GCC. Rupanya itu tidak didukung untuk mengkompilasinya dengan dentang atau ICC, meskipun mereka mendukung ekstensi GNU. GCC adalah kompiler terdepan yang mengubah file sumber C menjadi file objek kode mesin. Bukan penerjemah, jadi kecuali itu inline pada waktu kompilasi, byte dalam memori hanyalah byte dalam memori. yaitu UB ketat-aliasing tidak berbahaya ketika akses dengan tipe yang berbeda terjadi dalam fungsi yang berbeda yang tidak sejalan satu sama lain.
Ingatlah bahwa strlen
perilaku didefinisikan oleh standar ISO C. Nama fungsi itu secara khusus adalah bagian dari implementasi. Kompiler seperti GCC bahkan memperlakukan nama sebagai fungsi bawaan kecuali jika Anda menggunakannya -fno-builtin-strlen
, sehingga strlen("foo")
bisa berupa konstanta waktu kompilasi 3
. Definisi di perpustakaan hanya digunakan ketika gcc memutuskan untuk benar-benar memancarkan panggilan ke sana alih-alih inlining resepnya sendiri atau sesuatu.
Ketika UB tidak terlihat oleh kompiler pada waktu kompilasi, Anda mendapatkan kode mesin waras. Kode mesin harus bekerja untuk case no-UB, dan bahkan jika Anda mau , tidak ada cara bagi asm untuk mendeteksi tipe apa yang digunakan oleh penelepon untuk memasukkan data ke dalam memori menunjuk-ke.
Glibc dikompilasi ke perpustakaan statis atau dinamis yang berdiri sendiri yang tidak dapat sejalan dengan optimasi waktu tautan. skrip build glibc tidak membuat pustaka statis "gemuk" yang berisi kode mesin + gcc Representasi internal GIMPLE untuk optimasi tautan-waktu ketika masuk ke dalam sebuah program. (Yaitu libc.a
tidak akan berpartisipasi dalam -flto
optimasi tautan-waktu ke dalam program utama.) Membangun glibc dengan cara itu akan berpotensi tidak aman pada target yang benar-benar menggunakan ini.c
.
Bahkan seperti komentar @zwol, KPP tidak dapat digunakan ketika membangun glibc itu sendiri , karena kode "rapuh" seperti ini yang bisa pecah jika inlining antara file sumber glibc adalah mungkin. (Ada beberapa penggunaan internal strlen
, misalnya mungkin sebagai bagian dari printf
implementasi)
Ini strlen
membuat beberapa asumsi:
CHAR_BIT
adalah kelipatan dari 8 . Benar pada semua sistem GNU. POSIX 2001 bahkan menjamin CHAR_BIT == 8
. (Ini terlihat aman untuk sistem dengan CHAR_BIT= 16
atau 32
, seperti beberapa DSP; loop unaligned-prologue akan selalu menjalankan 0 iterasi jika sizeof(long) = sizeof(char) = 1
karena setiap pointer selalu sejajar dan p & sizeof(long)-1
selalu nol.) Tetapi jika Anda memiliki set karakter non-ASCII di mana karakter adalah 9 atau lebar 12 bit, 0x8080...
adalah pola yang salah.
- (mungkin)
unsigned long
adalah 4 atau 8 byte. Atau mungkin itu benar-benar berfungsi untuk ukuran unsigned long
hingga 8, dan itu menggunakan assert()
untuk memeriksa itu.
Keduanya tidak mungkin UB, mereka hanya non-portabilitas untuk beberapa implementasi C. Kode ini (atau dulu) adalah bagian dari implementasi C pada platform di mana ia bekerja, jadi tidak masalah.
Asumsi selanjutnya adalah potensi C UB:
- Muat selaras yang berisi byte yang valid tidak dapat kesalahan , dan aman selama Anda mengabaikan byte di luar objek yang Anda inginkan. (Benar dalam asm pada setiap sistem GNU, dan pada semua CPU normal karena perlindungan memori terjadi dengan perataan halaman selaras. Apakah aman untuk membaca melewati akhir buffer dalam halaman yang sama pada x86 dan x64? Aman di C saat UB tidak dapat dilihat pada waktu kompilasi. Tanpa inline, inilah kasusnya di sini. Kompiler tidak dapat membuktikan bahwa membaca sebelumnya
0
adalah UB; bisa berupa char[]
array C yang berisi {1,2,0,3}
misalnya)
Poin terakhir itulah yang membuatnya aman untuk membaca melewati akhir objek C di sini. Itu cukup aman bahkan ketika menyejajarkan dengan kompiler saat ini karena saya pikir mereka saat ini tidak memperlakukan bahwa menyiratkan jalur eksekusi tidak dapat dijangkau. Tapi bagaimanapun, aliasing yang ketat sudah menjadi showstopper jika Anda membiarkan ini sejalan.
Maka Anda akan memiliki masalah seperti memcpy
makro CPP kernel tua Linux yang tidak aman yang menggunakan pointer-casting ke unsigned long
( gcc, aliasing ketat, dan cerita horor ).
strlen
Tanggal ini kembali ke era ketika Anda bisa pergi dengan hal-hal seperti itu secara umum ; dulu cukup aman tanpa peringatan "hanya ketika tidak inlining" sebelum GCC3.
UB yang hanya terlihat ketika melihat lintas batas panggilan / ret tidak dapat menyakiti kita. (mis. memanggil ini pada char buf[]
bukannya array array unsigned long[]
ke a const char*
). Setelah kode mesin diatur dalam batu, itu hanya berurusan dengan byte dalam memori. Panggilan fungsi non-inline harus mengasumsikan bahwa callee membaca semua memori.
Menulis ini dengan aman, tanpa UB alias ketat
The jenis GCC atributmay_alias
memberikan jenis perawatan alias-apa sama char*
. (Disarankan oleh @KonradBorowsk). Header GCC saat ini menggunakannya untuk tipe vektor x86 SIMD seperti __m128i
sehingga Anda selalu dapat melakukannya dengan aman _mm_loadu_si128( (__m128i*)foo )
. (Lihat Apakah `reinterpret_cast`ing antara pointer vektor perangkat keras dan tipe yang sesuai merupakan perilaku yang tidak terdefinisi? Untuk perincian lebih lanjut tentang apa artinya ini dan yang tidak berarti.)
strlen(const char *char_ptr)
{
typedef unsigned long __attribute__((may_alias)) aliasing_ulong;
aliasing_ulong *longword_ptr = (aliasing_ulong *)char_ptr;
for (;;) {
unsigned long ulong = *longword_ptr++; // can safely alias anything
...
}
}
Anda juga bisa menggunakan aligned(1)
untuk mengekspresikan suatu tipe alignof(T) = 1
.
typedef unsigned long __attribute__((may_alias, aligned(1))) unaligned_aliasing_ulong;
Cara portabel untuk mengekspresikan muatan aliasing dalam ISO adalah denganmemcpy
, yang oleh kompiler modern benar-benar tahu bagaimana cara inline sebagai instruksi muatan tunggal. misalnya
unsigned long longword;
memcpy(&longword, char_ptr, sizeof(longword));
char_ptr += sizeof(longword);
Ini juga berfungsi untuk beban yang tidak selaras karena memcpy
berfungsi seolah-olah dengan char
akses pada waktu tertentu. Tetapi dalam praktiknya kompiler modern memcpy
sangat mengerti .
Bahayanya di sini adalah bahwa jika GCC tidak tahu pasti apakah char_ptr
itu selaras kata, itu tidak akan menyertainya pada beberapa platform yang mungkin tidak mendukung beban yang tidak selaras dalam asm. mis. MIPS sebelum MIPS64r6, atau ARM yang lebih lama. Jika Anda mendapat panggilan fungsi sebenarnya untuk memcpy
hanya memuat kata (dan meninggalkannya di memori lain), itu akan menjadi bencana. GCC terkadang dapat melihat kapan kode menyelaraskan sebuah pointer. Atau setelah loop char-at-a-time yang mencapai batas ulong yang bisa Anda gunakan
p = __builtin_assume_aligned(p, sizeof(unsigned long));
Ini tidak menghindari UB baca-lampau-objek yang mungkin, tetapi dengan GCC saat ini yang tidak berbahaya dalam praktiknya.
Mengapa sumber C yang dioptimalkan dengan tangan diperlukan: kompiler saat ini tidak cukup baik
ASM yang dioptimalkan dengan tangan bisa lebih baik lagi jika Anda menginginkan setiap tetes kinerja terakhir untuk fungsi pustaka standar yang banyak digunakan. Khusus untuk sesuatu seperti memcpy
, tetapi juga strlen
. Dalam hal ini tidak akan lebih mudah untuk menggunakan C dengan intrinsik x86 untuk memanfaatkan SSE2.
Tapi di sini kita hanya berbicara tentang versi C naif vs bithack tanpa fitur khusus ISA.
(Saya pikir kita bisa menganggapnya sebagai suatu pemberian yang strlen
cukup banyak digunakan sehingga membuatnya berjalan secepat mungkin adalah penting. Jadi pertanyaannya adalah apakah kita bisa mendapatkan kode mesin yang efisien dari sumber yang lebih sederhana. Tidak, kita tidak bisa.)
GCC dan dentang saat ini tidak mampu loop auto-vektorisasi di mana jumlah iterasi tidak diketahui sebelum iterasi pertama . (misalnya itu harus mungkin untuk memeriksa apakah loop akan menjalankan setidaknya 16 iterasi sebelum menjalankan iterasi pertama.) misalnya memcpy autovectorizing mungkin (buffer panjang-eksplisit) tetapi tidak strcpy atau strlen (string panjang-implisit), diberikan saat ini kompiler.
Itu termasuk loop pencarian, atau loop lain dengan data-dependent if()break
serta counter.
ICC (kompiler Intel untuk x86) dapat secara otomatis membuat vektor beberapa loop pencarian, tetapi masih hanya membuat ASM byte-at-a-time yang naif untuk C sederhana / naif strlen
seperti penggunaan libc OpenBSD. ( Godbolt ). (Dari jawaban @ Peske ).
Libc yang dioptimalkan dengan tangan strlen
diperlukan untuk kinerja dengan kompiler saat ini . Melangkah 1 byte pada satu waktu (dengan membuka gulungan mungkin 2 byte per siklus pada CPU superscalar lebar) menyedihkan ketika memori utama dapat mengimbangi sekitar 8 byte per siklus, dan cache L1d dapat mengirimkan 16 hingga 64 per siklus. (Beban 2x 32-byte per siklus pada CPU mainstream x86 modern sejak Haswell dan Ryzen. Tidak termasuk AVX512 yang dapat mengurangi kecepatan clock hanya untuk menggunakan vektor 512-bit; itulah sebabnya glibc mungkin tidak terburu-buru untuk menambahkan versi AVX512 Meskipun dengan vektor 256-bit, AVX512VL + BW bertopeng dibandingkan menjadi topeng dan ktest
atau kortest
bisa membuat strlen
lebih ramah hyperthreading dengan mengurangi uops / iterasinya.)
Saya termasuk non-x86 di sini, itulah "16 byte". misalnya kebanyakan CPU AArch64 dapat melakukan setidaknya itu, saya pikir, dan beberapa pasti lebih. Dan beberapa memiliki throughput eksekusi yang cukup untuk strlen
mengimbangi beban bandwidth tersebut.
Tentu saja program yang bekerja dengan string besar biasanya harus melacak panjang untuk menghindari keharusan mengulang menemukan panjang string C panjang implisit sangat sering. Tetapi kinerja pendek hingga menengah masih mendapat manfaat dari implementasi tulisan tangan, dan saya yakin beberapa program akhirnya menggunakan strlen pada string menengah.