Tutorial

Platform-independent SIMD di Go 1.27: Vektorisasi Tanpa Assembly

Platform-independent SIMD di Go 1.27: Vektorisasi Tanpa Assembly

Go 1.27 membawa sesuatu yang selama ini hanya bisa didapat lewat assembly: akses SIMD (Single Instruction Multiple Data) yang portabel, tidak terikat arsitektur, dan tidak terikat ukuran vektor. Paket eksperimental bernama simd ini diperkenalkan tim Go lewat tulisan di blog resmi pada 24 September 2026, ditulis David Chase dan Junyang Shao.

Kabar ini penting bukan karena SIMD itu hal baru. SIMD sudah lama ada di CPU modern, dan sejak dulu cara mengaksesnya dari Go adalah menulis assembly. Masalahnya, menulis assembly hanya masuk akal untuk kernel komputasi yang benar-benar kritis. Akibatnya, banyak perangkat lunak yang sebenarnya bisa untung dari SIMD memilih jalan aman dan meninggalkan sebagian besar kapasitas CPU tidak terpakai.

Apa yang sebenarnya ditambahkan

Ada dua lapis yang perlu dipisahkan di sini. Lapis pertama adalah paket archsimd, yang bersifat bergantung arsitektur. Go 1.26 menambahkan API SIMD untuk amd64. Go 1.27 melanjutkannya dengan API untuk arm64 (khususnya NEON) dan wasm.

Lapis kedua, dan ini yang menarik, adalah paket simd yang sepenuhnya portabel. Paket ini menghapus vektor berukuran tetap dari sistem tipe, lalu hanya mendukung operasi yang ada di irisan semua platform. Celah di antara platform diisi dengan emulasi yang efisien memakai instruksi SIMD lain. Paket ini secara longgar mengikuti Highway untuk C++.

Cakupan platform yang didukung paket simd: AVX, AVX2, dan AVX512 di amd64, NEON di arm64, dan instruksi SIMD milik wasm. Untuk memakainya, setel GOEXPERIMENT=simd saat build, sama seperti memakai paket archsimd.

Satu jaminan yang diberikan desain ini: di platform yang tidak punya instruksi SIMD, atau yang belum didukung archsimd, semua operasi diemulasi. Artinya kode yang ditulis memakai paket simd akan selalu jalan, hanya tanpa percepatan perangkat keras.

Kenapa butuh paket terpisah

Alasan utamanya adalah variasi antar arsitektur SIMD yang jauh lebih besar dari yang biasanya dibayangkan. Variasinya setidaknya muncul di tiga dimensi.

Pertama, ukuran vektor. Ada arsitektur yang menyediakan satu ukuran tetap, misalnya wasm, PowerPC, dan s390x di 128 bit. Ada yang menyediakan beberapa ukuran tetap, misalnya amd64 dengan 128, 256, dan 512 bit, serta loong64 dengan 128 dan 256 bit. Riscv64 mendukung vektor berukuran tidak ditentukan antara 128 sampai 65536 bit, meski panjangnya dibatasi pada pangkat dua. Arm64 punya satu ukuran tetap (128 bit, NEON) dan satu ukuran variabel (128 sampai 2048 bit, hanya pangkat dua, lewat SVE).

Konsekuensinya, mengetahui ukuran yang didukung sebuah mesin butuh feature check. Di amd64, mesin ini mendukung AVX, AVX2, atau AVX512? Di arm64, NEON atau SVE? Kalau SVE, seberapa besar, dan varian mana: SVE, SVE2, atau SVE2.1?

Kedua, cara menangani masking. Sebagian varian SIMD tidak menyediakan mask sama sekali, semua operasi bekerja di seluruh elemen, dan masking dilakukan memakai bitmask vektor plus operasi boolean vektor. Ini berlaku di wasm, AVX, AVX2, dan NEON. Sebagian lain menyediakan register mask khusus dengan satu bit mengatur satu elemen vektor, seperti AVX512 dan RVV. SVE mengalokasikan satu bit per byte vektor, tapi bit paling rendah dari tiap elemen yang menentukan operasi termask. AVX2 juga mendukung masked load dan store, tapi memakai vektor biasa sebagai mask dengan bit paling signifikan sebagai penentu.

Ketiga, operasi itu sendiri. Setiap arsitektur punya primitif sendiri untuk menyusun ulang elemen vektor, sebagian menuntut input konstan dan sebagian mendukung input variabel. Dukungan operasi kripto juga berbeda-beda. Bahkan aritmetika dasar bisa beda: wasm tidak punya perbandingan untuk vektor bilangan bulat 64 bit. Untuk panjang vektor tertentu di arsitektur tertentu pun, dukungan instruksi tetap bergantung pada feature yang harus diperiksa.

Paket archsimd memang sudah dirancang seuniform mungkin, tapi banyak keanehan ini tetap tersisa dan membuat penulisan serta pengujian kode SIMD multiplatform jadi berat. Paket simd mengambil jalan lain: menyembunyikan perbedaan itu, bukan merapikannya satu per satu.

Cara memakainya

Tipe vektor di paket ini adalah tipe primitif yang ditulis dengan huruf kapital dan bentuk jamak, misalnya simd.Uint8s atau simd.Float32s. Vektor dimuat dari slice dan disimpan kembali ke slice. Contoh yang dipakai di tulisan resmi Go adalah inner product:

// innerProduct returns the inner product of x and y.
func innerProduct(x, y []float32) float32 {
    var a simd.Float32s
    var i int
    for i = 0; i < len(x)-a.Len()+1; i += a.Len() {
        u := simd.LoadFloat32s(x[i : i+a.Len()])
        v := simd.LoadFloat32s(y[i : i+a.Len()])
        a = u.MulAdd(v, a)
    }
    if i < len(x) {
        u, _ := simd.LoadFloat32sPart(x[i:])
        v, _ := simd.LoadFloat32sPart(y[i:])
        a = u.MulAdd(v, a)
    }
    return sum(a)
}

Ada dua hal yang terlihat dari contoh itu. Pertama, panjang vektor tidak muncul di tipe, tapi diambil dari nilai lewat a.Len(), sehingga loop yang sama tetap benar di mesin dengan lebar vektor berbeda. Kedua, sisa elemen yang tidak cukup untuk satu vektor penuh ditangani LoadFloat32sPart, yang mengembalikan vektor plus jumlah elemen valid.

Keterbatasan yang diakui penulisnya: di rilis eksperimental pertama, belum ada cara umum untuk menjumlahkan seluruh elemen vektor, jadi operasi itu belum didukung di Go 1.27. Contoh di atas karena itu masih memakai fungsi sum bantu yang menyimpan vektor ke slice lalu menjumlahkannya secara skalar. ReduceSum akan muncul di rilis berikutnya sehingga fungsi bantu itu bisa diganti langsung.

Perbandingan menghasilkan nilai mask yang spesifik terhadap lebar elemen, jadi perbandingan Int8s menghasilkan Mask8s, dan seterusnya. Mask itu lalu dipakai untuk memilih dan menyaring vektor lewat operasi seperti IfElse.

Operasi aritmetika yang tersedia untuk tipe bilangan bulat dan float mencakup Add, AddSaturated, Average, Div, Abs, MulAdd, dan IfElse. Ada juga Len, Store, StorePart, String, serta pasangan load dan broadcast untuk tiap lebar tipe.

Bagaimana implementasinya bekerja

Bagian yang paling menarik secara teknis ada di kompilernya. Paket ini memakai penulisan ulang AST yang membuat beberapa salinan terspesialisasi dari fungsi, variabel, dan tipe yang menyebut tipe simd. Tipe simd di salinan itu diganti referensi ke tipe terspesialisasi ukuran di simd/internal/bridge.

Tiap tipe bridge didefinisikan sebagai tipe archsimd dengan himpunan metode terbatas. Fungsi, variabel, dan tipe hasil spesialisasi mendapat akhiran berbentuk @simdNNN, dengan NNN berupa panjang vektor 128, 256, atau 512, atau 0 yang menandakan emulasi.

Fungsi yang menyebut simd di dalam tapi tidak di signature-nya diubah menjadi wrapper yang memilih berdasarkan level SIMD yang dideteksi saat program mulai, lalu memanggil versi terspesialisasi yang sesuai. Fungsi terspesialisasi memanggil fungsi terspesialisasi lain secara langsung, tanpa overhead dispatch, dan berpeluang di-inline.

Strategi penulisan ulang ini dipilih sebagai kompromi antara duplikasi kode dan performa SIMD. Overhead diangkat setinggi yang perlu supaya tidak ada dispatch di dalam komputasi SIMD, tapi tidak lebih tinggi dari itu. Kalau dispatch terasa terlalu rendah di sebuah komputasi, menyebut tipe simd secara sengaja bisa mengangkatnya ke atas. Trik ini bahkan dipakai di contoh benchmark untuk membuat loop benchmark memanggil versi terspesialisasi secara langsung.

Kenapa ini relevan untuk developer Go

Sebelum paket ini ada, developer Go yang butuh SIMD punya dua pilihan yang keduanya tidak enak: menulis assembly dan menanggung beban perawatan multiplatform, atau melewatkan SIMD sama sekali dan menerima performa seadanya.

Paket simd mengubah bentuk keputusan itu. Kode ditulis sekali dalam Go, vektor berukuran tetap hilang dari sistem tipe, dan kompilernya yang menangani spesialisasi per lebar vektor. Untuk pekerjaan yang lebar vektornya bukan bagian dari logika, ini menghapus satu kelas masalah portabilitas.

Yang perlu dicatat, ini masih eksperimental dan butuh flag GOEXPERIMENT=simd. Ada juga operasi yang belum lengkap, dengan ReduceSum sebagai contoh paling jelas yang baru akan datang di rilis berikutnya. Untuk kode produksi, artinya API ini belum sesuatu yang bisa dipatok tanpa risiko berubah.

Namun arahnya cukup jelas. Tim Go bahkan menyebut Go sendiri sebagai pengguna: garbage collector Green Tea memakai SIMD untuk mempercepat pemindaian memori dalam mencari objek hidup. Kalau komponen sedasar GC sudah memanfaatkannya, jalur ini bukan eksperimen yang akan ditinggalkan begitu saja.

Batas yang perlu dipahami sebelum memakai

Ada satu hal yang mudah disalahpahami dari desain ini. Paket simd bukan lapisan tipis di atas semua instruksi yang tersedia di tiap arsitektur. Ia hanya mendukung operasi yang ada di irisan semua platform yang disasar. Kalau sebuah algoritma butuh operasi yang cuma ada di AVX512, paket portabel ini bukan tempatnya, dan developer tetap harus turun ke archsimd atau assembly.

Konsekuensi kedua: emulasi bukan gratis. Di platform tanpa SIMD, atau di platform yang belum didukung archsimd, semua operasi dijalankan lewat emulasi. Kode tetap benar dan tetap jalan, tapi kecepatannya tidak bisa dibandingkan dengan jalur perangkat keras. Jadi klaim "tulis sekali, jalan di mana saja" di sini berarti portabilitas benar, bukan performa seragam.

Ketiga, spesialisasi lewat akhiran @simdNNN berarti satu fungsi sumber bisa melahirkan beberapa salinan di hasil kompilasi. Untuk kode yang kecil ini tidak masalah. Untuk fungsi besar yang menyebut tipe simd di banyak tempat, ukuran biner bisa membengkak karena tiap level SIMD punya salinan sendiri. Penulis paketnya sendiri menyebut strategi ini sebagai kompromi sadar antara duplikasi kode dan performa, bukan solusi yang menghapus trade-off.

Keempat, karena API ini eksperimental dan di balik GOEXPERIMENT=simd, memakainya di kode produksi berarti menerima bahwa nama fungsi, tanda tangan, atau perilaku bisa berubah di rilis berikutnya. Untuk library pihak ketiga, ini juga berarti pengguna library harus menyalakan flag yang sama saat build.

Kapan sebaiknya dicoba sekarang

Untuk kode yang benar-benar dibatasi CPU dan lebar vektornya bukan bagian dari logika, paket ini layak dicoba lebih awal. Contoh yang paling jelas: pemrosesan data dalam jumlah besar, pencarian pola, penghitungan statistik atas array panjang, dan operasi berbasis array yang selama ini jadi bottleneck yang terukur.

Sebaliknya, kalau bottleneck-nya ada di I/O, alokasi memori, atau kode yang sudah terikat API pihak ketiga, SIMD portabel tidak akan mengubah banyak. Mengukur dulu bagian mana yang benar-benar menghabiskan CPU tetap langkah pertama yang benar, sebelum menukar kejelasan kode dengan vektorisasi.

Yang juga berubah adalah bentuk kode yang wajar ditulis. Karena vektor berukuran tetap hilang dari sistem tipe, kode vektorisasi jadi lebih mirip loop biasa: ambil vektor dari slice, operasikan, tangani sisa elemen dengan varian Part. Pola ini lebih mudah dibaca dan lebih mudah diuji dibanding assembly, dan itu memang bagian dari tujuan desainnya, termasuk supaya mudah dipahami kalau nanti kode seperti ini ditulis dengan bantuan model bahasa.

Sumber

Catatan: seluruh rincian API, cakupan platform, dan perilaku kompilasi dalam artikel ini bersumber dari tulisan resmi tim Go dan belum diverifikasi lewat pengujian independen. Paket ini masih eksperimental dan API-nya bisa berubah sebelum stabil.

Rekomendasi Tools & Layanan

Beberapa layanan yang dipake di panduan ini: free trial Alibaba Cloud (coba gratis, sesuaikan kebutuhan), dan halaman promo terbaru buat cek diskon yang lagi jalan bulan ini.

💬 Komentar (0)

Belum ada komentar. Jadilah yang pertama! 💬

Komentar akan muncul setelah moderasi.