Perintah dan konteks
Implementasikan y[i] = a * x[i] + b pada sebuah array di mana N mungkin bukan kelipatan dari lebar vektor dan input mungkin mengandung NaN. Gunakan tipe paralel-data C++26 dan jelaskan tail, alignment, mask, dukungan kompilator, fallback skalar, dan cara Anda membuktikan bahwa optimasi tersebut membantu.
Hal yang diuji pewawancara
- Apakah Anda mengetahui bahwa
std::simdadalah abstraksi paralel-data portabel, bukan jaminan satu instruksi tetap. - Apakah Anda membedakan nilai vektor, mask, ukuran tetap, dan lebar native, serta menangani batch akhir parsial.
- Apakah Anda menghindari asumsi akses yang tidak sejajar (misaligned), pelanggaran aliasing, akses di luar batas (out-of-bounds), dan penanganan NaN yang salah.
- Apakah Anda menyiapkan pengujian fitur (feature testing) dan jalur fallback saat dukungan C++26 dan toolchain masih belum merata.
- Apakah Anda menggunakan benchmark, pengujian kebenaran, dan hardware counter alih-alih mengklaim kecepatan hanya dari kode sumber.
Pertanyaan klarifikasi
- Apakah kompilator target dan pustaka standar mengimplementasikan header simd C++26, atau hanya namespace eksperimental?
- Apakah input dan output boleh memiliki alias, dan apa persyaratan NaN, tak hingga (infinity), serta pembulatannya?
- Berapa rentang N, tipe elemen, toleransi kesalahan, dan set instruksi CPU/GPU target?
- Apakah loop ini terikat pada bandwidth memori (memory-bandwidth bound), dan apakah frekuensi pemanggilannya cukup tinggi untuk membenarkan kompleksitas vektorisasi?
- Apakah lebar ABI tetap diperlukan, atau bolehkah implementasi memilih lebar native untuk perangkat keras target?
Kerangka jawaban 30 detik
“Saya akan mengonfirmasi implementasi header simd dan kontrak numerik terlebih dahulu, lalu memuat satu batch dengan std::simd, menerapkan multiply-add, dan menyimpannya. Loop utama menangani vektor penuh; sebuah mask atau loop skalar menangani tail tanpa akses di luar batas. Tag muat (load tag) harus cocok dengan alignment sebenarnya daripada asumsi cast. Saya akan mempertahankan referensi skalar dan memilih C++26, implementasi eksperimental, atau fallback skalar dengan feature test dan matriks build. Terakhir, saya akan menguji perilaku NaN, kesalahan, dan batas pada input yang identik serta membandingkan throughput dan bandwidth dengan benchmark tetap dan hardware counter.”
Pembahasan mendalam langkah demi langkah
1. Memilih abstraksi paralel-data
Tipe paralel-data C++26 menyediakan nilai vektor dan mask untuk mengekspresikan satu operasi di beberapa elemen. Jumlah lane dari std::simd dipilih oleh implementasi dan perangkat keras target; pilih tipe ukuran tetap hanya ketika tata letak atau stabilitas antarmuka memerlukannya. Abstraksi standar memungkinkan kompilator memetakannya ke register SIMD atau implementasi lain yang sesuai.
2. Menulis loop batch penuh
Misalkan V adalah tipe vektor dan memproses batch lengkap dari i hingga i + V::size(). Input dan output tidak boleh tumpang tindih kecuali kontrak fungsi mengizinkan operasi di tempat (in-place), dan pemanggil harus menyediakan rentang yang valid. Jangan berasumsi pointer sembarang sudah sejajar (aligned); tag muat harus sesuai dengan jaminan alignment yang sebenarnya.
template<class V>
void axpb_simd(const float* x, float* y, std::size_t n, float a, float b) {
const V va(a), vb(b);
std::size_t i = 0;
for (; i + V::size() <= n; i += V::size()) {
V vx(&x[i], std::element_aligned_tag{});
(vx * va + vb).copy_to(&y[i], std::element_aligned_tag{});
}
for (; i < n; ++i) y[i] = a * x[i] + b;
}3. Menggunakan mask untuk tail
Loop tail skalar adalah yang paling mudah diaudit. Jika tail sering terjadi, buat mask aktif dan muat serta simpan hanya lane yang valid. Mask harus membatasi operasi baca dan tulis sehingga lane yang tidak aktif tidak menyebabkan akses di luar batas atau efek samping. Jangan mengorbankan kejelasan batas hanya untuk menghilangkan beberapa iterasi skalar.
4. Mendefinisikan semantik NaN, kesalahan, dan exception
Definisikan propagasi NaN, tak hingga, dan persyaratan pembulatan sebelum melakukan vektorisasi. Vektorisasi dapat mengubah urutan operasi, sehingga hasil skalar dan vektor tidak secara otomatis identik secara bitwise; pengujian harus membandingkan toleransi kesalahan yang diizinkan dan perilaku nilai khusus. Jika kontrak bisnis memerlukan hasil IEEE yang ketat atau urutan flag exception, verifikasi opsi floating-point kompilator dan semantik pustaka sebelum memilih SIMD.
5. Menyediakan jalur standar dan fallback
Makro feature-test header simd C++26 adalah __cpp_lib_simd, tetapi dukungan toolchain mungkin tertinggal. Setelah deteksi kapabilitas, build harus memilih tipe standar, antarmuka eksperimental dari suatu implementasi, atau template skalar. Jangan mengekspos tipe vektor privat milik kompilator dalam API publik. Setiap jalur harus menjalankan pengujian kebenaran yang sama.
6. Membuktikan performa dan batasan
Kunci N, distribusi data, opsi kompilator, dan jumlah thread dalam benchmark yang membandingkan skalar, SIMD, dan lebar yang berbeda. Catat throughput, latensi, cache miss, rasio instruksi vektor, dan bandwidth memori; uji N=1.000, input kosong, alamat tidak sejajar, dan data NaN secara terpisah. Jika bandwidth memori atau overhead panggilan adalah bottleneck, SIMD mungkin tidak membantu dan implementasi sederhana harus tetap dipertahankan.
Contoh jawaban yang kuat
“Saya memperlakukan std::simd sebagai abstraksi paralel-data yang portabel dan tidak berasumsi pada jumlah lane atau instruksi yang tetap. Loop utama memproses batch penuh V::size() dan menggunakan element_aligned_tag untuk array yang mungkin tidak sejajar; bagian tail menggunakan loop skalar atau mask yang membatasi pembacaan dan penulisan. Saya mendefinisikan aliasing, propagasi NaN, dan persyaratan kesalahan terlebih dahulu, kemudian memilih C++26, implementasi eksperimental, atau template skalar dengan feature test. Saya menguji N=1.000, input kosong, alamat yang tidak sejajar, dan NaN, serta membandingkan throughput, perilaku cache, dan bandwidth dengan hardware counter. Jika memori membatasi loop, saya tidak memaksakan vektorisasi.”
Kesalahan umum
- Memperlakukan
std::simdsebagai register lebar-tetap → kode bergantung pada satu CPU → gunakan lebar yang dipilih implementasi atau lebar yang ditetapkan secara eksplisit dengan sengaja. - Memuat vektor penuh untuk tail → akses di luar batas → gunakan mask atau loop tail skalar.
- Mengasumsikan alignment → undefined behavior atau pemuatan lebih lambat → sesuaikan tag muat dengan jaminan sebenarnya.
- Hanya membandingkan nilai rata-rata → perbedaan NaN, tak hingga, dan pembulatan terlewat → definisikan kontrak nilai khusus dan kesalahan.
- Menyatakan berhasil hanya setelah melihat instruksi vektor → bandwidth memori mungkin mendominasi → buktikan dengan benchmark tetap dan counter.
Pertanyaan lanjutan dan tanggapan
Bagaimana Anda memilih antara lebar tetap vs native?
Lebar native memungkinkan implementasi memilih lebar register untuk perangkat keras target dan biasanya cocok untuk throughput. Lebar tetap cocok untuk tata letak yang stabil, ABI, atau perilaku lintas platform yang dapat direproduksi. Periksa ABI, tata letak data, dan benchmark sebelum memilih; lebar tetap itu sendiri bukan jaminan performa.
Mengapa tidak selalu menggunakan mask untuk tail?
Mask mempertahankan satu bentuk loop tetapi dapat menambah overhead pembuatan, pemuatan, dan penyimpanan. Tail yang pendek lebih mudah diaudit dengan kode skalar; bandingkan tail bermasker dan skalar yang diukur ketika rasio tail cukup signifikan.
Bagaimana Anda membuktikan bahwa kompilator tidak men-skalarisasi loop?
Periksa assembly yang dihasilkan atau laporan optimasi dan sandingkan dengan hardware counter untuk instruksi vektor, throughput, dan perilaku cache. Pemeriksaan kode sumber atau satu sampel waktu jam dinding (wall-clock) tidak cukup; pertahankan kompilator, flag target, dan ukuran data tetap konstan.
Bagaimana jika header simd tidak tersedia di kompilator produksi?
Gunakan feature test dan matriks build untuk memilih implementasi yang didukung sambil mempertahankan kebenaran skalar. Jangan menyalin tipe vektor privat ke antarmuka publik. Aktifkan jalur standar setelah pembaruan toolchain dan gunakan kembali pengujian batas dan numerik yang sama.