<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          從慢速到SIMG: 一個(gè)Go優(yōu)化的故事

          共 13362字,需瀏覽 27分鐘

           ·

          2024-04-10 22:51

          SourceGraph 的工程師 Camden Cheek 提供的一個(gè)利用 SIMD 進(jìn)行 Go 性能優(yōu)化的故事: From slow to SIMD: A Go optimization story [1]。

          這是一個(gè)關(guān)于某函數(shù)的故事,這個(gè)函數(shù)被大量調(diào)用,而且這些調(diào)用都在關(guān)鍵路徑上。讓我們來(lái)看看如何讓它變快。

          劇透一下,這個(gè)函數(shù)是一個(gè)點(diǎn)積函數(shù)。

          點(diǎn)積(Dot Product),也稱(chēng)為內(nèi)積或數(shù)量積,是一種數(shù)學(xué)運(yùn)算,通常用于計(jì)算兩個(gè)向量之間的乘積。點(diǎn)積的結(jié)果是一個(gè)標(biāo)量(即一個(gè)實(shí)數(shù)),而不是一個(gè)向量。

          假設(shè)有兩個(gè)向量:

          >

          那么,這兩個(gè)向量的點(diǎn)積為:

          一些背景

          在 Sourcegraph,我們正在開(kāi)發(fā)一個(gè)名為 Cody 的 Code AI 工具。為了讓 Cody 能夠很好地回答問(wèn)題,我們需要給它足夠的上下文。我們做的一種方式是利用嵌入[2](embedding)。

          為了我們的目的,嵌入是文本塊的向量表示。它們用某種方式構(gòu)建,以便語(yǔ)義上相似的文本塊具有更相似的向量。當(dāng) Cody 需要更多信息來(lái)回答查詢(xún)時(shí),我們?cè)谇度肷线\(yùn)行相似性搜索,以獲取一組相關(guān)的代碼塊,并將這些結(jié)果提供給 Cody,以提高結(jié)果的相關(guān)性。

          和這篇文章相關(guān)的部分是相似度度量,它是一個(gè)函數(shù),用于判斷兩個(gè)向量有多相似。對(duì)于相似性搜索,常見(jiàn)的度量是余弦相似度[3]。然而,對(duì)于歸一化向量(單位幅度的向量),點(diǎn)積產(chǎn)生的排名與余弦相似度是等價(jià)的。為了運(yùn)行一次搜索,我們計(jì)算數(shù)據(jù)集中每個(gè)嵌入的點(diǎn)積,并保留前幾個(gè)結(jié)果。由于我們?cè)诘玫奖匾纳舷挛闹盁o(wú)法開(kāi)始執(zhí)行 LLM,因此優(yōu)化這一步至關(guān)重要。

          你可能會(huì)想:為什么不使用索引向量數(shù)據(jù)庫(kù)?除了添加我們需要管理的另一個(gè)基礎(chǔ)設(shè)施外,索引的構(gòu)建會(huì)增加延遲并增加資源需求。此外,標(biāo)準(zhǔn)的最近鄰索引只提供近似檢索,這與更易于解釋的窮舉搜索相比,增加了另一層模糊性。鑒于這一點(diǎn),我們決定在我們的手工解決方案中投入一點(diǎn)精力,看看我們能走多遠(yuǎn)。

          目標(biāo)

          下面的代碼是一個(gè)計(jì)算兩個(gè)向量點(diǎn)積的簡(jiǎn)單的 Go 函數(shù)實(shí)現(xiàn)。我的目標(biāo)是刻畫(huà)出我為優(yōu)化這個(gè)函數(shù)所采取的方法,并分享我在這個(gè)過(guò)程中學(xué)到的一些工具。

                
                func DotNaive(a, b []float32) float32 {
           sum := float32(0)
           for i := 0; i < len(a) && i < len(b); i++ {
            sum += a[i] * b[i]
           }
           return sum
          }

          除非另有說(shuō)明,否則所有基準(zhǔn)都在 Intel Xeon Platinum 8481C 2.70GHz CPU 上運(yùn)行。這是一個(gè) c3-highcpu-44 GCE VM。本博客文章中的代碼都可以在這里[4]找到。

          循環(huán)展開(kāi) (Loop unrolling)

          現(xiàn)代的 CPU 都有一個(gè)叫做指令流水線(xiàn)[5]的東西,它可以同時(shí)運(yùn)行多條指令,如果它們之間沒(méi)有數(shù)據(jù)依賴(lài)的話(huà)。數(shù)據(jù)依賴(lài)只是意味著一個(gè)指令的輸入取決于另一個(gè)指令的輸出。

          在我們的簡(jiǎn)單實(shí)現(xiàn)中,我們的循環(huán)迭代之間有數(shù)據(jù)依賴(lài)。實(shí)際上,每個(gè)迭代都有一個(gè)讀/寫(xiě)對(duì),這意味著一個(gè)迭代不能開(kāi)始執(zhí)行,直到前一個(gè)迭代完成。

          一個(gè)常見(jiàn)的方法是在循環(huán)中展開(kāi)一些迭代,這樣我們就可以在沒(méi)有數(shù)據(jù)依賴(lài)的情況下執(zhí)行更多的指令。此外,它將固定的循環(huán)開(kāi)銷(xiāo)(增量和比較)分?jǐn)偟蕉鄠€(gè)操作中。

                
                func DotUnroll4(a, b []float32) float32 {
           sum := float32(0)
           for i := 0; i < len(a); i += 4 {
            s0 := a[i] * b[i]
            s1 := a[i+1] * b[i+1]
            s2 := a[i+2] * b[i+2]
            s3 := a[i+3] * b[i+3]
            sum += s0 + s1 + s2 + s3
           }
           return sum
          }

          在我們的展開(kāi)代碼中,乘法指令的依賴(lài)關(guān)系被移除了,這使得 CPU 可以更好地利用流水線(xiàn)。這使我們的吞吐量比我們的簡(jiǎn)單實(shí)現(xiàn)提高了 37%。

          cf5c37e5da8d04ac681b468b3e5e4af2.webp

          注意,我們實(shí)際上可以通過(guò)調(diào)整我們展開(kāi)的迭代次數(shù)來(lái)進(jìn)一步提高性能。在基準(zhǔn)機(jī)器上,8 似乎是最佳的,但在我的筆記本電腦上,4 的性能最好。然而,改進(jìn)是與平臺(tái)相關(guān)的,而且改進(jìn)相當(dāng)微小,所以在本文的其余部分,我將使用 4 個(gè)展開(kāi)深度來(lái)提高可讀性。

          邊界檢查消除 (Bounds-checking elimination)

          為了防止越界的切片訪(fǎng)問(wèn)成為安全漏洞(如著名的 Heartbleed 漏洞[6]),go 編譯器在每次讀取之前插入檢查。你可以在生成的匯編中查看[7]它(查找 runtime.panic)。

          編譯的代碼看起來(lái)像我們寫(xiě)了這樣的東西:

                
                func DotUnroll4(a, b []float32) float32 {
           sum := float32(0)
           for i := 0; i < len(a); i += 4 {
                  if i >= cap(b) {
                      panic("out of bounds")
                  }
            s0 := a[i] * b[i]
                  if i+1 >= cap(a) || i+1 >= cap(b) {
                      panic("out of bounds")
                  }
            s1 := a[i+1] * b[i+1]
                  if i+2 >= cap(a) || i+2 >= cap(b) {
                      panic("out of bounds")
                  }
            s2 := a[i+2] * b[i+2]
                  if i+3 >= cap(a) || i+3 >= cap(b) {
                      panic("out of bounds")
                  }
            s3 := a[i+3] * b[i+3]
            sum += s0 + s1 + s2 + s3
           }
           return sum
          }

          在像這樣的頻繁調(diào)用循環(huán)(hot loop)中,即使是現(xiàn)代的分支預(yù)測(cè),每次迭代的額外分支也會(huì)增加相當(dāng)大的性能損失。這在我們的例子中尤其明顯,因?yàn)椴迦氲奶D(zhuǎn)限制了我們可以利用流水線(xiàn)的程度。

          如果我們可以告訴編譯器這些讀取永遠(yuǎn)不會(huì)越界,它就不會(huì)插入這些運(yùn)行時(shí)檢查。這種技術(shù)被稱(chēng)為“邊界檢查消除”,相同的模式也適用于 Go 之外的語(yǔ)言。

          理論上,我們應(yīng)該能夠在循環(huán)之外做所有的檢查,編譯器就能夠確定所有的切片索引都是安全的。然而,我找不到正確的檢查組合來(lái)說(shuō)服編譯器我所做的是安全的。我最終選擇了斷言長(zhǎng)度相等的組合,并將所有的邊界檢查移到循環(huán)的頂部。這足以接近無(wú)邊界檢查版本的速度。

                
                func DotBCE(a, b []float32) float32 {
           if len(a) != len(b) {
            panic("slices must have equal lengths")
           }

              if len(a)%4 != 0 {
            panic("slice length must be multiple of 4")
           }

           sum := float32(0)
           for i := 0; i < len(a); i += 4 {
            aTmp := a[i : i+4 : i+4]
            bTmp := b[i : i+4 : i+4]
            s0 := aTmp[0] * bTmp[0]
            s1 := aTmp[1] * bTmp[1]
            s2 := aTmp[2] * bTmp[2]
            s3 := aTmp[3] * bTmp[3]
            sum += s0 + s1 + s2 + s3
           }
           return sum
          }

          這個(gè)邊界檢查的最小化使我們的性能提高了 9%。但是始終未將檢查降到零,沒(méi)有什么值得一提的。

          aee099736e5a97a0b905143344ee5647.webp

          這個(gè)技術(shù)對(duì)于內(nèi)存安全的編程語(yǔ)言來(lái)說(shuō)是非常有用的,比如 Rust。

          一個(gè)問(wèn)題拋給讀者:為什么我們要像a[i:i+4:i+4]這樣切片,而不是只是a[i:i+4]?

          量化 (Quantization)

          目前我們已經(jīng)提高了單核的搜索的吞吐率 50%以上,但現(xiàn)在我們遇到了一個(gè)新的瓶頸:內(nèi)存使用。我們的向量是1536維的。用 4 字節(jié)的元素,這就是每個(gè)向量6KiB,我們每 GiB 代碼生成大約一百萬(wàn)個(gè)向量。這很快就積累起來(lái)了。我們有一些客戶(hù)帶著一些大型的monorepo來(lái)找我們,我們想減少我們的內(nèi)存使用,這樣我們就可以更便宜地支持這些大型代碼庫(kù)。

          一個(gè)可能的緩解措施是將向量移動(dòng)到磁盤(pán)上,但是在搜索時(shí)從磁盤(pán)加載它們可能會(huì)增加顯著的延遲,特別是在慢速磁盤(pán)上。相反,我們選擇用int8量化我們的向量。

          有很多方式可以壓縮向量,但我們將討論整數(shù)量化,這是相對(duì)簡(jiǎn)單但有效的。這個(gè)想法是通過(guò)將4 字節(jié)float32向量元素轉(zhuǎn)換為1 字節(jié)int8來(lái)減少精度。

          我不會(huì)深入討論我們?nèi)绾卧?code style="color:rgb(248,57,41);font-size:14px;font-family:'Operator Mono', Consolas, Monaco, Menlo, monospace;">float32和int8之間進(jìn)行轉(zhuǎn)換,因?yàn)檫@是一個(gè)相當(dāng)深?yuàn)W的話(huà)題[8],但可以說(shuō)我們的函數(shù)現(xiàn)在看起來(lái)像下面這樣:

                
                func DotInt8BCE(a, b []int8) int32 {
           if len(a) != len(b) {
            panic("slices must have equal lengths")
           }

           sum := int32(0)
           for i := 0; i < len(a); i += 4 {
            aTmp := a[i : i+4 : i+4]
            bTmp := b[i : i+4 : i+4]
            s0 := int32(aTmp[0]) * int32(bTmp[0])
            s1 := int32(aTmp[1]) * int32(bTmp[1])
            s2 := int32(aTmp[2]) * int32(bTmp[2])
            s3 := int32(aTmp[3]) * int32(bTmp[3])
            sum += s0 + s1 + s2 + s3
           }
           return sum
          }

          這個(gè)改變導(dǎo)致內(nèi)存使用量減少了 4 倍,但犧牲了一些準(zhǔn)確性(我們進(jìn)行了仔細(xì)的測(cè)量,但這與本博客文章無(wú)關(guān))。

          不幸的是,這個(gè)改變導(dǎo)致我們的性能下降了。查看產(chǎn)生的匯編代碼(go tool compile -S),我們可以看到一些int8int32轉(zhuǎn)換的指令,這可能解釋了差異。我沒(méi)有深入研究,因?yàn)槲覀冊(cè)谙乱还?jié)中的所有性能改進(jìn)都變得無(wú)關(guān)緊要了。

          d361f1c2fc2be8ed751d865b0083fcc0.webp

          SIMD

          到目前為止,速度提升還不錯(cuò),但對(duì)于我們最大的客戶(hù)來(lái)說(shuō),還不夠。所以我們開(kāi)始嘗試一些更激進(jìn)的方法。

          我總是喜歡找借口來(lái)玩 SIMD。而這個(gè)問(wèn)題似乎正好對(duì)癥下藥。

          對(duì)于還不熟悉 SIMD 的同學(xué)來(lái)說(shuō),SIMD 代表“單指令多數(shù)據(jù)”(Single Instruction Multiple Data)。就像它說(shuō)的那樣,它允許你用一條指令在一堆數(shù)據(jù)上運(yùn)行一個(gè)操作。舉個(gè)例子,要對(duì)兩個(gè)int32向量逐元素相加,我們可以用ADD指令一個(gè)一個(gè)地加起來(lái),或者我們可以用VPADDD指令一次加上 64 對(duì),延遲相同(取決于架構(gòu))。

          但是我們還是有點(diǎn)問(wèn)題。Go 不像 C 或 Rust 那樣暴露 SIMD 內(nèi)部函數(shù)。我們有兩個(gè)選擇:用 C 寫(xiě),然后用 Cgo,或者用 Go 的匯編器手寫(xiě)。我盡量避免使用 Cgo,因?yàn)橛泻芏嘣?,這些原因都不是根本原因,但其中一個(gè)原因是 Cgo 會(huì)帶來(lái)性能損失,而這個(gè)片段的性能是至關(guān)重要的。此外,用匯編寫(xiě)一些東西聽(tīng)起來(lái)很有趣,所以我就這么做了。

          我想要這個(gè)這個(gè)算法可以輸出到其他編程語(yǔ)言,所以我限制自己只使用 AVX2 指令,這些指令在大多數(shù) x86_64 服務(wù)器 CPU 上都支持。我們可以使用運(yùn)行時(shí)進(jìn)行檢測(cè)[9],在純 Go 中回退到一個(gè)更慢的選項(xiàng)。

                
                #include "textflag.h"
          TEXT ·DotAVX2(SB), NOSPLIT, $0-52
          // Offsets based on slice header offsets.
          // To check, use `GOARCH=amd64 go vet`
          MOVQ a_base+0(FP), AX
          MOVQ b_base+24(FP), BX
          MOVQ a_len+8(FP), DX
          XORQ R8, R8 // return sum
          // Zero Y0, which will store 8 packed 32-bit sums
          VPXOR Y0, Y0, Y0
          // In blockloop, we calculate the dot product 16 at a time
          blockloop:
          CMPQ DX, $16
          JB reduce
          // Sign-extend 16 bytes into 16 int16s
          VPMOVSXBW (AX), Y1
          VPMOVSXBW (BX), Y2
          // Multiply words vertically to form doubleword intermediates,
          // then add adjacent doublewords.
          VPMADDWD Y1, Y2, Y1
          // Add results to the running sum
          VPADDD Y0, Y1, Y0
          ADDQ $16, AX
          ADDQ $16, BX
          SUBQ $16, DX
          JMP blockloop
          reduce:
          // X0 is the low bits of Y0.
          // Extract the high bits into X1, fold in half, add, repeat.
          VEXTRACTI128 $1, Y0, X1
          VPADDD X0, X1, X0
          VPSRLDQ $8, X0, X1
          VPADDD X0, X1, X0
          VPSRLDQ $4, X0, X1
          VPADDD X0, X1, X0
          // Store the reduced sum
          VMOVD X0, R8
          end:
          MOVL R8, ret+48(FP)
          VZEROALL
          RET

          這個(gè)實(shí)現(xiàn)的核心循環(huán)依賴(lài)于三條主要指令:

          • VPMOVSXBW:將一個(gè)int8加載到一個(gè)int16向量中
          • VPMADDWD:將兩個(gè)int16向量逐個(gè)元素相乘,然后將相鄰的兩對(duì)模糊堆疊相加,生成一個(gè) int32 向量。
          • VPADDD:這將生成的 int32 向量累積到我們的運(yùn)行總和

          VPMADDWD 在這里是真正的主力軍。通過(guò)將乘法和加法步驟合并為一個(gè)步驟,它不僅節(jié)省了指令,還幫助我們避免了溢出問(wèn)題,同時(shí)將結(jié)果擴(kuò)展為 int32

          讓我們看看這給我們帶來(lái)了什么。

          15637f54e56396be3006601f08311b20.webp

          哇,這是我們之前最好表現(xiàn)的 530% 的增加!SIMD 勝利了 ??。

          現(xiàn)在,情況并非一帆風(fēng)順。在 Go 中手寫(xiě)匯編是有點(diǎn)奇怪的。它使用自定義的匯編器,這意味著它的匯編語(yǔ)言看起來(lái)與您通常在網(wǎng)上找到的匯編片段相比,會(huì)有略微不同而令人困惑。它有一些奇怪的怪癖,比如改變指令操作數(shù)的順序或者使用不同的指令名稱(chēng)。在 Go 匯編器中,有些指令甚至沒(méi)有名稱(chēng),只能通過(guò)它們的二進(jìn)制編碼來(lái)使用。不得不說(shuō)一句:我發(fā)現(xiàn) sourcegraph.com 對(duì)于查找 Go 匯編示例非常有價(jià)值,可以供參考。

          話(huà)雖如此,與 Cgo 相比,還是有一些不錯(cuò)的好處。調(diào)試仍然很好用,匯編可以逐步執(zhí)行,并且可以使用 delve 檢查寄存器。沒(méi)有額外的構(gòu)建步驟(不需要設(shè)置 C 工具鏈)。很容易設(shè)置一個(gè)純 Go 的備用方案,所以跨編譯仍然有效。常見(jiàn)問(wèn)題被 go vet 捕捉到。

          SIMD ... 更大

          以前,我們限制自己只使用 AVX2,但如果不這樣呢?AVX-512VNNI 擴(kuò)展添加了 VPDPBUSD 指令,該指令計(jì)算 int8 向量而不是 int16 的點(diǎn)積。這意味著我們可以在單個(gè)指令中處理四倍的元素,因?yàn)槲覀儾槐叵绒D(zhuǎn)換為 int16,并且我們的向量寬度在 AVX-512 中加倍!

          唯一的問(wèn)題是該指令要求一個(gè)向量是有符號(hào)字節(jié),另一個(gè)向量是無(wú)符號(hào)字節(jié)。而我們的兩個(gè)向量都是有符號(hào)的。我們可以借鑒英特爾開(kāi)發(fā)者指南中的技巧來(lái)解決這個(gè)問(wèn)題。給定兩個(gè) int8 元素 anbn,我們進(jìn)行逐元素計(jì)算如下:an * (bn + 128) - an * 128。an * 128 項(xiàng)是將 128 加到 bn 以將其提升到 u8 范圍的超出部分。我們單獨(dú)跟蹤這部分并在最后進(jìn)行減法。該表達(dá)式中的每個(gè)操作都可以進(jìn)行向量化處理。

                
                #include "textflag.h"
          // DotVNNI calculates the dot product of two slices using AVX512 VNNI
          // instructions The slices must be of equal length and that length must be a
          // multiple of 64.
          TEXT ·DotVNNI(SB), NOSPLIT, $0-52
          // Offsets based on slice header offsets.
          // To check, use `GOARCH=amd64 go vet`
          MOVQ a_base+0(FP), AX
          MOVQ b_base+24(FP), BX
          MOVQ a_len+8(FP), DX
          ADDQ AX, DX // end pointer
          // Zero our accumulators
          VPXORQ Z0, Z0, Z0 // positive
          VPXORQ Z1, Z1, Z1 // negative
          // Fill Z2 with 128
          MOVD $0x80808080, R9
          VPBROADCASTD R9, Z2
          blockloop:
          CMPQ AX, DX
          JE reduce
          VMOVDQU8 (AX), Z3
          VMOVDQU8 (BX), Z4
          // The VPDPBUSD instruction calculates of the dot product 4 columns at a
          // time, accumulating into an i32 vector. The problem is it expects one
          // vector to be unsigned bytes and one to be signed bytes. To make this
          // work, we make one of our vectors unsigned by adding 128 to each element.
          // This causes us to overshoot, so we keep track of the amount we need
          // to compensate by so we can subtract it from the sum at the end.
          //
          // Effectively, we are calculating SUM((Z3 + 128) · Z4) - 128 * SUM(Z4).
          VPADDB Z3, Z2, Z3 // add 128 to Z3, making it unsigned
          VPDPBUSD Z4, Z3, Z0 // Z0 += Z3 dot Z4
          VPDPBUSD Z4, Z2, Z1 // Z1 += broadcast(128) dot Z4
          ADDQ $64, AX
          ADDQ $64, BX
          JMP blockloop
          reduce:
          // Subtract the overshoot from our calculated dot product
          VPSUBD Z1, Z0, Z0 // Z0 -= Z1
          // Sum Z0 horizontally. There is no horizontal sum instruction, so instead
          // we sum the upper and lower halves of Z0, fold it in half again, and
          // repeat until we are down to 1 element that contains the final sum.
          VEXTRACTI64X4 $1, Z0, Y1
          VPADDD Y0, Y1, Y0
          VEXTRACTI128 $1, Y0, X1
          VPADDD X0, X1, X0
          VPSRLDQ $8, X0, X1
          VPADDD X0, X1, X0
          VPSRLDQ $4, X0, X1
          VPADDD X0, X1, X0
          // Store the reduced sum
          VMOVD X0, R8
          end:
          MOVL R8, ret+48(FP)
          VZEROALL
          RET

          這種實(shí)現(xiàn)又帶來(lái)了另外 21% 的改進(jìn)。真不賴(lài)!

          0cac5066b22a871c72ddea18b36c9c2d.webp

          下一步

          好吧,我對(duì)吞吐量增加 9.3 倍和內(nèi)存使用量減少 4 倍感到非常滿(mǎn)意,所以我可能會(huì)適可而止了。

          現(xiàn)實(shí)生活中的答案可能是“使用索引”。有大量?jī)?yōu)秀的工作致力于使最近鄰居搜索更快,并且有許多內(nèi)置向量 DB 使其部署相當(dāng)簡(jiǎn)單。

          然而,如果你想要一些有趣的思考,我的一位同事在 GPU 實(shí)現(xiàn)的點(diǎn)積[10]。

          一些有價(jià)值的資料

          • 如果你還沒(méi)有使用過(guò) benchstat[11],你應(yīng)該使用。太棒了。基準(zhǔn)測(cè)試結(jié)果超級(jí)簡(jiǎn)單統(tǒng)計(jì)比較。
          • 不要錯(cuò)過(guò)compiler explorer[12],這是一個(gè)非常有用的挖掘生成的匯編代碼工具。
          • 還有一次,我被技術(shù)上的挑戰(zhàn)吸引,實(shí)現(xiàn)了ARM NEON 的版本[13],這帶來(lái)了一些有趣的對(duì)比。
          • 如果您還沒(méi)有遇到過(guò)它,Agner Fog 說(shuō)明表[14]會(huì)讓您大吃一驚,很多底層優(yōu)化的參考資料。在優(yōu)化點(diǎn)積函數(shù)的工作中,我使用它們來(lái)理解指令延遲的差異,以及為什么某些流水線(xiàn)優(yōu)于其他流水線(xiàn)。
          參考資料 [1]

          From slow to SIMD: A Go optimization story: https://sourcegraph.com/blog/slow-to-simd

          [2]

          嵌入: https://platform.openai.com/docs/guides/embeddings

          [3]

          余弦相似度: https://en.wikipedia.org/wiki/Cosine_similarity

          [4]

          這里: https://github.com/camdencheek/simd_blog

          [5]

          指令流水線(xiàn): https://chadaustin.me/2009/02/latency-vs-throughput/

          [6]

          Heartbleed 漏洞: https://en.wikipedia.org/wiki/Heartbleed

          [7]

          查看: https://go.godbolt.org/z/qT3M7nPGf

          [8]

          話(huà)題: https://huggingface.co/docs/optimum/concept_guides/quantization

          [9]

          檢測(cè): https://sourcegraph.com/github.com/sourcegraph/sourcegraph@3ac2170c6523dd074835919a1804f197cf86e451/-/blob/internal/embeddings/dot_amd64.go?L17-21

          [10]

          GPU 實(shí)現(xiàn)的點(diǎn)積: https://github.com/sourcegraph/sourcegraph/compare/simd-post-gpu-embeddings~3...simd-post-gpu-embeddings

          [11]

          benchstat: https://pkg.go.dev/golang.org/x/perf/cmd/benchstat

          [12]

          compiler explorer: https://go.godbolt.org/z/qT3M7nPGf

          [13]

          ARM NEON 的版本: https://github.com/camdencheek/simd_blog/blob/main/dot_arm64.s

          [14]

          Agner Fog 說(shuō)明表: https://www.agner.org/optimize/


          - END -

          推薦閱讀:

          節(jié)后首場(chǎng)meet up,議題征集及現(xiàn)場(chǎng)招聘正式啟動(dòng)

          2024年的Rust與Go

          我是如何實(shí)現(xiàn)Go性能5倍提升的?

          「GoCN酷Go推薦」我用go寫(xiě)了魔獸世界登錄器?

          Go區(qū)不大,創(chuàng)造神話(huà),科目三殺進(jìn)來(lái)了

          Go 1.22新特性前瞻

          這些流行的K8S工具,你都用上了嗎


          想要了解Go更多內(nèi)容,歡迎掃描下方??關(guān)注公眾號(hào), 回復(fù)關(guān)鍵詞 [實(shí)戰(zhàn)群]   ,就有機(jī)會(huì)進(jìn)群和我們進(jìn)行交流



          分享、在看與點(diǎn)贊Go 
          瀏覽 51
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  青娱乐国产一区 | 亚洲国产精品成人va在线观看 | 91久久婷婷国产麻豆精品 | 色色热热热 | 四虎影库男人精品 |