- Ini adalah eksperimen untuk mengekstrak performa SIMD bahkan pada string kecil dengan memproses penyalinan string sambil mengubah huruf besar ASCII menjadi huruf kecil 64 byte sekaligus menggunakan AVX-512-BW
- Inti implementasinya adalah membandingkan setiap byte apakah berada di antara
'A'dan'Z', lalu melakukan operasi mask yang menambahkan'a' - 'A'hanya pada posisi tersebut - Sisa ekor untuk string pendek dan bagian akhir string panjang ditangani dengan masked load/store, sehingga mengurangi biaya penanganan potongan kecil yang sering menjadi masalah pada kode SIMD
- Hasil pengukuran penyalinan sekitar 1MiB dalam chunk 1 byte hingga 1KiB pada Clang 16, Debian 11, dan AMD Ryzen 9 7950X menunjukkan bahwa
tolower64secara konsisten termasuk yang lebih cepat di antara pembanding - Pada Zen 4, AVX-512-BW terlihat sangat cocok untuk pemrosesan string, tetapi ARM SVE dan ekstensi RISC-V Vector belum bisa diverifikasi langsung secara mendalam
Membuat tolower() 64 byte dengan AVX-512-BW
- Tujuannya adalah mengimplementasikan kernel
tolower()dengan SIMD yang menyalin string sambil mengubah karakter ASCII huruf besar menjadi huruf kecil - AVX-512-BW adalah ekstensi yang mendukung operasi satuan byte dan word, dan dapat digunakan pada prosesor AMD Zen terbaru
- AVX-512 terbagi ke dalam beberapa ekstensi sehingga dukungan ketersediaannya cukup rumit
- Dukungan di pihak Intel dinilai sangat tidak konsisten
- ARM SVE juga menyediakan masked load/store satuan byte yang cocok untuk pemrosesan string
- Tersedia pada core big-ARM Neoverse terbaru, misalnya Amazon Graviton
- Tidak tersedia di Apple Silicon
- Ekstensi RISC-V Vector juga memiliki gaya yang mirip dengan ARM SVE, dan tersedia pada berbagai komputer papan tunggal kecil
Cara kerja tolower64()
tolower64()adalah kernel berbasis AVX-512 yang memproses 64 byte sekaligus- Pertama, nilai acuan diisikan ke register vektor yang memuat 64 byte
'A''Z''a' - 'A'
- Vektor karakter input
cdibandingkan dengan'A'dan'Z'untuk membuat masing-masing mask 64-bit- posisi dengan
c >= 'A' - posisi dengan
c <= 'Z'
- posisi dengan
- Kedua mask digabungkan dengan
_kand_mask64()untuk membuat maskis_upperyang hanya menandai posisi huruf besar - Terakhir,
_mm512_mask_add_epi8()diterapkan- Byte dengan
is_upperfalse mempertahankancasli - Byte dengan
is_uppertrue menjadic + ('a' - 'A')
- Byte dengan
Menangani string panjang dan string pendek
- Sebagian besar string panjang diproses dengan vector load/store tak terjajar biasa
_mm512_loadu_epi8()tolower64()_mm512_storeu_epi8()
- Untuk string pendek dan potongan sisa terakhir dari string panjang, digunakan masked unaligned load/store
- Mask dibuat dalam bentuk hanya
lenbit bagian bawah yang menyalauint64_t len_bits = (~0ULL) >> (64 - len)- Dinaikkan ke register mask SIMD dengan
_cvtu64_mask64(len_bits)
_mm512_maskz_loadu_epi8()mengisi register tujuan dengan 0 pada posisi saat mask mati_mm512_mask_storeu_epi8()hanya menyimpan posisi saat mask menyala- Pendekatan ini adalah kunci untuk memproses potongan string kecil dengan cepat
Kondisi benchmark dan objek pembanding
- Benchmark dijalankan pada Clang 16, Debian 11, dan AMD Ryzen 9 7950X
- Yang diukur adalah penyalinan sekitar 1MiB, dengan panjang chunk diubah dari 1 byte hingga 1KiB
- Untuk mencerminkan perbedaan alignment string sumber dan tujuan, beberapa byte disisipkan di antara masing-masing string, dan byte-byte ini tidak dimasukkan ke dalam total pengukuran 1MiB
- Cache L2 Ryzen 9 7950X adalah 1MiB per core, sehingga tiap eksekusi pengujian diperkirakan akan melewati hingga cache L3
- Setiap fungsi dikompilasi secara terpisah untuk menghindari interferensi dari inlining dan perpindahan kode
- Pada kode nyata, kemungkinan besar inlining justru lebih dianjurkan daripada dicegah
Hasil: performa tolower64 yang mulus
tolower64berwarna merah muda secara umum konsisten berada di kelompok tercepat di antara fungsi-fungsi yang diuji- Saat panjangnya 65 byte, performa sedikit turun ketika berpindah ke vektor kedua
- Setelah itu cepat naik dan tidak memiliki lembah performa yang dalam, menunjukkan bahwa masked load/store efektif untuk menangani potongan string pendek
copybytes64berwarna hijau adalah versimemcpyyang menggunakan AVX-512 dengan cara serupa- Tidak jauh lebih cepat daripada
tolower64 - Clang terbaru mengenali makna fungsi ini dan menulis ulang seluruhnya, sehingga dikompilasi dengan Clang 11
- Tidak jauh lebih cepat daripada
copybytes1berwarna oranye adalah versimemcpysatuan byte- Dikompilasi dengan Clang 11
- Menunjukkan bahwa heuristik auto-vectorization Clang 11 relatif kurang baik untuk potongan string di bawah 256 byte
tolowerberwarna merah adalah baseline yang memanggiltolower()standar dari<ctype.h>dan sangat lambattolower1berwarna ungu adalahtolower()satuan byte yang dikompilasi dengan Clang 16- Auto-vectorization Clang 16 jauh lebih baik daripada Clang 11
- Tetap lebih lambat daripada versi tulis tangan dan menghasilkan kode yang jauh lebih rumit
- Penanganan potongan string pendek tidak sebagus
tolower64, sehingga grafik performanya bergetar tajam
tolower8berwarna cokelat adalahtolower()SWAR dari tulisan sebelumnya- Clang mencoba melakukan auto-vectorization, tetapi hasilnya kurang baik karena fungsinya rumit
- Meski dikompilasi dengan Clang 16, tetap muncul jurang performa 256 byte ala Clang 11
memcpyberwarna biru memanggilmemcpydari glibc- Pada awalnya cepat, tetapi ada rentang di mana kecepatannya turun hingga sekitar setengah dari
copybytes64 - Penyebabnya belum dapat dipastikan
- Pada awalnya cepat, tetapi ada rentang di mana kecepatannya turun hingga sekitar setengah dari
Kesimpulan dan kode
- AVX-512-BW sangat cocok untuk menangani string, terutama string pendek
- Di Zen 4, performanya sangat cepat, dan fungsi intrinsic-nya juga relatif mudah digunakan
- Karakteristik yang paling menonjol adalah performa yang mulus
- Hampir tidak terlihat lembah performa saat auto-vectorization beralih ke kode skalar pada potongan string kecil
- Karena tidak ada akses mudah ke perangkat yang mendukung ARM SVE atau ekstensi RISC-V Vector, kedua ekstensi itu belum bisa diteliti secara rinci
- Kodenya dapat dilihat di repositori git situs web
1 komentar
Komentar Hacker News
Trik “unsafe read beyond of death” dianggap sebagai perilaku tak terdefinisi dalam model memori Rust dan LLVM, meskipun diizinkan oleh perangkat keras
Seperti perilaku tak terdefinisi lainnya, compiler dapat berasumsi bahwa “hal seperti itu tidak terjadi” dalam proses optimisasi, sehingga bisa muncul hasil yang tidak terduga; untuk mengakalinya perlu memakai assembly inline
https://github.com/ogxd/gxhash/issues/82
Load yang “membaca nilai di luar rentang alokasi sebagai elemen tak terspesifikasi, dan hanya menjadi perilaku tak terdefinisi ketika perangkat keras tidak menyukainya” tampaknya tidak sulit untuk didukung, dan cukup saja bila secara internal itu menjadi alias dari pemanggilan assembly tersebut
Lebih jauh lagi, akan bagus jika setelah setiap alokasi seperti
malloc, stack, konstanta, dan sebagainya dijamin ada setidaknya sekitar 64 byte alamat tanpa fault, tetapi itu jauh lebih rumit karena membutuhkan kerja sama banyak komponenPada custom allocator itu hal sepele, tetapi dalam kasus tersebut kode SIMD sulit dipakai untuk data di luar heap kustom, dan kita jadi terikat pada kemungkinan segfault yang sangat kecil
sanitizer atau Valgrind juga tetap berguna, karena nilai di luar rentang bisa dilacak sebagai nilai tak pasti dan dapat memunculkan error saat benar-benar digunakan
Saya penasaran apa yang terjadi jika membaca page yang tidak dipetakan atau memori yang diproteksi, dan karena belum melihat kodenya saya tidak tahu apakah jaminan alignment menghindari hal ini
Perilaku tak terdefinisi adalah istilah teknis dalam standar C, jadi menggeneralisasikannya saja sudah aneh; ANSI C tidak secara eksplisit mengizinkan asumsi seperti itu, dan ISO C memang lebih terbuka, tetapi tidak secara spesifik membenarkan asumsi tersebut
Menurut saya penjelasan seperti “UB = boleh diasumsikan tidak mungkin terjadi” cukup mendekati fearmongering yang tidak jujur
Melihat kode dalam tulisan itu yang rapi dan berkinerja bagus, saya jadi penasaran bagaimana implementasi AVX512 AMD akan bersaing dengan AVX10 yang direncanakan Intel
Inti AVX10 tampaknya adalah menyelesaikan situasi P-core/E-core Intel, sedangkan AMD tampaknya memilih pendekatan yang lebih baik: tetap menjaga API mulus sambil memakai implementasi lebar penuh pada Zen5 atau metode pemrosesan 2 kali 256-bit pada Zen4 dan Zen5 mobile, tergantung situasinya
Peningkatan kinerja besar dalam tulisan itu juga semuanya berasal dari core Zen4, dan karena AVX512 punya banyak keunggulan, terasa menjengkelkan bahwa Intel terlalu membatasinya untuk segmentasi pasar hingga secara praktis menghambat adopsinya dalam kode klien umum
Pasar berulang kali menolak percabangan jalur kode per CPU, dan implementasi SIMD yang benar-benar penting adalah common denominator terendah
AVX10.1/256 dan AVX512VL memiliki subset yang sama, jadi setelah waktu yang cukup berlalu dan sebagian besar CPU mendukungnya, orang-orang akan menargetkan bagian itu
AMD akan terus mendapat kemenangan mudah pada beberapa aplikasi benchmark yang diperbarui untuk mendukung AVX512, tetapi jika Intel mempertahankan rencana AVX10, AMD kemungkinan pada akhirnya juga akan memakai pipeline SIMD 2 kali pemrosesan secara penuh agar bisa mendukung AVX10/256 secara efisien sambil mempertahankan kompatibilitas AVX512
Intel membuat banyak pilihan buruk selama 10 tahun terakhir, tetapi memecah pasar lewat instruction set adalah salah satu yang terburuk. Mereka seperti membunuh sendiri momentum dan minat terhadap inovasi terbaru; fitur seperti operasi mask jauh lebih penting daripada lebarnya sendiri, jadi saya berharap mereka memasukkan AVX10/256 ke seluruh lini produk
Istilah itu punya makna tertentu, dan tidak cocok dengan cara kerjanya yang sebenarnya
Zen 4 hanya mendecode operasi register ZMM menjadi beberapa micro-op lalu menjadwalkannya ke unit 256-bit yang kosong, sementara shuffle lebar penuh 512-bit ditangani secara khusus oleh hardware khusus untuk menghindari emulasi yang mahal
Karena itu, Zen 4 dengan 4 unit SIMD 256-bit pun berperilaku seperti core 2×512-bit yang kuat, dan implementasi ini sama sekali bukan cara yang murah; mungkin ini bentuk terbaik yang sejauh ini ada di hardware konsumen
Mereka sudah punya waktu bertahun-tahun untuk memperbaikinya, dan menyebalkan bahwa meski AMD mendukungnya, adopsi tidak terjadi karena pangsa pasar; sayangnya AVX10 sepertinya akan membuat Intel menahan dunia lebih lama lagi
Di desktop, saya ingin melihat core yang lebih baik, lebih banyak core, dan instruction set yang distandardisasi dengan baik yang membuka fitur berguna seperti SIMD lebar, float16, dan gather/scatter; AMD melakukannya dengan cukup baik
Sebaliknya Intel menempelkan core lemah di samping core yang bagus, membatasi core yang bagus agar sesuai dengan core yang lemah, merilis CPU dengan jumlah core yang sama selama beberapa generasi, membuat seolah-olah jumlah core banyak dengan core lemah, mengeluarkan terlalu banyak varian instruksi sehingga sulit terbentuk common set yang berguna, dan bahkan meninggalkan dukungan instruksi yang sebelumnya tampak mereka janjikan
Preferensi produsen desktop dulu adalah Intel pada 90-an, AMD pada awal 2000-an, Intel pada akhir 2000-an dan 2010-an, dan sekarang kembali AMD. Saya penasaran apa yang akan dilakukan Intel untuk mendapatkan kembali pijakan selain menghambat lawannya, dan kompetisi harus terus berjalan agar salah satu pihak tidak terlalu terlena
Materi menarik untuk dilihat sekadar iseng: http://www.unicode.org/Public/3.1-Update1/CaseFolding-4.txt
Misalnya
"straße".upper()menjadi'STRASSE'Selain itu, jika locale tidak ditentukan, saat huruf i tanpa titik dalam bahasa-bahasa Turkik dikonversi bolak-balik ke huruf besar/kecil,
'ı'.upper().lower()menjadi'i'sehingga rusakAda beberapa protokol ASCII yang tidak membedakan huruf besar/kecil, dan ini sering muncul di hot path banyak server
Yang pertama biasanya cukup memakai ASCII murni dalam encoding 8-bit, tetapi yang kedua menjadi rumit
Alamat DNS adalah contoh yang mudah: secara teknis dapat memuat hampir semua Unicode, tetapi untuk resolusi DNS yang sebenarnya dikonversi menjadi subset ASCII yang sangat terbatas, dan proses resolusi itu tidak membedakan huruf besar/kecil
Tentu ada juga bahasa pemrograman yang mendukung semua sistem aksara Unicode tetapi identifier-nya tidak membedakan huruf besar/kecil. Jika Anda menangani hal semacam itu, turut berduka
maßeyang berubah menjadiMASSE, dalam bahasa Jerman juga ada ẞ, yaitu Eszett kapitalIni belum tersebar luas dan font yang mendukungnya masih sedikit, tetapi secara teori sekarang sudah ada
Rasanya penjelasan “penjumlahan mask” di artikel itu salah
Bukankah seharusnya saat
is_upperfalse dilakukan penjumlahan, dan saat true disalin apa adanya?to_upperterbalik, jadi seharusnya disebutto_lowerTerima kasih sudah menunjukkan bagian yang membingungkan; artikel dan kodenya sudah saya perbaiki
tolowerHuruf kapital A adalah
0x40, huruf kecil adalah0x60, jadi menambahkan 0x20 harus terjadi saatis_upperbernilai trueOptimisasi SWAR seperti ini sering kali hanya berguna ketika string terselaraskan pada alamat 8 byte
Jika algoritma SWAR diterapkan pada string yang tidak terselaraskan, sering kali hasilnya lebih lambat daripada algoritma aslinya
Jika dipecah menjadi 3 tahap—memproses bagian awal hingga alamat terselaraskan, memproses badan yang terselaraskan, lalu memproses ekor kurang dari 8 byte—jumlah instruksi malah bertambah
Contoh serupa dan benchmark terkait klaim keliru bahwa
utf8.IsValiddi Go lebih cepat ada di sini: https://github.com/sugawarayuuta/charcoal/pull/1Operasi memori selalu dibuat terselaraskan dan sebesar ukuran vektor penuh, tetapi mask dapat diterapkan hanya pada elemen yang valid
Bahkan jika operasi memori vektor bermasker tidak terselaraskan dan melintasi halaman yang tidak dipetakan atau dilindungi, jika lane tersebut dimatikan oleh mask, fault tidak akan terjadi
Untuk operasi yang panjangnya belum diketahui sebelumnya seperti
strlen(), ada juga instruksi load khusus yang mengurangi panjang vektor tepat sebelum elemen pertama yang akan menimbulkan faultPenjumlahan mask terlihat keren. Akan bagus kalau register mask AVX512 bisa dimanipulasi langsung dari intrinsic .NET, tetapi saat ini harus bergantung pada “idiom yang dikenali”
Jika loop inti buatan penulis yang dihasilkan GCC dianalisis dengan uiCA(CQA/MAQAO) untuk Ice Lake, hasilnya sekitar 32B/cycle, dan jika dikonversi pada 3GHz menjadi hampir 96GiB/s dengan asumsi tidak ada bottleneck memori. Tentu saja, pada algoritma seperti ini akses memori selalu menjadi bottleneck
Namun tampaknya belum benar-benar mendekati pemanfaatan optimal, dan jika memakai Clang, hasil unroll yang lebih terurai serta pemilihan instruksi yang lebih baik bisa mencapai 42,67B/cycle. Cache L2 pun tampaknya sulit mempertahankan throughput seperti itu, tetapi menarik bahwa konversi huruf besar/kecil untuk string berukuran menengah selesai kira-kira dalam waktu cahaya layar mencapai kornea
Beberapa bulan lalu saya mengimplementasikan konversi huruf besar/kecil ASCII dalam UTF-8 yang mirip di C#: https://github.com/U8String/U8String/blob/main/Sources/U8Str...
Karena string pendek mendominasi sebagian besar codebase, konversi yang di-unroll untuk panjang di bawah panjang vektorisasi itu penting, dan
switchdikompilasi menjadi jump table serta fall-through branchlessSaat ini hanya memakai hingga 256-bit, karena pada kasus seperti Zen 3 atau 4 yang hanya memiliki unit SIMD 256×4, itu sudah jenuh. Contoh perbandingan berdampingan dengan versi C ada di sini: https://godbolt.org/z/eTGYhTPan
Di AVX512, sepertinya konversi dengan 3 instruksi menggunakan
vpternlogdjuga mungkin, dan ketika perangkat keras AVX512 bisa dipakai, .NET mengoptimalkannya seperti itu pada lebar 256-bit + AVX512VL, tetapi sekarang anehnya tidak bisa direproduksi pada lebar 512-bitAnda juga akan melihat percobaan SWAR yang gagal pada bagian dispatch
switch; saya penasaran dengan lisensi tulisannya. Kalau lolos test suite, saya ingin memakainyaKalau melihat struktur kedua compiler, ini bisa dipahami, tetapi hasilnya kadang membaik dan kadang merugikan
Beberapa tahun lalu saat mengerjakan proyek yang sangat tervectorisasi dan harus bisa dikompilasi dengan keduanya, akhirnya saya menyimpan inline assembly khusus target tertentu dan file
.Sdi repository, bersama versi referensi CMakefile jadi berantakan dan benchmark pun harus dimasukkan ke test suite, sehingga beban pemeliharaannya besar; karena itu saya menyimpulkan bahwa memakai intrinsic sebagai sarana low-level yang lebih baik daripada auto-vectorization harus dilakukan dengan sangat hati-hati
Contoh: di https://godbolt.org/z/T4Pjhrz5d, output GCC sesuai dugaan, tetapi output Clang mengejutkan dan memang lebih lambat. Jika dijalankan sebagai loop, menurut uiCA hasilnya 7 cycle dibanding 4 cycle pada GCC, dan ini juga terlihat pada benchmark aplikasi nyata tempat fungsi ini dieksekusi miliaran kali dalam algoritma brute-force
Saat melihat codebase LLVM, saya juga ingat melihat masalah bahwa Clang 16 mungkin sama sekali tidak mengeluarkan sebagian instruksi AVX-512 bermasker karena refactoring internal
Tujuannya bukan mengejar performa setinggi mungkin; awalnya hanya ingin melihat apakah bisa berjalan, dan hasil percobaan pertama yang cukup bagus adalah bonus
Perhatian utama saya adalah string yang lebih pendek daripada register vektor dan menghilangkan lembah pada grafik throughput
Jika mengikuti tautan kode di akhir posting blog, ada informasi lisensinya; selain bagian MPL-2.0 yang awalnya ditulis untuk BIND, lisensinya 0BSD atau MIT-0
(x >= 'a' && x <= 'z')menjadi bentuk(x - 'a') < ...sehingga mengurangi satu instruksiKarena encoding opcode yang aneh, kadang register load juga ikut berkurang
Saya tidak tahu apa itu swar
Biasanya berarti teknik memasukkan beberapa item ke dalam satu register secara packed sehingga secara efektif dipakai seperti SIMD, tanpa instruksi SIMD eksplisit
Misalnya, jika memasukkan angka 31-bit dan 32-bit ke register 64-bit lalu menyisakan 1 bit untuk carry, satu penjumlahan 64-bit bisa melakukan dua penjumlahan
Di game, nilai RGB(A) di-packing ke integer 32-bit untuk trik grafis seperti ini, dan ScummVM juga punya kode yang menginterpolasi 2 piksel RGB 16-bit di dalam nilai 32-bit, total 6 komponen: https://github.com/scummvm/scummvm/blob/master/graphics/scal...
Setelah Unicode muncul, konsep huruf besar dan huruf kecil menjadi rawa
Untuk melakukannya dengan benar, dibutuhkan banyak data
Jika Anda mengerjakan sesuatu yang keberhasilannya bergantung pada apakah ASCII
tolowerselesai tepat waktu, lebih baik mengubah permainan dan mengganti prasyaratnyaDulu saya pernah menambahkan bingkai hitam di sekitar gambar untuk sepenuhnya menghindari masalah SIMD membaca melewati buffer
Itu bekerja sangat baik dan dari sisi kecepatan bisa mengalahkan beberapa implementasi OpenCV, tetapi tidak selalu mungkin mengendalikan input sepenuhnya seperti itu
Saya penasaran apakah pernah mencoba cara seperti ini. Hasil auto-vectorization tampak cukup rapi
https://godbolt.org/z/1c5joKK5n
tolower1. Lihat bullet di bawah grafik