Pergi Tanpa Cabang · Felix Geisendörfer

Pergi Tanpa Cabang · Felix Geisendörfer


Pergi Tanpa Cabang · Felix Geisendörfer

Diterbitkan:

Sepertinya postingan saya sebelumnya berhasil dan banyak dari Anda telah mengirimkan solusi Anda sendiri melalui twitter. Sebelum saya melanjutkan untuk memilih hari lain sebagai tantangan, saya ingin melakukan sedikit tindak lanjut untuk hari 1-1.

Solusi Valentin Deleplace

Mari kita mulai dengan melihat kemenangannya larutan dari Valentin Deleplace yang mencapai hasil spektakuler 13.8 ns/op di mesin saya. Ini 6.5x lebih cepat dari milikku v3 solusi dan 23x lebih cepat dari milikku v1 🤯.

func Answer(input string) (int, error) {
	var prev int64 = -9999
	var increases int = -1

	val := int64(0)
	for p, N := 0, len(input); p < N; p++ {
		c := input(p)
		if c != '\n' {
			val = (val << 8) + int64(c)
		} else {
			if val > prev {
				increases++
			}
			prev = val
			val = 0
		}
	}
	if val > prev {
		increases++
	}
	return increases, nil
}

Saya tahu bahwa solusi saya tidak ideal, namun besarnya peningkatan masih mengejutkan saya. Jadi saya memutuskan untuk melihat lebih dekat untuk melihat bagaimana hal itu dilakukan:

  • Panggilan ke strings.IndexByte() telah dihentikan demi perulangan langsung pada masing-masing karakter masukan. Ini berfungsi dengan baik di sini karena rata-rata panjang garis sangat pendek (4 karakter) dan pemanggilan fungsi rakitan dari Go memiliki overhead yang tidak sepele terkait dengannya.
  • Tanpa diduga, isi fungsi tersebut sesuai dengan anggaran Go. Bangun dengan -gcflags="-m" jika Anda ingin memverifikasinya sendiri. Hal ini menghilangkan biaya tambahan untuk memanggil fungsi Answer di benchmark.
  • Perulangan input dilakukan tanpa menggunakan range pada string input yang menghindari overhead unicode.
  • Penguraian digit masukan secara langsung diintegrasikan ke dalam perulangan for dan menghindari pengulangan karakter tersebut dua kali.
  • val << 8 digunakan sebagai gantinya val*10. Ini berubah valtapi tidak merusak val > prev perbandingan.
  • Daripada menggunakan yang terpisah bool untuk menunjukkan apakah nilai sebelumnya telah terlihat, prev Dan increases diinisialisasi ke -9999 Dan -1 masing-masing merupakan permainan yang adil karena kami melihat para elf ragu-ragu untuk memberikan angka negatif kepada kami.
  • Penanganan kesalahan seperti pemeriksaan digit yang valid telah dihilangkan.

Yang terpenting, solusi Valentin jauh lebih kompak daripada solusi saya v3 dan tetap cukup mudah dibaca mengingat situasinya 👏.

Pergi Tanpa Cabang

Menurut pengalaman saya, optimasi kode selalu melibatkan banyak trial and error. Jadi menurut saya penting untuk menyadari bahwa ide pengoptimalan sering kali gagal dan mendiskusikan apa yang dapat kita pelajari dari kegagalan ini.

Salah satu kegagalan tersebut adalah upaya pertama saya untuk lebih mengoptimalkan solusi Valentin dengan menghilangkan cabang. Inspirasi saya untuk ini datang dari presentasi Parsing JSON Really Quickly: Lessons Learned karya Daniel Lemire serta Branchless Coding karya Matt Nakama di Go.

Saya belum pernah melakukan ini sebelumnya, jadi kode di bawah ini pasti bisa diperbaiki, tapi inilah yang saya hasilkan:

func Answer(input string) (int, error) {
	var prev int64 = -9999
	var increases int = -1

	val := int64(0)
	for p, N := 0, len(input); p < N; p++ {
		c := input(p)
		notNl := boolToInt64(c != '\n')
		increases += int((^notNl & 1) * boolToInt64(val > prev))
		prev = notNl*prev + val*(^notNl&1)
		val = notNl * ((val << 8) + int64(c))
	}
	increases += int(boolToInt64(val > prev))
	return increases, nil
}

func boolToInt64(b bool) int64 {
	return int64((*(*uint8)(unsafe.Pointer(&b))) & 1)
}

Ide intinya adalah untuk melakukan cast bool nilai-nilai ke dalam 0 atau 1 yang memungkinkan kita mengambil cabang seperti ini:

if c != '\n' {
  val = (val << 8) + int64(c)
} else {
  val = 0
}

Dan ubah menjadi ekspresi bebas cabang yang setara seperti yang ditunjukkan di bawah ini:

val = ((val << 8) + int64(c)) * boolToInt64(c != '\n')

Ini berfungsi karena kita selalu menghitung ekspresi dari cabang pertama dan kemudian mengalikannya dengan 1 jika cabang pertama harus diambil, atau 0 untuk cabang kedua sama settingnya val = 0 secara langsung.

Dan sementara boolToInt64(c != '\n') mungkin terlihat seperti mimpi buruk kompilasi, kompiler Go tampaknya mengeluarkan sesuatu yang relatif masuk akal (lihat https://godbolt.org/z/Tzr99vqr3):

CMPB    R8B, $10        ; compare c and '\n' (ASCII 10)
SETNE   "".b+5(SP)      ; store 1 on stack if c != '\n' otherwise 0
MOVBLZX "".b+5(SP), R9  ; move result from stack to register R9
ANDL    $1, R9          ; last part of int64((*(*uint8)(unsafe.Pointer(&b))) & 1

Dari sudut pandang saya, hal seperti ini akan lebih baik:

CMPB    R8B, $10        ; compare c and '\n' (ASCII 10)
SETNE   R9              ; store 1 in R9 if c != '\n' otherwise 0

Namun sayang sekali – saya tidak bisa mendapatkan kompiler untuk menghasilkan ini. Bagaimanapun, karena kita menghilangkan semua cabang, solusi kita seharusnya berjalan sangat cepat sekarang, bukan? Mari kita lihat:

$ benchstat v4-valentin.txt v5.txt 
name      old time/op  new time/op  delta
Answer-6  13.8ns ± 1%  77.1ns ± 0%  +460.24%  (p=0.008 n=5+5)

Aduh. Ternyata sedikit ilmu itu berbahaya 🤕. Awalnya saya cukup sedih dengan hal ini, karena saya telah berusaha keras untuk membuat semua kode “pintar” ini. Namun mengetahui bahwa mengelola emosi sering kali merupakan bagian tersulit – dan karena saat itu jam 3 pagi – saya memutuskan untuk mengakhirinya.

Saya akhirnya menebak-nebak masalahnya saat mengendarai sepeda keesokan harinya. Namun saya mungkin juga bisa mengetahuinya dengan melihat View -> Source output dari profil CPU yang ditunjukkan di bawah ini:

Seperti yang Anda lihat, kami menghabiskan cukup banyak waktu online 36 yang melakukan kondisi increases++ operasi.

Untuk memahami mengapa hal ini buruk, mari kita ingat data masukan yang kita gunakan:

199
200
208
210
200
207
240
269
260
263

Serta kode Valentin yang kami coba optimalkan:

if c != '\n' {
  val = (val << 8) + int64(c)
} else {
  if val > prev {
    increases++
  }
  prev = val
  val = 0
}

Seperti yang Anda lihat, semua baris memiliki 3 digit ditambah satu baris baru, sehingga peluang untuk berakhir di baris asli else blok saja 1/4. Untuk angka masukan yang lebih besar peluangnya akan lebih rendah lagi. Selain itu val > prev predikatnya harus truejadi eksekusi tanpa syarat kami increases++ operasi tidak memiliki peluang yang sangat baik untuk membuahkan hasil dalam banyak kasus.

Namun, kita tahu bahwa val > prev predikat dengan sendirinya adalah true 7 dari 10 kali, jadi mungkin kita bisa mengalahkan prediktor cabang CPU dengan membatasi diri kita hanya untuk menghilangkan cabang itu? Kode untuk ini ditunjukkan di bawah ini:

func Answer(input string) (int, error) {
	var prev int64 = -9999
	var increases int = -1

	val := int64(0)
	for p, N := 0, len(input); p < N; p++ {
		c := input(p)
		if c != '\n' {
			val = (val << 8) + int64(c)
		} else {
			increases += int(boolToInt64(val > prev))
			prev = val
			val = 0
		}
	}
	if val > prev {
		increases++
	}
	return increases, nil
}

func boolToInt64(b bool) int64 {
	return int64((*(*uint8)(unsafe.Pointer(&b))) & 1)
}

Mari kita lihat perbandingannya dengan solusi asli Valentin:

benchstat v4-valentin.txt v6.txt .txt 
name      old time/op  new time/op  delta
Answer-6  13.8ns ± 1%  12.7ns ± 2%  -7.91%  (p=0.008 n=5+5)

Luar biasa, sepertinya kita menghemat nanodetik lagi yang akan diubah oleh para elf menjadi hadiah untuk anak-anak!

Baiklah, itu saja untuk hari ini. Saya harap ini menyenangkan dan saya akan segera meluangkan waktu untuk membuat postingan tentang tantangan AoC lainnya.

Terima kasih telah membaca ini!

Lampiran: FAQ

Saya membayangkan kode bebas cabang di atas menimbulkan beberapa alis. Untuk menghindari kesalahpahaman, izinkan saya mencoba menjawab beberapa pertanyaan yang mungkin Anda miliki:

Apakah Anda merekomendasikan untuk menulis kode Go seperti ini?

Sama sekali tidak. Saya suka menggunakan munculnya kode untuk menjelajah dan belajar. Harap jangan menulis kode produksi seperti ini kecuali “Anda tahu apa yang Anda lakukan”™️. Saya pasti tidak akan menganggap diri saya termasuk dalam grup ini :).

Apakah ini akan berfungsi untuk distribusi data yang berbeda?

Mungkin tidak! Kode ini dioptimalkan untuk input sampel kecil yang digunakan oleh AoC untuk menjelaskan masalahnya.

Mengapa Anda tidak mengimplementasikan algoritma dalam perakitan saja?

Itu mungkin menyenangkan! Namun untuk kemunculan kode tahun ini, saya rasa saya hanya akan menggunakan assembly jika ada di stdlib Go.

–Felix Geisendörfer


Berlangganan blog ini melalui RSS atau E-Mail atau dapatkan pembaruan kecil dari saya melalui Twitter.





Source link

Postagens Similares

  • Podcast Baru Favorit Saya tahun 2023

    Sebagai seseorang yang banyak belajar melalui podcast, saya telah membuat daftar pilihan selama 4 tahun berturut-turut (lihat daftar utama tahun 2019, lalu perbedaan tahun 2020, 2021, dan 2022), jadi inilah waktunya untuk melakukan tahun ke-5(!) Pembaruan 2024: ini reset 2024 Pada tahun 2023 saya akhirnya menjadi pembawa acara podcast semi-sukses, setelah upaya 1, 2, 3….

  • Apple meluncurkan Studio Display baru dan Studio Display XDR yang semuanya baru

    3 Maret 2026 SIARAN PERS Apple meluncurkan Studio Display baru dan Studio Display XDR yang semuanya baru Studio Display XDR adalah layar pro terbaik di dunia, menampilkan layar Retina XDR 5K 27 inci dengan lampu latar mini-LED, kecerahan puncak HDR 2000 nits, dan kecepatan refresh 120Hz CUPERTINO, CALIFORNIA Apple hari ini mengumumkan rangkaian layar baru…

  • 你怎麼知道的

    2014年12月 我讀過維爾哈杜安關於第四次十字軍東徵的編年史至少兩遍,也許三次。然而,如果我必須寫下我所記得的一切,我懷疑它會比一頁多得多。乘以幾百,當我看著我的書架時,我有一種不安的感覺。如果我對這些書的記憶如此之少,那麼閱讀所有這些書有什麼用呢? 幾個月前,當我閱讀康斯坦斯·里德(Constance Reid)撰寫的出色的希爾伯特傳記時,我找到了這個問題的答案,即使不是這個問題的答案,至少也讓我感覺更好了。她寫道: 希爾伯特對數學講座沒有耐心,因為數學講座向學生灌輸大量事實,卻沒有教他們如何提出問題並解決問題。他經常告訴他們「問題的完美表述已經是解決方案的一半」。 在我看來,這一點一直很重要,在聽到希爾伯特的證實後,我更加確信這一點。 但我最初是如何相信這個想法的呢?結合了我自己的經驗和我讀過的其他內容。那一刻我什麼都不記得了!最後我忘了希爾伯特也證實了這一點。但我對這個想法重要性的日益增強的信念仍然是我從這本書中學到的東西,即使在我忘記了我學過它之後。 閱讀和經驗可以訓練你的世界觀。即使你忘記了這段經歷或你讀過的內容,它對你的世界模型的影響仍然存在。你的頭腦就像一個已編譯的程序,但你卻失去了它的來源。它有效,但你不知道為什麼。 我從維爾哈杜安的編年史中尋找的地方不是我從中記住的東西,而是我對十字軍東徵、威尼斯、中世紀文化、攻城戰等的心智模型。這並不意味著我不能更用心地閱讀,但至少閱讀的收穫並不像看起來那麼小。 回想起來,這是顯而易見的事情之一。但這對我來說是一個驚喜,想必對任何其他因為(顯然)忘記他們讀過的東西而感到不安的人來說也是如此。 不過,意識到這一點不僅能讓你對遺忘感覺好一點。有具體的影響。 例如,閱讀和經驗通常是在發生時使用您當時的大腦狀態進行「編譯」的。同一本書在你人生的不同階段會有不同的編輯方式。這意味著重要的書籍非常值得多次閱讀。我過去常常對重讀書籍感到有些疑慮。我無意識地將閱讀與木工等工作混為一談,木工等工作必須重新做某件事就表明你第一次做錯了。而現在「已讀」這個短語似乎幾乎是格式錯誤。 有趣的是,這種意義不僅限於書籍。科技將越來越多地使重溫我們的經驗成為可能。今天,當人們這樣做時,通常是為了再次享受它們(例如,當查看旅行的照片時)或在編譯的代碼中找到某些錯誤的根源(例如,當斯蒂芬·弗萊(Stephen Fry)成功地記住了阻止他唱歌的童年創傷時)。但隨著記錄和回放生活的技術不斷進步,人們在沒有任何目標的情況下重溫經歷可能會變得很常見,只是為了再次從中學習,就像重讀一本書時一樣。 最終,我們不僅能夠回放體驗,還能夠索引甚至編輯它們。因此,儘管不知道如何知道事情似乎是人類的一部分,但事實可能並非如此。 謝謝 感謝 Sam Altman、Jessica Livingston 和 Robert Morris 閱讀本文的草稿。 Source link

  • Nefret edenler

    Ocak 2020 (Başlangıçta bunu, şirketleri büyüdükçe gördükleri ilgiye sıklıkla şaşıran startup kurucuları için tasarlamıştım, ancak bu ünlü olan herkes için de aynı şekilde geçerlidir.) Yeterince ünlü olursanız sizi çok seven hayranlar edinirsiniz. Bu insanlara bazen “hayranlar” denir ve bu tabiri sevmesem de burada onu kullanmak zorunda kalacağım. Onlar için bir söze ihtiyacımız var çünkü bu,…

  • Super Mario Bros. Remastered

    Super Mario Bros. Remastered Super Mario Bros. Remastered to open source, utworzona przez fanów, zremasterowana wersja oryginalnej Super Mario Bros. Zwiastun jest powyżej. Gra obejmuje nowe poziomy, niestandardowe tryby i postacie, niestandardowy edytor poziomów i wiele innych. Potrzebujesz ROM SMB1 NES, aby go odtworzyć – „Żaden z oryginalnych zasobów nie jest zawarty w kodzie źródłowym,…

Deixe um comentário

O seu endereço de email não será publicado. Campos obrigatórios marcados com *