- API seperti
strstrdanstd::string::findmengasumsikan pencarian substring sekali jalan, tetapi pada CPU modern biaya perbandingan string pendek dan vektor rendah, sehingga pendekatan SIMD bisa menguntungkan - Ide intinya adalah mengganti kondisi hash lemah Karp-Rabin dengan predikat vektor, lalu melakukan perbandingan substring yang presisi hanya pada posisi kandidat
- Algoritme SIMD generik mengurangi kandidat dengan membandingkan karakter pertama dan karakter terakhir needle secara paralel, lalu memverifikasi hanya posisi yang mungkin cocok dengan
memcmp - Pendekatan SSE4.1
MPSADBWdan SSE4.2PCMPESTRMjuga dibandingkan, tetapi hasil pengukuran menunjukkan SIMD generik lebih stabil dan cepat, sementaraPCMPESTRMcenderung lebih lambat bahkan dibandingMPSADBW - SIMD generik lebih cepat daripada C
strstrdi semua platform, tetapi karena dapat membaca di luar string input, ia tidak aman, dan kondisi perbandingannya juga tidak sepenuhnya sama karena menerima informasi panjang lebih dulu
Asumsi biaya yang berubah dalam pencarian string
- API seperti
strstrdi C,std::string::finddi C++, sertaposdanindexpada string Python dirancang untuk pencarian sekali jalan atas substring dalam string yang diberikan - Algoritme lama secara garis besar terbagi menjadi dua kelompok
- Pendekatan berbasis deterministic finite automata seperti Knuth-Morris-Pratt dan Boyer Moore
- Pendekatan berbasis perbandingan sederhana seperti Karp-Rabin
- Algoritme standar mengasumsikan bahwa membandingkan sepasang karakter, melihat tabel bantu, dan percabangan bersyarat itu murah, sedangkan membandingkan dua substring itu mahal
- Pada CPU desktop modern, asumsi ini mungkin tidak lagi sesuai
- Pada CPU 64-bit, tidak ada perbedaan biaya untuk perbandingan 1, 2, 4, dan 8 byte
- Jika mendukung instruksi SIMD, perbandingan vektor 16, 32, atau 64 byte bisa sama murahnya dengan perbandingan satu byte
- Lookup tabel memerlukan biaya akses memori setingkat perjalanan pulang-pergi cache L1, dan pembacaan per karakter juga memiliki biaya serupa
- Branch yang salah diprediksi dapat menimbulkan penalti sekitar 10–20 siklus
- Rantai dependensi pendek dari membaca karakter, membandingkan, hingga percabangan bersyarat menyulitkan pemanfaatan eksekusi out-of-order CPU
Pendekatan: menggunakan predikat vektor sebagai pengganti hash lemah
- Karp-Rabin melakukan perbandingan presisi hanya ketika hash lemah substring target pencarian sama dengan hash segmen string saat ini
- Pendekatan SIMD mengganti kondisi hash ini dengan predikat vektor
- Predikat dihitung secara paralel
- Perbandingan substring yang presisi dilakukan hanya untuk setiap posisi yang bernilai benar dalam vektor predikat
- Spesialisasi berdasarkan panjang juga digunakan untuk meningkatkan performa
- Implementasi umum memanggil fungsi seperti
memcmpuntuk perbandingan substring - Jika panjang substring yang dicari diketahui, pemanggilan fungsi dapat diganti dengan beberapa instruksi CPU yang disesuaikan dengan panjang tertentu, atau dalam beberapa kasus satu instruksi saja
- Cara ini mengurangi biaya pemanggilan fungsi dan biaya internal
memcmp
- Implementasi umum memanggil fungsi seperti
Algoritme 1: SIMD generik
- Algoritme SIMD generik dapat diterapkan pada berbagai set instruksi SIMD dan pendekatan SWAR
- Predikat dasarnya adalah memeriksa apakah karakter pertama dan karakter terakhir needle sama-sama cocok
- Isi register
Fdengan karakter pertama - Isi register
Ldengan karakter terakhir - Pada setiap iterasi, baca chunk
Adari offset haystack saat inii, dan baca chunkBdarii + k - 1 - Hitung
F == AdanB == L, lalu gabungkan kedua hasilnya untuk membuat mask posisi kandidat - Perbandingan substring yang presisi dilakukan hanya pada posisi yang benar di mask
- Isi register
- Dalam contoh mencari
"cat"pada"a_cat_tries", satu-satunya posisi tempat karakter pertamacdan karakter terakhirtsama-sama cocok adalah indeks 2, sehingga perbandingan presisi juga hanya dilakukan sekali - Memilih karakter pertama dan terakhir tidak selalu baik
- Jika string input sebagian besar berisi
'A'dan needle adalah"AjohndoeA", banyak kandidat dapat muncul - Implementasi dapat memilih karakter terjauh yang berbeda dari karakter pertama sebagai pengganti karakter terakhir
- Jika semua karakter dalam needle sama, prosedur khusus untuk pola seperti
"AAAAA"dapat digunakan
- Jika string input sebagian besar berisi
Perbedaan antarimplementasi
- Implementasi SSE dan AVX2 memiliki struktur yang hampir sama dan menggunakan jumlah instruksi minimum
- Karena sudah diketahui bahwa karakter pertama dan terakhir cocok,
memcmptidak perlu membandingkan lagi karakter-karakter ini
- Karena sudah diketahui bahwa karakter pertama dan terakhir cocok,
- Pendekatan SWAR memanfaatkan fakta bahwa byte sama jika hasil XOR adalah 0
- Alih-alih melakukan AND pada hasil parsial seperti SSE/AVX2, pendekatan ini menggunakan OR
- Proses menemukan posisi byte 0 memerlukan lebih banyak pekerjaan
- Implementasi C++ untuk vektor 64-bit menghitung mask byte yang presisi
- AVX512F tidak memiliki operasi byte dan item vektor terkecilnya adalah word 32-bit, sehingga diperlukan teknik SWAR
- Dua XOR dan satu OR dihitung dengan instruksi AVX512F
- Elemen 32-bit yang mengandung byte 0 dicari
- Untuk setiap elemen 32-bit tersebut, 4 kandidat substring diperiksa
- Implementasi ARM Neon 32-bit menggunakan register SIMD 128-bit
- Perjalanan pulang-pergi yang panjang dari unit Neon kembali ke CPU menjadi bottleneck
- Hasil perbandingan disimpan ke memori sebagai word 64-bit untuk diproses
- Meng-unroll 2 loop internal membuatnya sekitar 1,2 kali lebih cepat
- Implementasi AArch64 hampir sama dengan prosedur ARM Neon, tetapi operasi membaca lane register SIMD secara langsung lebih cepat
Algoritme 2: SSE4.1 MPSADBW
MPSADBWpada SSE4.1 dan AVX2 menghitung Manhattan distance antara subvektor 4 byte dari satu register dan 8 subvektor 4 byte berurutan dari register lain- Jika dua subvektor sama, jarak L1 menjadi 0, sehingga ini digunakan untuk mencari posisi kandidat
- Pendekatan ini menggunakan kesamaan 4 karakter pertama sebagai predikat
- Karena membandingkan 4 karakter pertama, tampaknya lebih kuat daripada perbandingan karakter pertama dan terakhir, tetapi dalam kasus terburuk tidak dapat menghindari kompleksitas kuadratik
- Jika haystack penuh dengan
"a"dan needle adalah"aaaabcde", predikat akan benar untuk setiap karakter input
- Jika haystack penuh dengan
- Pendekatan
MPSADBWmemiliki beberapa batasan- Pada dasarnya tidak cocok untuk substring dengan panjang kurang dari 4
- Penanganan panjang 3 dimungkinkan, tetapi membutuhkan kode tambahan
- SSE
MPSADBWhanya memproses 8 byte dalam satu waktu - AVX2
MPSADBWbekerja per lane 128-bit, bukan pada keseluruhan 256-bit, sehingga memerlukan kode loading tambahan - Latency instruksinya tinggi, 5–7 siklus tergantung CPU, tetapi throughput-nya 1–2 siklus sehingga latency dapat disembunyikan dengan loop unrolling
- AVX512F tidak memiliki
MPSADBW, tetapi karena elemen 32-bit didukung secara native, predikat kesamaan prefix 4 byte dapat ditiru- Pada setiap iterasi, dibuat 4 vektor AVX512 yang berisi prefix 4 byte yang mungkin
- Penyusunan setiap vektor memerlukan 2 shift dan 1 OR
- Loop perbandingan menjadi rumit, dan untuk mengisi elemen terakhir diperlukan 4 byte di luar vektor
Algoritme 3: SSE4.2 PCMPESTRM
- SSE4.2 memperkenalkan set instruksi STNI yang ditujukan sebagai building block untuk operasi string
- Intel pada dasarnya menghentikan STNI pada prosesor-prosesor berikutnya dan tidak memperkenalkan versi AVX2, sementara instruksi-instruksi ini sangat lambat
- Latency 11 siklus dinilai sulit diterima
- Instruksi
PCMPxSTRxmemiliki empat varian bergantung pada cara menentukan panjang string dan cara menyimpan hasil- Panjang dapat diberikan secara eksplisit, atau byte 0 pertama dapat dianggap sebagai akhir seperti string C tradisional
- Hasil dapat disimpan sebagai mask bit/byte atau sebagai nomor bit pertama/terakhir yang disetel dalam mask
- Di sini digunakan mode perbandingan
range ordered- Meski namanya demikian, mode ini mencari substring atau prefix-nya jika melampaui lebar register
- Dalam contoh mencari
"ABCD"pada"ABCD_ABC_ABCD_AB", suffix"AB"juga dapat diperlakukan sebagai kecocokan - Karena itu, yang dapat diasumsikan dengan aman hanyalah kecocokan karakter pertama, sedangkan sisanya harus diperiksa dengan
memcmp
Lingkungan pengukuran performa
- Performa beberapa implementasi SIMD diukur, termasuk spesialisasi runtime untuk substring pendek
- C
strstrdisertakan sebagai pembanding, tetapi karena bug performastd::string::findpada GNU libc, ia dikecualikan dari tabel x64 - Pengujian menjalankan setiap program tiga kali
- Lingkungan pengujian x64
- Westmere i5 M540, GCC 6.2.0
- Bulldozer FX-8150, GCC 4.8.4
- Haswell i7 4470, GCC 5.4.1
- Skylake i7 6700, GCC 5.4.1
- Knights Landing 7210, GCC 5.3.0
- Lingkungan pengujian ARM
- ARMv7 Raspberry Pi 3, kode 32-bit, GCC 4.9.2
- ARMv8 ARM Cortex A57 / AMD Opteron A1100, kode 64-bit, Clang 3.8.0
Hasil x64
- Implementasi SIMD umum menunjukkan peningkatan kecepatan terbesar dibanding
strstrpada x64- Di Westmere, SSE2 generic mencatat 0,74589 detik, sedangkan
strstr0,82246 detik - Di Haswell, AVX2 generic mencatat 0,38653 detik, sedangkan
strstr0,52786 detik - Di Skylake, AVX2 generic mencatat 0,36309 detik, sedangkan
strstr0,66148 detik - Di KNL, AVX512F generic mencatat 1,14057 detik, sedangkan
strstr4,94606 detik
- Di Westmere, SSE2 generic mencatat 0,74589 detik, sedangkan
- Peningkatan kecepatan maksimum adalah 1,10 kali di Westmere, 1,37 kali di Haswell, 1,82 kali di Skylake, dan 4,33 kali di KNL
- Performa
strstrpada Bulldozer adalah 9,37792 detik, sangat buruk sehingga sulit digunakan sebagai patokan - Pendekatan
MPSADBWsecara umum kurang baik kecuali di Skylake, dan sangat buruk di KNL - Pendekatan
PCMPESTRMmenunjukkan hasil yang lebih lambat daripadaMPSADBW
Hasil ARM
- Di ARMv7,
std::strstrmencatat 7,30405 detik, sedangkanstd::string::find4,17131 detik - Di ARMv7, ARM Neon 32-bit generic mencatat 1,29861 detik, hingga 3,1 kali lebih cepat dibanding
std::string::find - Di ARMv8,
std::strstrmencatat 3,37546 detik, sedangkanstd::string::find1,81368 detik - Di ARMv8, AArch64 64-bit generic mencatat 0,27897 detik, hingga 6,5 kali lebih cepat dibanding
std::string::find - Di ARMv8, SWAR 64-bit generic mencatat 0,46269 detik, dengan performa yang mendekati prosedur SIMD 32-bit
Kesimpulan dan batasan
- Algoritme SIMD umum lebih cepat daripada C
strstrdi semua platform - Implementasi sebaiknya memilih versi SIMD tertinggi yang tersedia pada CPU tertentu
MPSADBWtidak berkinerja baik kecuali pengecualian di Skylake, dan sangat buruk di Knights LandingPCMPESTRMberkinerja lebih rendah daripadaMPSADBW- Performa ARM Neon baik, termasuk implementasi SWAR
- Versi SWAR 1,7 kali lebih cepat daripada
string::find - Versi SIMD 3,1 kali lebih cepat daripada
string::find
- Versi SWAR 1,7 kali lebih cepat daripada
- Perbandingan dengan
strstrmungkin tidak sepenuhnya adilstrstrmemproses string tanpa mengetahui panjangnya- Implementasi ini menerima panjang sebagai argumen dan memanfaatkannya
- Implementasinya tidak aman
- Dapat membaca data di luar string input
- Jika string berada tepat sebelum memori yang tidak dipetakan, pelanggaran akses dapat terjadi
- Address sanitizer juga dapat melaporkan masalah
- Membuatnya aman itu mungkin, tetapi bukan tujuan pekerjaan ini
- Semua implementasi dan program pengujian tersedia di GitHub
Belum ada komentar.