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

  • पॉडकास्ट कसे करावे

    माझ्या 4 (!) पॉडकास्टद्वारे मी साहजिकच पॉडकास्टिंगवर बऱ्याच वर्षांपासून अनेक मते तयार केली आहेत. त्यापैकी काही येथे आहेत. आमच्या काळातील दोन बाह्य पॉडकास्ट द्वारकेश आणि TBPN आहेत, आणि मी त्यांच्याबद्दलचे माझे मानसिक मॉडेल एका वेगळ्या पोस्टमध्ये स्पष्ट करेन – मुळात मॅक्क्लस्की वक्र लागू. “सरासरी” पॉडकास्टसाठी मला ठामपणे विश्वास ठेवणारा खालील सल्ला आहे जो चांगले करू…

  • বোল্ডারিং

    বোল্ডারিংয়ের সাথে আমার প্রথম পরিচয় ছিল 2017 সালে, ব্রুকলিন ব্রিজ পার্কের ডাম্বো বোল্ডারে (এখন বন্ধ): আমি তখন এটাকে ঘৃণা করতাম কারণ এটা এত খোঁড়া মনে হয়েছিল। শুধু মানুষের উচ্চতা থেকে সবে উপরে উঠুন এবং আবার নিচে নেমে যান। আমি অবশ্যই এটা ভাল ছিল না. 6 বছর পরে, আমি এখন ~ দৈনিক বোল্ডারিংয়ের 2 মাস সম্পূর্ণ…

  • Revisión y desembalaje de la billetera fría de la tarjeta Ellipal X

    La billetera fría de tarjeta Ellipal X le permite almacenar, intercambiar, comprar, vender y apostar de forma segura más de 10,000 tokens. Esta es la primera billetera fría criptográfica con espacio de aire en una tarjeta del mundo. La tarjeta ELLIPAL X hace que el almacenamiento en frío sea sencillo, potente y siempre listo. La…

  • Pininfarina B95 Gotham at South OC Cars and Coffee

    {“@context”:”http:\/\/schema.org\/”,”@id”:”https:\/\/johnchow.com\/pininfarina-b95-gotham-at-south-oc-cars-and-coffee\/#arve-youtube-ly7nrl50xju-2″,”type”:”VideoObject”,”embedURL”:”https:\/\/www.youtube-nocookie.com\/embed\/Ly7nRL50XJU?feature=oembed&iv_load_policy=3&modestbranding=1&rel=0&autohide=1&playsinline=0&autoplay=0″} While Batman has the Batmobile, Bruce Wayne has the Pininfarina B95 Gotham. A visionary collaboration between Automobili Pininfarina and Wayne Enterprises to build a car befitting its billionaire CEO. This 1900 hp hypercar transcends mere speed, offering an exhilarating experience of daylight freedom, encased in a luxurious design inspired by the vibrant streets of…

  • Swyx 新加坡簡單指南

    為外國友人提供的新加坡私人旅遊指南。 歡迎來到亞洲瓦幹達。 IMO Grab(新加坡的 Uber)對於快速出行至關重要。盡快下載並註冊該應用程式。你絕對可以使用地鐵和公車系統(不需要買卡,如果你可以使用Apple Wallet。可能需要使用金融卡),但不會像你在新加坡短時間旅行所需要的那樣點對點。 早上 6 點至 10 點:非常適合在戶外散步/跑步,因為從前一天晚上開始就會很涼爽。 亨德森在市中心附近的花柏山揮手一座簡單的橋樑,這是一個相對較新的景點,但因其與自然的聯繫而出現在新加坡的許多影片中 麥裡芝水庫 (MacRitchie Reservoir) 更適合認真的跑步者,是城市北部的一條非常受歡迎的路線。新加坡沒有太多天然水,因此強調水循環利用(曾經是新加坡熱門新創公司之一),並使用水庫系統收集雨水進行淨化(並透過從馬來西亞購買水進行補充) 東海岸公園 – 個人最喜歡的公園,因為我住在附近。有很多食物選擇,還有自行車可供出租。 上午 8 點至 10 點 順便去亞坤卡亞吐司餐廳像當地人一樣享用早餐。它們非常普遍。我確實認為亞坤比它的克隆更好。我通常會點「A 套餐 with Teh O Kosong」。我吃的是半熟的蛋,加了一點生抽。 替代早餐:椰漿飯、炒粿條、普拉塔(見下文)或點心 上午 9 點至中午 12 點:大部分商場都會開門營業,此時人潮不會太多,適合逛逛。或者,您也可以提早前往北部的新加坡動物園,該動物園於上午 8.30 開放(不過,如果您想與夜間野生動物園結合使用,請在下午前往,夜間動物園開放時間為晚上 7 點至凌晨 12 點)。 東部:星耀樟宜(但可能會在航班起飛後/起飛前)或淡濱尼購物中心(淡濱尼地鐵站附近有 3 個購物中心) 西邊:裕廊東購物中心(Jem、Westgate 以及裕廊圖書館方向的非購物中心商店)。克萊門蒂也是如此,但程度較小。 在市中心:也許是在烏節路閒逛併步行前往獅城廣場(步行 30 分鐘)的好時機。如果您想在果園結束午餐,請調轉方向。沿途停靠 – 薩默塞特近年來確實取得了長足的進步,尤其是 313 新加坡。 如果你不介意船車的噱頭,那麼預訂新加坡鴨子船遊覽濱海灣地區也不失為一個好主意。或想要更活躍的活動,自行車之旅!…

Deixe um comentário

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