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

  • Фатальний щипок

    Грудень 2014 року Багато стартапів за кілька місяців до смерті проходять через такий момент, коли, хоча вони мають значну суму грошей у банку, вони також багато втрачають щомісяця, а зростання доходу або не існує, або є посереднім. Компанія має, скажімо, 6 місяців злітної смуги. Або, кажучи більш жорстоко, за 6 місяців до того, як вони…

  • 评论:这就是它听起来的样子(罗杰斯和奥加斯)

    在 这就是它听起来的样子:你喜欢的音乐告诉你什么苏珊·罗杰斯(Susan Rogers)(与合著者奥吉·奥加斯(Ogi Ogas))为爱上歌曲的体验提供了一个科学的框架。本书的核心是听众档案,这是一个对我们的音乐“最佳听点”进行分类的系统框架。罗杰斯将聆听体验分为七个主要维度: “什么”: 旋律、歌词、节奏和音色。 “如何”: 真实性、新颖性和现实性。 虽然罗杰斯作为认知神经科学家的背景很突出,但她平衡了这一点与她在普林斯期间为普林斯进行时间工程的各种“幕后”轶事。 紫雨 时代以及她与 Geggy Tah 和 Barenaked Ladies 的合作。这些片段为临床方法提供了必要的人类脉搏。 有趣的方面之一是罗杰斯对新颖性和我们对音乐风险的兴趣的讨论。这是一条新颖性-流行度曲线:最简单、最熟悉的音乐在左边;右边是突破界限、复杂的音乐;纵轴为销量/受欢迎程度。这条曲线在不断发展,今天可能被认为很复杂的东西在未来很容易变得更加熟悉。 尽管它的形状代代相传,但随着不同的音乐创新变得司空见惯,曲线本身沿着新颖的轴稳定地向右滑动。曲线的顶峰——以及最流行的音乐风格——保留了熟悉和新颖元素的平衡,但随着观众习惯了音乐的进步,这些元素听起来会发生变化。 来源: 这就是它听起来的样子:你喜欢的音乐告诉你什么 通过苏珊·罗杰斯 我忍不住将其与雷蒙德·威廉姆斯的文化进化理论进行了比较: 主导的: 定义当前“主流”的音乐。 剩余: 过去的声音仍然塑造着我们的现在。 紧急情况: 突破界限的新“新颖”表达方式。 罗杰斯认为,我们的“唱片制作人大脑”正在不断扫描这些元素。然而,品味很少是静态的。您可能会发现您的个人资料会随着年龄的增长或根据您的社交环境而变化。这种流动性表明,听众档案并不是固定的 DNA 序列,而是随着我们的生命阶段而演变的活文件。 我们为自己构建的身份反映在我们收集和喜欢的东西中,以至于当我们揭示我们吃的食物、我们喜欢的爱好或我们喜欢的音乐流派的巨大变化时,了解我们的人明白我们身份的一些重要的东西已经发生了变化。实证研究表明,我们的个人身份概念与我们的音乐选择有关。 来源: 这就是它听起来的样子:你喜欢的音乐告诉你什么 通过苏珊·罗杰斯 另一件引人注目的事情是罗杰斯的个人资料和罪恶感的快乐概念之间的紧张关系。当我们将罗杰斯的科学与唐纳德·温尼科特的真我和假我的概念结合起来时,“内疚的快乐”往往只是我们之间的冲突 真实的自我 (对旋律的原始情感反应)和 虚假的自我 (我们为了满足社会期望而呈现的角色)。罗杰斯对该框架的意图是提供剥离“虚假自我”所需的结构,并理解为什么特定的音色或节奏会引起我们的共鸣,无论其感知的“酷”如何。与 Chilly Gonzales 的《Enya: A Treatise on Unguilty Pleasures》以及如何把握个人品味一起思考这一点是很有趣的。 在录音室安静的时刻,我喜欢请唱片制作人说出一种罪恶感的快乐——一张你会不好意思承认自己喜欢的唱片。这样的坦白可以具有深刻的启示意义。我们珍视的唱片暗中反映了我们音乐自我的各个方面,而我们不想让别人知道这些方面。 来源: 这就是它听起来的样子:你喜欢的音乐告诉你什么 通过苏珊·罗杰斯 曾经有人告诉我“没有人喜欢你的音乐”。当然,尽管如此,但讽刺的是,当谈到听众档案时,这是事实,我们都是独一无二的,没有平均水平。作为神经线路、个人历史和情感关联的结合,我们与歌曲的关系是独一无二的。 如果米歇尔·法贝尔…

  • ਪੈਨਸੀਵ: ਮਾਰਚ 8 2024 – ਡੂਨ 2 ਨੂੰ

    ਜਨਤਕ ਵਿਚਾਰਾਂ ਦਾ ਸੰਗ੍ਰਹਿ ਜੋ ਬਲੌਗਪੋਸਟ ਹੋ ਸਕਦਾ ਹੈ ਪਰ ਮੇਰੇ ਕੋਲ ਸਮਾਂ ਨਹੀਂ ਹੈ, ਇਸ ਲਈ ਇੱਥੇ ਛੋਟਾ ਰੂਪ ਹੈ। ਮੈਂ ਭਵਿੱਖ ਵਿੱਚ ਇਹਨਾਂ ਨੂੰ ਪੂਰੀਆਂ ਪੋਸਟਾਂ ਵਿੱਚ ਅੱਪਗ੍ਰੇਡ ਕਰ ਸਕਦਾ ਹਾਂ। Source link

  • Riwaya na Uzushi

    Novemba 2019 Ukigundua jambo jipya, kuna uwezekano mkubwa kwamba utashutumiwa kwa aina fulani ya uzushi. Ili kugundua mambo mapya, unapaswa kufanyia kazi mawazo ambayo ni mazuri lakini yasiyo dhahiri; ikiwa wazo ni zuri, labda watu wengine tayari wanalifanyia kazi. Njia moja ya kawaida ya wazo zuri kutokuwa dhahiri ni kwa kufichwa kwenye kivuli cha dhana…

  • Jinsi ya kubadilisha kikoa maalum kwenye Substack

    Tangu Septemba, nimekuwa nikiendesha jarida langu la AI kwenye https://lspace.swyx.io Tangu wakati huo nimepata kikoa cha https://latent.space, lakini hati chafu za Substack haitoi usaidizi wowote wa jinsi ya kupitia mabadiliko maalum ya kikoa (jinsi ya kuongeza moja kwa mara ya kwanza). na kitufe cha Lemaza Kikoa Maalum kinatisha sana bila huruma yoyote kuhusu jinsi ya…

  • Rozwiązanie Anthropic Vision Advantage jest bardzo podobne do rozwiązania Apple z 2010 roku

    Konkurując z Anthropic, OpenAI i Google mają więcej niż tylko modelowy problem. Anthropic przypomina teraz Apple w 2010 roku z iPhonem. A Opus 4.5 jest jak ich iPhone. Rzecz w tym, że tak naprawdę nie chodzi tu o model czy telefon. To ekosystem. Apple dominował nie tylko dlatego, że miał lepszy sprzęt, ale także dlatego,…

Deixe um comentário

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