Highway adalah perpustakaan C++ yang menyediakan intrinsik SIMD/vektor portabel.
Dokumentasi
Sebelumnya berlisensi di bawah Apache 2, sekarang berlisensi ganda sebagai Apache 2/BSD-3.
Kami tertarik dengan perangkat lunak berkinerja tinggi. Kami melihat potensi besar yang belum dimanfaatkan pada CPU (server, seluler, desktop). Highway diperuntukkan bagi para insinyur yang ingin secara andal dan ekonomis mendorong batas-batas dari apa yang mungkin dilakukan dalam perangkat lunak.
CPU menyediakan instruksi SIMD/vektor yang menerapkan operasi yang sama ke beberapa item data. Hal ini dapat mengurangi penggunaan energi misalnya lima kali lipat karena lebih sedikit instruksi yang dijalankan. Kita juga sering melihat percepatan 5-10x .
Highway menjadikan pemrograman SIMD/vektor praktis dan dapat diterapkan sesuai dengan prinsip panduan berikut:
Melakukan apa yang Anda harapkan : Highway adalah pustaka C++ dengan fungsi yang dipilih dengan cermat yang memetakan dengan baik ke instruksi CPU tanpa transformasi kompiler yang ekstensif. Kode yang dihasilkan lebih dapat diprediksi dan tahan terhadap perubahan kode/pembaruan kompiler daripada vektorisasi otomatis.
Bekerja pada platform yang banyak digunakan : Highway mendukung lima arsitektur; kode aplikasi yang sama dapat menargetkan berbagai set instruksi, termasuk yang memiliki vektor 'scalable' (ukuran tidak diketahui pada waktu kompilasi). Highway hanya membutuhkan C++11 dan mendukung empat keluarga kompiler. Jika Anda ingin menggunakan Highway pada platform lain, silakan ajukan masalah.
Fleksibel untuk diterapkan : Aplikasi yang menggunakan Highway dapat berjalan di cloud heterogen atau perangkat klien, memilih set instruksi terbaik yang tersedia saat runtime. Alternatifnya, pengembang dapat memilih untuk menargetkan satu set instruksi tanpa overhead runtime apa pun. Dalam kedua kasus tersebut, kode aplikasinya sama kecuali untuk menukar HWY_STATIC_DISPATCH
dengan HWY_DYNAMIC_DISPATCH
ditambah satu baris kode. Lihat juga pengenalan @kfjahnke tentang pengiriman.
Cocok untuk berbagai domain : Jalan Raya menyediakan serangkaian operasi ekstensif, digunakan untuk pemrosesan gambar (floating-point), kompresi, analisis video, aljabar linier, kriptografi, pengurutan, dan pembuatan acak. Kami menyadari bahwa kasus penggunaan baru mungkin memerlukan operasi tambahan dan dengan senang hati menambahkannya jika memungkinkan (misalnya tidak ada penurunan kinerja pada beberapa arsitektur). Jika Anda ingin berdiskusi, silakan ajukan masalah.
Menghargai desain paralel data : Highway menyediakan alat seperti Gather, MaskedLoad, dan FixedTag untuk mengaktifkan percepatan pada struktur data lama. Namun, keuntungan terbesar diperoleh dengan merancang algoritma dan struktur data untuk vektor yang dapat diskalakan. Teknik yang berguna mencakup pengelompokan, tata letak struktur array, dan alokasi yang selaras/berlapis.
Kami merekomendasikan sumber daya berikut untuk memulai:
Demo online menggunakan Compiler Explorer:
Kami mengamati bahwa Highway direferensikan dalam proyek sumber terbuka berikut, yang ditemukan melalui sourcegraph.com. Sebagian besar adalah repositori GitHub. Jika Anda ingin menambahkan proyek Anda atau menautkannya secara langsung, silakan ajukan masalah atau hubungi kami melalui email di bawah.
Lainnya
Jika Anda ingin mendapatkan Highway, selain mengkloning dari repositori GitHub ini atau menggunakannya sebagai submodul Git, Anda juga dapat menemukannya di manajer paket atau repositori berikut:
Lihat juga daftarnya di https://repology.org/project/highway-simd-library/versions.
Highway mendukung 24 target, tercantum dalam urutan platform menurut abjad:
EMU128
, SCALAR
;NEON_WITHOUT_AES
, NEON
, NEON_BF16
, SVE
, SVE2
, SVE_256
, SVE2_128
;Z14
, Z15
;PPC8
(v2.07), PPC9
(v3.0), PPC10
(v3.1B, belum didukung karena bug kompiler, lihat #1207; juga memerlukan QEMU 7.2);RVV
(1.0);WASM
, WASM_EMU256
(versi wasm128 yang belum dibuka gulungannya 2x, diaktifkan jika HWY_WANT_WASM2
ditentukan. Ini akan tetap didukung hingga berpotensi digantikan oleh versi WASM yang akan datang.);SSE2
SSSE3
(~Intel Inti)SSE4
(~Nehalem, juga termasuk AES + CLMUL).AVX2
(~ Haswell, juga termasuk BMI2 + F16 + FMA)AVX3
(~Skylake, AVX-512F/BW/CD/DQ/VL)AVX3_DL
(~Icelake, termasuk BitAlg + CLMUL + GFNI + VAES + VBMI + VBMI2 + VNNI + VPOPCNT; memerlukan keikutsertaan dengan mendefinisikan HWY_WANT_AVX3_DL
kecuali dikompilasi untuk pengiriman statis),AVX3_ZEN4
(seperti AVX3_DL tetapi dioptimalkan untuk AMD Zen4; memerlukan keikutsertaan dengan mendefinisikan HWY_WANT_AVX3_ZEN4
jika dikompilasi untuk pengiriman statis, tetapi diaktifkan secara default untuk pengiriman runtime),AVX3_SPR
(~Sapphire Rapids, termasuk AVX-512FP16)Kebijakan kami adalah kecuali ditentukan lain, target akan tetap didukung selama target tersebut dapat dikompilasi (silang) dengan Clang atau GCC yang saat ini didukung, dan diuji menggunakan QEMU. Jika target dapat dikompilasi dengan trunk LLVM dan diuji menggunakan versi QEMU kami tanpa tanda tambahan, maka target tersebut memenuhi syarat untuk disertakan dalam infrastruktur pengujian berkelanjutan kami. Jika tidak, target akan diuji secara manual sebelum rilis dengan versi/konfigurasi Clang dan GCC yang dipilih.
SVE awalnya diuji menggunakan farm_sve (lihat ucapan terima kasih).
Rilis jalan raya bertujuan untuk mengikuti sistem semver.org (MAJOR.MINOR.PATCH), menambah MINOR setelah penambahan yang kompatibel ke belakang dan PATCH setelah perbaikan yang kompatibel ke belakang. Kami merekomendasikan penggunaan rilis (daripada tip Git) karena rilis tersebut diuji lebih ekstensif, lihat di bawah.
Versi 1.0 saat ini menandakan peningkatan fokus pada kompatibilitas ke belakang. Aplikasi yang menggunakan fungsionalitas terdokumentasi akan tetap kompatibel dengan pembaruan di masa mendatang yang memiliki nomor versi utama yang sama.
Pengujian integrasi berkelanjutan dibuat dengan versi terbaru Clang (berjalan pada x86 asli, atau QEMU untuk RISC-V dan Arm) dan MSVC 2019 (v19.28, berjalan pada x86 asli).
Sebelum rilis, kami juga menguji x86 dengan Clang dan GCC, serta Armv7/8 melalui kompilasi silang GCC. Lihat proses pengujian untuk detailnya.
Direktori contrib
berisi utilitas terkait SIMD: kelas gambar dengan baris sejajar, perpustakaan matematika (16 fungsi sudah diterapkan, sebagian besar trigonometri), dan fungsi untuk menghitung perkalian titik dan pengurutan.
Jika Anda hanya memerlukan dukungan x86, Anda juga dapat menggunakan perpustakaan kelas vektor VCL Agner Fog. Ini mencakup banyak fungsi termasuk perpustakaan matematika lengkap.
Jika Anda sudah memiliki kode yang menggunakan intrinsik x86/NEON, Anda mungkin tertarik dengan SIMDe, yang mengemulasi intrinsik tersebut menggunakan intrinsik atau vektorisasi otomatis platform lain.
Proyek ini menggunakan CMake untuk menghasilkan dan membangun. Pada sistem berbasis Debian Anda dapat menginstalnya melalui:
sudo apt install cmake
Tes unit Highway menggunakan googletest. Secara default, CMake Highway mengunduh ketergantungan ini pada waktu konfigurasi. Anda dapat menghindari hal ini dengan mengatur variabel HWY_SYSTEM_GTEST
CMake ke ON dan menginstal gtest secara terpisah:
sudo apt install libgtest-dev
Alternatifnya, Anda dapat menentukan HWY_TEST_STANDALONE=1
dan menghapus semua kemunculan gtest_main
di setiap file BUILD, lalu pengujian menghindari ketergantungan pada GUnit.
Menjalankan tes kompilasi silang memerlukan dukungan dari OS, yang di Debian disediakan oleh paket qemu-user-binfmt
.
Untuk membangun Highway sebagai perpustakaan bersama atau statis (bergantung pada BUILD_SHARED_LIBS), alur kerja CMake standar dapat digunakan:
mkdir -p build && cd build
cmake ..
make -j && make test
Atau Anda dapat menjalankan run_tests.sh
( run_tests.bat
di Windows).
Bazel juga didukung untuk membangun, tetapi tidak digunakan/diuji secara luas.
Saat membuat untuk Armv7, batasan kompiler saat ini mengharuskan Anda menambahkan -DHWY_CMAKE_ARM7:BOOL=ON
ke baris perintah CMake; lihat #834 dan #1032. Kami memahami bahwa upaya sedang dilakukan untuk menghilangkan batasan ini.
Membangun x86 32-bit tidak didukung secara resmi, dan AVX2/3 dinonaktifkan secara default di sana. Perhatikan bahwa johnplatts telah berhasil membangun dan menjalankan pengujian Highway pada x86 32-bit, termasuk AVX2/3, pada GCC 7/8 dan Clang 8/11/12. Di Ubuntu 22.04, Dentang 11 dan 12, tetapi bukan versi yang lebih baru, memerlukan flag kompiler tambahan -m32 -isystem /usr/i686-linux-gnu/include
. Dentang 10 dan yang lebih lama memerlukan plus di atas -isystem /usr/i686-linux-gnu/include/c++/12/i686-linux-gnu
. Lihat #1279.
jalan raya sekarang tersedia di vcpkg
vcpkg install highway
Pelabuhan jalan raya di vcpkg selalu diperbarui oleh anggota tim Microsoft dan kontributor komunitas. Jika versinya sudah kedaluwarsa, silakan buat masalah atau tarik permintaan pada repositori vcpkg.
Anda dapat menggunakan benchmark
di dalam example/ sebagai titik awal.
Halaman referensi cepat mencantumkan secara singkat semua operasi dan parameternya, dan instruction_matrix menunjukkan jumlah instruksi per operasi.
FAQ menjawab pertanyaan tentang portabilitas, desain API, dan di mana menemukan informasi lebih lanjut.
Kami merekomendasikan penggunaan vektor SIMD lengkap bila memungkinkan untuk portabilitas kinerja maksimum. Untuk mendapatkannya, teruskan tag ScalableTag
(atau yang setara dengan HWY_FULL(float)
) ke fungsi seperti Zero/Set/Load
. Ada dua alternatif untuk kasus penggunaan yang memerlukan batas atas pada jalur:
Untuk hingga N
jalur, tentukan CappedTag
atau yang setara dengan HWY_CAPPED(T, N)
. Jumlah jalur sebenarnya akan dibulatkan menjadi N
ke bawah ke pangkat dua terdekat, seperti 4 jika N
adalah 5, atau 8 jika N
adalah 8. Hal ini berguna untuk struktur data seperti matriks sempit. Perulangan masih diperlukan karena vektor sebenarnya mungkin mempunyai jalur kurang dari N
Untuk pangkat dua N
jalur yang tepat, tentukan FixedTag
. N
terbesar yang didukung bergantung pada target, namun dijamin setidaknya 16/sizeof(T)
.
Karena pembatasan ADL, kode pengguna yang memanggil operasi Jalan Raya harus:
namespace hwy { namespace HWY_NAMESPACE {
; ataunamespace hn = hwy::HWY_NAMESPACE; hn::Add()
; atauusing hwy::HWY_NAMESPACE::Add;
. Selain itu, setiap fungsi yang memanggil operasi Jalan Raya (seperti Load
) harus diawali dengan HWY_ATTR
, ATAU berada di antara HWY_BEFORE_NAMESPACE()
dan HWY_AFTER_NAMESPACE()
. Fungsi Lambda saat ini memerlukan HWY_ATTR
sebelum kurung kurawal pembukanya.
Jangan gunakan cakupan namespace atau penginisialisasi static
untuk vektor SIMD karena hal ini dapat menyebabkan SIGILL saat menggunakan pengiriman runtime dan kompiler memilih penginisialisasi yang dikompilasi untuk target yang tidak didukung oleh CPU saat ini. Sebaliknya, konstanta yang diinisialisasi melalui Set
umumnya harus berupa variabel lokal (const).
Titik masuk ke dalam kode menggunakan Highway sedikit berbeda tergantung pada apakah mereka menggunakan pengiriman statis atau dinamis. Dalam kedua kasus tersebut, kami merekomendasikan agar fungsi tingkat atas menerima satu atau lebih pointer ke array, daripada tipe vektor spesifik target.
Untuk pengiriman statis, HWY_TARGET
akan menjadi target terbaik yang tersedia di antara HWY_BASELINE_TARGETS
, yaitu target yang diizinkan untuk digunakan oleh kompiler (lihat referensi cepat). Fungsi di dalam HWY_NAMESPACE
dapat dipanggil menggunakan HWY_STATIC_DISPATCH(func)(args)
dalam modul yang sama tempat mereka didefinisikan. Anda dapat memanggil fungsi dari modul lain dengan membungkusnya dalam fungsi reguler dan mendeklarasikan fungsi reguler di header.
Untuk pengiriman dinamis, tabel penunjuk fungsi dihasilkan melalui makro HWY_EXPORT
yang digunakan oleh HWY_DYNAMIC_DISPATCH(func)(args)
untuk memanggil penunjuk fungsi terbaik untuk target yang didukung CPU saat ini. Sebuah modul secara otomatis dikompilasi untuk setiap target di HWY_TARGETS
(lihat referensi cepat) jika HWY_TARGET_INCLUDE
didefinisikan dan foreach_target.h
disertakan. Perhatikan bahwa pemanggilan pertama HWY_DYNAMIC_DISPATCH
, atau setiap panggilan ke penunjuk yang dikembalikan oleh pemanggilan pertama HWY_DYNAMIC_POINTER
, melibatkan beberapa overhead deteksi CPU. Anda dapat mencegah hal ini dengan memanggil yang berikut ini sebelum pemanggilan HWY_DYNAMIC_*
: hwy::GetChosenTarget().Update(hwy::SupportedTargets());
.
Lihat juga pengantar terpisah tentang pengiriman dinamis oleh @kfjahnke.
Saat menggunakan pengiriman dinamis, foreach_target.h
disertakan dari unit terjemahan (file .cc), bukan header. Header yang berisi kode vektor yang digunakan bersama antara beberapa unit terjemahan memerlukan penjaga penyertaan khusus, misalnya berikut ini diambil dari examples/skeleton-inl.h
:
#if defined(HIGHWAY_HWY_EXAMPLES_SKELETON_INL_H_) == defined(HWY_TARGET_TOGGLE)
#ifdef HIGHWAY_HWY_EXAMPLES_SKELETON_INL_H_
#undef HIGHWAY_HWY_EXAMPLES_SKELETON_INL_H_
#else
#define HIGHWAY_HWY_EXAMPLES_SKELETON_INL_H_
#endif
#include "hwy/highway.h"
// Your vector code
#endif
Berdasarkan konvensi, kami menamai header tersebut -inl.h
karena isinya (seringkali templat fungsi) biasanya berjajar.
Aplikasi harus dikompilasi dengan optimasi diaktifkan. Tanpa memasukkan kode SIMD mungkin melambat dengan faktor 10 hingga 100. Untuk dentang dan GCC, -O2
secara umum sudah cukup.
Untuk MSVC, kami merekomendasikan kompilasi dengan /Gv
untuk memungkinkan fungsi non-inline meneruskan argumen vektor dalam register. Jika ingin menggunakan target AVX2 bersama dengan vektor setengah lebar (misalnya untuk PromoteTo
), penting juga untuk mengkompilasi dengan /arch:AVX2
. Ini tampaknya menjadi satu-satunya cara untuk menghasilkan instruksi SSE berkode VEX di MSVC dengan andal. Terkadang MSVC menghasilkan instruksi SSE berkode VEX jika dicampur dengan AVX, tetapi tidak selalu, lihat DevCom-10618264. Jika tidak, pencampuran instruksi AVX2 berkode VEX dan SSE non-VEX dapat menyebabkan penurunan kinerja yang parah. Sayangnya, dengan opsi /arch:AVX2
, biner yang dihasilkan akan memerlukan AVX2. Perhatikan bahwa tanda seperti itu tidak diperlukan untuk clang dan GCC karena keduanya mendukung atribut spesifik target, yang kami gunakan untuk memastikan pembuatan kode VEX yang tepat untuk target AVX2.
Saat membuat vektorisasi sebuah loop, pertanyaan penting adalah apakah dan bagaimana menangani sejumlah iterasi ('jumlah perjalanan', dilambangkan count
) yang tidak membagi ukuran vektor secara merata N = Lanes(d)
. Misalnya, mungkin perlu menghindari penulisan melewati akhir array.
Di bagian ini, misalkan T
menunjukkan tipe elemen dan d = ScalableTag
. Asumsikan badan perulangan diberikan sebagai template
.
"Strip-mining" adalah teknik untuk membuat vektorisasi suatu loop dengan mengubahnya menjadi loop luar dan loop dalam, sehingga jumlah iterasi pada loop dalam sesuai dengan lebar vektor. Kemudian, loop dalam diganti dengan operasi vektor.
Highway menawarkan beberapa strategi untuk vektorisasi loop:
Pastikan semua input/output diisi. Maka loop (luar) itu sederhana
for (size_t i = 0; i < count; i += N) LoopBody(d, i, 0);
Di sini, parameter template dan argumen fungsi kedua tidak diperlukan.
Ini adalah pilihan yang lebih disukai, kecuali N
berjumlah ribuan dan operasi vektor disalurkan dengan latensi yang panjang. Hal ini terjadi pada superkomputer di tahun 90an, namun saat ini harga ALU murah dan kita melihat sebagian besar implementasi membagi vektor menjadi 1, 2, atau 4 bagian, sehingga hanya ada sedikit biaya untuk memproses seluruh vektor meskipun kita tidak memerlukan semua jalurnya. Memang hal ini menghindari biaya predikasi (yang berpotensi besar) atau pemuatan/penyimpanan sebagian pada target lama, dan tidak menduplikasi kode.
Memproses seluruh vektor dan memasukkan elemen yang telah diproses sebelumnya ke dalam vektor terakhir:
for (size_t i = 0; i < count; i += N) LoopBody(d, HWY_MIN(i, count - N), 0);
Ini adalah opsi pilihan kedua asalkan count >= N
dan LoopBody
idempoten. Beberapa elemen mungkin diproses dua kali, tetapi satu jalur kode dan vektorisasi penuh biasanya sepadan. Bahkan jika count < N
, biasanya masuk akal untuk menambah input/output hingga N
.
Gunakan fungsi Transform*
di hwy/contrib/algo/transform-inl.h. Ini menangani penanganan loop dan sisanya dan Anda cukup mendefinisikan fungsi lambda generik (C++14) atau fungsi yang menerima vektor saat ini dari array input/output, ditambah vektor opsional dari hingga dua array input tambahan, dan mengembalikan nilai yang akan ditulis ke array input/output.
Berikut adalah contoh penerapan fungsi BLAS SAXPY ( alpha * x + y
):
Transform1(d, x, n, y, [](auto d, const auto v, const auto v1) HWY_ATTR {
return MulAdd(Set(d, alpha), v, v1);
});
Proses seluruh vektor seperti di atas, diikuti dengan loop skalar:
size_t i = 0;
for (; i + N <= count; i += N) LoopBody(d, i, 0);
for (; i < count; ++i) LoopBody(CappedTag(), i, 0);
Parameter template dan argumen fungsi kedua sekali lagi tidak diperlukan.
Hal ini menghindari duplikasi kode, dan masuk akal jika count
banyak. Jika count
kecil, putaran kedua mungkin lebih lambat dibandingkan opsi berikutnya.
Proses seluruh vektor seperti di atas, diikuti dengan satu panggilan ke LoopBody
yang dimodifikasi dengan masking:
size_t i = 0;
for (; i + N <= count; i += N) {
LoopBody(d, i, 0);
}
if (i < count) {
LoopBody(d, i, count - i);
}
Sekarang parameter templat dan argumen fungsi ketiga dapat digunakan di dalam LoopBody
untuk 'memadukan' jalur num_remaining
pertama dari v
secara non-atom dengan konten memori sebelumnya di lokasi berikutnya: BlendedStore(v, FirstN(d, num_remaining), d, pointer);
. Demikian pula, MaskedLoad(FirstN(d, num_remaining), d, pointer)
memuat elemen num_remaining
pertama dan mengembalikan nol di jalur lain.
Ini adalah default yang baik ketika tidak mungkin untuk memastikan vektor diisi, tetapi hanya aman #if !HWY_MEM_OPS_MIGHT_FAULT
! Berbeda dengan perulangan skalar, hanya diperlukan satu iterasi terakhir. Peningkatan ukuran kode dari dua badan loop diharapkan bermanfaat karena menghindari biaya masking di semua kecuali iterasi akhir.
Kami telah menggunakan farm-sve oleh Berenger Bramas; ini terbukti berguna untuk memeriksa port SVE pada mesin pengembangan x86.
Ini bukan produk Google yang didukung secara resmi. Hubungi: [email protected]