Go memungkinkan Anda untuk menulis di assembler. Tetapi penulis bahasa menulis perpustakaan standar sedemikian rupa sehingga tidak perlu dilakukan. Ada beberapa cara untuk menulis kode portabel dan cepat secara bersamaan. Bagaimana? Selamat datang di bawah cut.
Memulai menulis fungsi di assembler in go sangat sederhana. Misalnya, mendeklarasikan (meneruskan pernyataan) fungsi 
Add , yang menambahkan 2 int64:
 func Add(a int64, b int64) int64 
Ini adalah fungsi normal, tetapi fungsi tubuh tidak ada. Kompiler akan bersumpah secara wajar ketika mencoba mengkompilasi sebuah paket.
 % go build examples/asm ./decl.go:4:6: missing function body 
Tambahkan file dengan ekstensi .s dan terapkan fungsinya di assembler.
 TEXT ยทAdd(SB),$0-24 MOVQ a+0(FP), AX ADDQ b+8(FP), AX MOVQ AX, ret+16(FP) RET 
Sekarang Anda dapat mengkompilasi, menguji dan menggunakan 
Add sebagai fungsi normal. Ini banyak digunakan oleh pengembang bahasa sendiri dalam paket 
runtime, matematika, bytealg, syscall, reflect, crypto . Ini memungkinkan Anda untuk menggunakan pengoptimalan prosesor dan 
perintah yang tidak terwakili dalam bahasa .
Tetapi ada masalah - fungsi pada asm tidak dapat dioptimalkan dan built-in (inline). Tanpa ini, overhead bisa menjadi penghalang.
 var Result int64 func BenchmarkAddNative(b *testing.B) { var r int64 for i := 0; i < bN; i++ { r = int64(i) + int64(i) } Result = r } func BenchmarkAddAsm(b *testing.B) { var r int64 for i := 0; i < bN; i++ { r = Add(int64(i), int64(i)) } Result = r } 
 BenchmarkAddNative-8 1000000000 0.300 ns/op BenchmarkAddAsm-8 606165915 1.930 ns/op 
Ada beberapa saran untuk assembler inline, seperti arahan 
asm(...) dalam gcc. Tak satu pun dari mereka diterima. Di tempat ini, pergi menambahkan fungsi 
intrinsik .
Fungsi built-in ditulis dalam plain go. Tetapi kompiler tahu bahwa mereka dapat diganti dengan sesuatu yang lebih optimal. Di Go 1.13, fungsi yang disematkan terkandung dalam 
math/bits dan 
sync/atomic .
Fungsi dalam paket ini memiliki tanda tangan mewah. Bahkan, mereka mengulangi tanda tangan perintah prosesor. Ini memungkinkan kompiler, jika arsitektur target mendukung, untuk secara transparan mengganti panggilan fungsi dengan instruksi assembler.
Di bawah ini saya ingin berbicara tentang dua cara berbeda di mana kompiler go membuat kode yang lebih efisien menggunakan fungsi bawaan.
Jumlah populasi
jumlah unit dalam representasi biner dari angka ini adalah primitif kriptografi yang penting. Karena ini adalah operasi yang penting, sebagian besar CPU modern menyediakan implementasi dalam perangkat keras.
Paket 
math/bits menyediakan operasi ini dalam fungsi 
OnesCount* . Mereka dikenali dan diganti dengan 
POPCNT prosesor 
POPCNT .
Untuk melihat bagaimana ini bisa lebih efisien, mari kita bandingkan 3 implementasi. Yang pertama adalah 
algoritma Kernigan .
 func kernighan(x uint64) (count int) { for x > 0 { count++ x &= x - 1 } return count } 
Jumlah siklus algoritma bertepatan dengan jumlah bit yang ditetapkan. Lebih banyak bit - waktu eksekusi lebih lama, yang berpotensi menyebabkan kebocoran informasi pada saluran pihak ketiga.
Algoritma kedua adalah dari 
Hacker's Delight .
 func hackersdelight(x uint64) uint8 { const m1 = 0b0101010101010101010101010101010101010101010101010101010101010101 const m2 = 0b0011001100110011001100110011001100110011001100110011001100110011 const m4 = 0b0000111100001111000011110000111100001111000011110000111100001111 const h1 = 0b0000000100000001000000010000000100000001000000010000000100000001 x -= (x >> 1) & m1 x = (x & m2) + ((x >> 2) & m2) x = (x + (x >> 4)) & m4 return uint8((x * h1) >> 56) } 
Strategi membagi dan menaklukkan memungkinkan versi ini bekerja untuk O (logโ) dari angka yang panjang, dan untuk waktu yang konstan dari jumlah bit, yang penting untuk kriptografi. Mari kita bandingkan kinerja dengan 
math/bits.OnesCount64 .
 func BenchmarkKernighan(b *testing.B) { var r int for i := 0; i < bN; i++ { r = kernighan(uint64(i)) } runtime.KeepAlive(r) } func BenchmarkPopcnt(b *testing.B) { var r int for i := 0; i < bN; i++ { r = hackersdelight(uint64(i)) } runtime.KeepAlive(r) } func BenchmarkMathBitsOnesCount64(b *testing.B) { var r int for i := 0; i < bN; i++ { r = bits.OnesCount64(uint64(i)) } runtime.KeepAlive(r) } 
Sejujurnya, kami meneruskan parameter yang sama ke fungsi: urutan dari 0 hingga bN Ini lebih benar untuk metode Kernigan, karena waktu pelaksanaannya meningkat dengan jumlah bit dari argumen input. 
โ BenchmarkKernighan-4 100000000 12.9 ns/op BenchmarkPopcnt-4 485724267 2.63 ns/op BenchmarkMathBitsOnesCount64-4 1000000000 0.673 ns/op 
math/bits.OnesCount64 menang dalam kecepatan 4 kali. Tetapi apakah itu benar-benar menggunakan implementasi perangkat keras, atau apakah kompiler hanya mengoptimalkan algoritma dari Hackers Delight? Saatnya masuk ke assembler.
 go test -c  
Ada utilitas sederhana untuk membongkar objek go objdump, tapi saya (tidak seperti penulis artikel asli), saya akan menggunakan IDA.
Ada banyak hal yang terjadi di sini. Paling penting: instruksi 
POPCNT x86 dibangun ke dalam kode tes itu sendiri, seperti yang kita harapkan. Ini membuat banchmark lebih cepat daripada alternatif.
Percabangan ini menarik.
 cmp cs:runtime_x86HasPOPCNT, 0 jz lable 
Ya, ini polifil di assembler. Tidak semua prosesor mendukung 
POPCNT . Ketika program dimulai, sebelum 
main Anda, fungsi 
runtime.cpuinit memeriksa apakah ada instruksi yang diperlukan dan menyimpannya di 
runtime.x86HasPOPCNT . Setiap kali program memeriksa apakah mungkin untuk menggunakan 
POPCNT , atau menggunakan polyfile. Karena nilai 
runtime.x86HasPOPCNT tidak berubah setelah inisialisasi, prediksi percabangan prosesor relatif akurat.
Penghitung atom
Fungsi intrinsik adalah kode Go biasa, mereka dapat inline dalam aliran instruksi. Sebagai contoh, kami akan membuat abstraksi dari penghitung dengan metode dari tanda tangan aneh dari fungsi paket atom.
 package main import ( "sync/atomic" ) type counter uint64 func (c *counter) get() uint64 { return atomic.LoadUint64((*uint64)(c)) } func (c *counter) inc() uint64 { return atomic.AddUint64((*uint64)(c), 1) } func (c *counter) reset() uint64 { return atomic.SwapUint64((*uint64)(c), 0) } func F() uint64 { var c counter c.inc() c.get() return c.reset() } func main() { F() } 
Seseorang akan berpikir bahwa OOP akan menambah overhead. Tapi Go bukan Java - bahasa tidak menggunakan binding in runtime kecuali Anda secara eksplisit menggunakan antarmuka. Kode di atas akan diciutkan menjadi aliran instruksi prosesor yang efisien. Seperti apa tampilan utama?
Dalam rangka. 
c.inc berubah menjadi 
lock xadd [rax], 1 - penambahan atom x86. 
c.get menjadi instruksi 
mov biasa, yang sudah menjadi atom di x86. 
c.reset menjadi pertukaran atom 
xchg antara register nol dan memori.
Kesimpulan
Fungsi tertanam adalah solusi rapi yang menyediakan akses ke operasi tingkat rendah tanpa memperluas spesifikasi bahasa. Jika arsitektur tidak memiliki sinkronisasi khusus / primitif atomik (seperti beberapa varian ARM), atau operasi dari matematika / bit, maka kompiler menyisipkan polyfile di perjalanan murni.