Bahasa Go semakin populer. Begitu yakin bahwa ada semakin banyak konferensi, misalnya,
GolangConf , dan bahasanya
adalah salah satu dari sepuluh teknologi yang paling berbayar. Karena itu, sudah masuk akal untuk membicarakan masalah spesifiknya, misalnya kinerja. Selain masalah umum untuk semua bahasa yang dikompilasi, Go memiliki masalahnya sendiri. Mereka terkait dengan optimizer, stack, sistem tipe, dan model multitasking. Cara untuk menyelesaikannya dan penyelesaiannya terkadang sangat spesifik.
Daniel Podolsky , meskipun penginjil Go, juga menemukan banyak hal aneh dalam dirinya. Semuanya aneh dan, yang paling penting, menarik, mengumpulkan dan
menguji , dan kemudian membicarakannya di HighLoad ++. Transkrip laporan akan mencakup angka, grafik, contoh kode, hasil profiler, perbandingan kinerja algoritme yang sama dalam berbagai bahasa - dan yang lainnya, yang kami benci dengan kata "optimisasi". Tidak akan ada wahyu dalam transkrip - dari mana mereka berasal dalam bahasa yang begitu sederhana - dan segala sesuatu yang dapat dibaca di koran.
Tentang pembicara. Daniil Podolsky : 26 tahun pengalaman, 20 dalam operasi, termasuk pemimpin grup, 5 tahun pemrograman on Go.
Kirill Danshin : pencipta Gramework, Maintainer, HTTP Cepat, Black Go-mage.
Laporan itu disusun bersama oleh Daniel Podolsky dan Kirill Danshin, tetapi Daniel membuat laporan, dan Kirill membantu secara mental.Konstruksi bahasa
Kami memiliki standar kinerja -
direct
. Ini adalah fungsi yang menambah variabel dan tidak lagi melakukan apa pun.
Hasil dari fungsi adalah
1,46 ns per operasi . Ini adalah opsi minimum. Lebih cepat dari 1,5 ns per operasi, mungkin tidak akan berfungsi.
Tunda betapa kami mencintainya
Banyak yang tahu dan suka menggunakan konstruksi bahasa menunda. Cukup sering kita menggunakannya seperti ini.
func BenchmarkDefer(b *testing.B) { for i := 0; i < bN; i++ { incDefer() } } func incDefer() { defer incDirect() }
Tapi Anda tidak bisa menggunakannya seperti itu! Setiap penundaan makan 40 ns per operasi.
// BenchmarkDirect-4 2000000000 1.46 / // defer BenchmarkDefer-4 30000000 40.70 /
Saya pikir mungkin ini karena inline? Mungkin inline sangat cepat?
Direct adalah inline, dan fungsi defer tidak bisa inline. Karena itu, susun fungsi tes terpisah tanpa inline.
func BenchmarkDirectNoInline(b *testing.B) { for i := 0; i < bN; i++ { incDirectNoInline() } }
Tidak ada yang berubah, menunda mengambil 40 ns yang sama. Tunda sayang, tapi tidak bencana.
Di mana fungsi membutuhkan waktu kurang dari 100 ns, Anda dapat melakukannya tanpa menunda.
Tetapi jika fungsinya membutuhkan lebih dari satu mikrodetik, semuanya tetap sama - Anda dapat menggunakan penundaan.
Melewati parameter dengan referensi
Pertimbangkan mitos populer.
func BenchmarkDirectByPointer(b *testing.B) { for i := 0; i < bN; i++ { incDirectByPointer(&testInt64) } } func incDirectByPointer(n *int64) { *n++ }
Tidak ada yang berubah - tidak ada yang sepadan.
// BenchmarkDirectByPointer-4 2000000000 1.47 / BenchmarkDeferByPointer-4 30000000 43.90 /
Kecuali untuk 3 ns per penundaan, tetapi ini dihapuskan karena fluktuasi.
Fungsi Anonim
Terkadang pemula bertanya, "Apakah fungsi anonim mahal?"
func BenchmarkDirectAnonymous(b *testing.B) { for i := 0; i < bN; i++ { func() { testInt64++ }() } }
Fungsi anonim tidak mahal, dibutuhkan 40,4 ns.
Antarmuka
Ada antarmuka dan struktur yang mengimplementasikannya.
type testTypeInterface interface { Inc() } type testTypeStruct struct { n int64 } func (s *testTypeStruct) Inc() { s.n++ }
Ada tiga opsi untuk menggunakan metode kenaikan. Langsung dari Struct:
var testStruct = testTypeStruct{}
.
Dari antarmuka konkret yang sesuai:
var testInterface testTypeInterface = &testStruct
.
Dengan konversi antarmuka runtime:
var testInterfaceEmpty interface{} = &testStruct
.
Di bawah ini adalah konversi antarmuka runtime dan penggunaan langsung.
func BenchmarkInterface(b *testing.B) { for i := 0; i < bN; i++ { testInterface.Inc() } } func BenchmarkInterfaceRuntime(b *testing.B) { for i := 0; i < bN; i++ { testInterfaceEmpty.(testTypeInterface).Inc() } }
Antarmuka, dengan demikian, tidak ada biaya.
// BenchmarkStruct-4 2000000000 1.44 / BenchmarkInterface-4 2000000000 1.88 / BenchmarkInterfaceRuntime-4 200000000 9.23 /
Konversi antarmuka Runtime sepadan, tetapi tidak mahal - Anda tidak perlu menolak secara khusus. Tapi coba lakukan tanpanya jika memungkinkan.
Mitos:- Dereference - petunjuk dereferencing - gratis.
- Fitur anonim gratis.
- Antarmuka gratis.
- Konversi antarmuka runtime - TIDAK GRATIS.
Beralih, petakan, dan iris
Setiap pendatang baru Go bertanya apa yang terjadi jika Anda mengganti sakelar dengan peta. Akankah ini lebih cepat?
Switch datang dalam berbagai ukuran. Saya menguji pada tiga ukuran: kecil untuk 10 kasus, sedang untuk 100 dan besar untuk 1000 kasus. Beralih untuk 1000 kasus ditemukan dalam kode produksi nyata. Tentu saja, tidak ada yang menulisnya dengan tangannya. Ini adalah kode yang dibuat secara otomatis, biasanya tipe switch. Diuji pada dua jenis: int dan string. Tampaknya itu akan menjadi lebih jelas.
Sedikit beralih. Opsi tercepat adalah saklar aktual. Mengikutinya segera pergi slice, di mana indeks integer yang sesuai berisi referensi ke fungsi. Peta bukan pemimpin di int atau string.
Mengaktifkan string secara signifikan lebih lambat daripada di int. Jika Anda dapat beralih bukan ke string, tetapi ke int, maka lakukanlah.
Saklar tengah. Beralih sendiri masih aturan int, tetapi irisan telah menyusul sedikit. Peta masih buruk. Tetapi pada kunci string, peta lebih cepat daripada beralih - seperti yang diharapkan.
Sakelar besar. Seribu kasus menunjukkan kemenangan peta tanpa syarat dalam nominasi "switch by string". Secara teoritis, slice menang, tetapi dalam praktiknya saya menyarankan Anda untuk menggunakan switch yang sama di sini. Peta masih lambat, bahkan mengingat peta memiliki kunci integer dengan fungsi hash khusus. Secara umum, fungsi ini tidak melakukan apa-apa. Int itu sendiri memiliki hash untuk int.
Kesimpulan Peta hanya lebih baik dalam jumlah besar dan tidak dalam kondisi bilangan bulat. Saya yakin bahwa pada salah satu kondisi kecuali int, itu akan berperilaku sama seperti pada string. Slice selalu mengarahkan ketika kondisinya bilangan bulat. Gunakan jika Anda ingin "mempercepat" program Anda sebanyak 2 ns.
Interaksi antar rutin
Topiknya kompleks, saya telah melakukan banyak tes dan akan menyajikan yang paling terbuka. Kita tahu cara-
cara interaksi antarlembaga berikut.
- Atom Ini adalah cara penerapan terbatas - Anda dapat mengganti pointer atau menggunakan int.
- Mutex telah digunakan secara luas sejak Jawa.
- Saluran unik untuk GO.
- Buffered Channel - saluran buffered.
Tentu saja, saya menguji sejumlah besar goroutine yang bersaing untuk satu sumber daya. Tetapi ia memilih tiga untuk dirinya sendiri sebagai indikasi: sedikit - 100, menengah - 1000 dan banyak - 10.000.
Profil beban berbeda . Terkadang semua gorutin ingin menulis dalam satu variabel, tetapi ini jarang terjadi. Biasanya, setelah semua, beberapa menulis, beberapa membaca. Dari sebagian besar pembaca - 90% membaca, dari mereka yang menulis - 90% menulis.
Ini adalah kode yang digunakan sehingga goroutine yang melayani saluran dapat menyediakan bacaan dari dan penulisan ke variabel.
go func() { for { select { case n, ok := <-cw: if !ok { wgc.Done() return } testInt64 += n case cr <- testInt64: } } }()
Jika pesan sampai kepada kami melalui saluran yang kami gunakan untuk menulis, kami melaksanakannya. Jika saluran ditutup, kami menyelesaikan goroutin. Kapan saja, kami siap menulis ke saluran yang digunakan oleh goroutine lain untuk membaca.
Ini adalah data untuk satu goroutine. Tes saluran dilakukan pada dua goroutine: satu memproses Saluran, yang lain menulis ke Saluran ini. Dan opsi ini telah diuji pada satu.
- Menulis langsung ke variabel.
- Mutex mengambil log, menulis ke variabel, dan merilis log.
- Atomic menulis ke variabel melalui Atomic. Ini tidak gratis, tetapi masih jauh lebih murah daripada Mutex pada satu garutin.
Dengan sejumlah kecil goroutine, Atomic adalah cara yang efektif dan cepat untuk menyinkronkan, yang tidak mengejutkan. Direct tidak ada di sini, karena kami memerlukan sinkronisasi, yang tidak disediakannya. Tapi Atomic memiliki kekurangan, tentu saja.
Selanjutnya adalah Mutex. Saya berharap Channel akan secepat Mutex, tapi tidak.
Saluran adalah urutan besarnya lebih mahal daripada Mutex.
Selain itu, Saluran dan Saluran yang disangga keluar dengan harga yang sama. Dan ada Channel, di mana buffer tidak pernah meluap. Ini adalah urutan besarnya lebih murah daripada yang buffernya meluap. Hanya jika buffer di Channel tidak penuh, maka harganya hampir sama dalam urutan besarnya seperti Mutex. Inilah yang saya harapkan dari ujian.
Gambar ini dengan distribusi berapa biayanya diulang pada profil beban apa pun - baik di MostlyRead dan MostlyWrite. Selain itu, Saluran MostlyRead lengkap harganya sama dengan yang tidak lengkap. Dan Channel buffered MostlyWrite, di mana buffer tidak penuh, harganya sama dengan yang lainnya. Saya tidak bisa mengatakan mengapa demikian - saya belum mempelajari masalah ini.
Melewati parameter
Bagaimana cara melewati parameter lebih cepat - dengan referensi atau berdasarkan nilai? Mari kita periksa.
Saya memeriksa sebagai berikut - membuat tipe bersarang dari 1 hingga 10.
type TP001 struct { I001 int64 } type TV002 struct { I001 int64 S001 TV001 I002 int64 S002 TV001 }
Tipe bersarang kesepuluh akan memiliki 10 bidang int64, dan tipe bersarang dari sarang sebelumnya juga akan menjadi 10.
Lalu dia menulis fungsi yang membuat jenis bersarang.
func NewTP001() *TP001 { return &TP001{ I001: rand.Int63(), } } func NewTV002() TV002 { return TV002{ I001: rand.Int63(), S001: NewTV001(), I002: rand.Int63(), S002: NewTV001(), } }
Untuk pengujian, saya menggunakan tiga opsi tipe: kecil dengan nesting 2, medium dengan nesting 3, besar dengan nesting 5. Saya harus melakukan tes yang sangat besar dengan nesting 10 pada malam hari, tetapi di sana gambarnya persis sama dengan untuk 5.
Dalam fungsi, melewati dengan nilai setidaknya dua kali lebih cepat daripada melewati dengan referensi . Ini disebabkan oleh fakta bahwa passing oleh nilai tidak memuat analisis escape. Dengan demikian, variabel yang kami alokasikan ada di tumpukan. Ini jauh lebih murah untuk runtime, untuk pengumpul sampah. Meskipun dia mungkin tidak punya waktu untuk terhubung. Tes ini berlangsung selama beberapa detik - pengumpul sampah mungkin masih tertidur.
Ilmu hitam
Apakah Anda tahu apa yang akan dihasilkan oleh program ini?
package main type A struct { a, b int32 } func main() { a := new(A) aa = 0 ab = 1 z := (*(*int64)(unsafe.Pointer(a))) fmt.Println(z) }
Hasil program tergantung pada arsitektur di mana ia dieksekusi. Pada little endian, misalnya, AMD64, program ditampilkan
. Pada big endian, satu. Hasilnya berbeda, karena pada endian kecil unit ini muncul di tengah-tengah angka, dan pada endian besar - di akhir.
Masih ada prosesor di dunia di mana endian beralih, misalnya, Power PC. Penting untuk mengetahui endian apa yang dikonfigurasikan pada komputer Anda saat startup, sebelum membuat kesimpulan tentang apa trik yang tidak aman dilakukan. Misalnya, jika Anda menulis kode Go yang akan dieksekusi pada beberapa server multiprosesor IBM.
Saya mengutip kode ini untuk menjelaskan mengapa saya menganggap semua ilmu hitam tidak aman. Anda tidak perlu menggunakannya. Tetapi Cyril percaya bahwa itu perlu. Dan inilah alasannya.
Ada fungsi yang melakukan hal yang sama dengan GOB - Go Binary Marshaller. Ini Encoder, tetapi tidak aman.
func encodeMut(data []uint64) (res []byte) { sz := len(data) * 8 dh := (*header)(unsafe.Pointer(&data)) rh := &header{ data: dh.data, len: sz, cap: sz, } res = *(*[]byte)(unsafe.Pointer(&rh)) return }
Bahkan, dibutuhkan sepotong memori dan menarik array byte darinya.
Ini bahkan bukan pesanan - ini adalah dua pesanan. Oleh karena itu, Cyril Danshin, ketika ia menulis kode kinerja tinggi, tidak ragu untuk masuk ke nyali programnya dan membuatnya tidak aman.
Kami akan membahas fitur Go yang lebih spesifik pada 7 Oktober di GolangConf - sebuah konferensi untuk mereka yang menggunakan Go dalam pengembangan profesional, dan mereka yang menganggap bahasa ini sebagai alternatif. Daniil Podolsky hanyalah anggota Komite Program, jika Anda ingin berdebat dengan artikel ini atau mengungkapkan masalah terkait - kirimkan aplikasi untuk laporan.
Untuk yang lainnya, mengenai kinerja tinggi, tentu saja, HighLoad ++ . Kami juga menerima aplikasi di sana. Daftarkan diri Anda untuk menerima buletin dan dapatkan berita terbaru dari semua konferensi kami untuk pengembang web.