Go ARM64 Map最佳化小記

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

背景

Go內建了map類型,而其中重要的雜湊演算法是一個cityhash的變種。

同時,為了避免雜湊衝突攻擊(collision attack)和加速雜湊計算速度,Keith Randall於Go1.0中就添加了x86_64支援的有硬體加速的AESHASH演算法。我搜遍了互連網,驚訝地發現,這個演算法僅僅在Go裡面有實現,這思路真是絕了。

這就被我這個四處搜尋ARM64 Go runtime待最佳化點的人找到了:ARM64也支援AESHASH的硬體加速指令,但是Go並沒有用上。

我嘴角又微微地一笑,滿心歡喜準備加代碼。可我並不知道,這看似平靜的海面下不知道藏著什麼妖怪……

開工

打蛇打七寸,看代碼實現自然要先看頭。初始化代碼在runtime/alg.go

if (GOARCH == "386" || GOARCH == "amd64") &&GOOS != "nacl" &&support_aes && // AESENCsupport_ssse3 && // PSHUFBsupport_sse41 { // PINSR{D,Q}useAeshash = truealgarray[alg_MEM32].hash = aeshash32algarray[alg_MEM64].hash = aeshash64algarray[alg_STRING].hash = aeshashstr// Initialize with random data so hash collisions will be hard to engineer.getRandomData(aeskeysched[:])return}

可以看到,通過替換algarrary中的hash函數成aeshash,就完成了這個加速替換,充分考慮了未來其他平台的跟進,讚歎同時感到這個簡直就是盛情邀請我來完成接下來的工作。

先看看最簡單的aeshash64的具體實現

//func aeshash64(p unsafe.Pointer, h uintptr) uintptrTEXT runtime·aeshash64(SB),NOSPLIT,$0-24MOVQp+0(FP), AX// ptr to dataMOVQh+8(FP), X0// seedPINSRQ$1, (AX), X0// dataAESENCruntime·aeskeysched+0(SB), X0AESENCruntime·aeskeysched+16(SB), X0AESENCruntime·aeskeysched+32(SB), X0MOVQX0, ret+16(FP)RET

注釋也很清晰,AX載入了資料的指標,然後,把map的種子和資料載入X0中等待計算。這裡要說明一下,每個hashmap在初始化的時候都會隨機分配一個種子,防止駭客找到系統的種子而發起雜湊衝突攻擊。在接下來的幾步中,使用程式初始化時產生的隨機種子 runtime·aeskeysched 對資料進行加密,最後把結果返回。

更複雜的aeshash也只是載入各種長度進行計算而已。

到這,我只能感歎,這太簡單了,便花了兩個周末就寫完了大體的代碼,還碰到了以下問題:

  1. 平台差異
  2. Smhasher
  3. 衝突(Collision)
  4. Go編譯器bug

平台差異

首先發現的問題是,ARM64並沒有X86的AESENC,而是分成兩個指令,AESE和AESMC。

先看了看AES的介紹標準AES加密分成了4步:

  1. AddRoundKey
  2. SubBytes
  3. ShiftRows
  4. MixColumns

X86的AESENC指令等價於:

  1. AddRoundKey
  2. SubBytes
  3. ShiftRows
  4. MixColumns
  5. data XOR Key

但是……ARM64的AESE指令等價於:

  1. data XOR Key
  2. AddRoundKey
  3. SubBytes
  4. ShiftRows

所以,要是單純模仿X86的AESENC X0,X0寫法時……資料就會被清空掉……(摔。無奈,只好另尋出路,用系統隨機種子加密資料,代碼思路如下:

// 把系統種子載入 V1// 再將種子和資料載入 V2AESEV1.B16, V2.B16

SMhasher&Collision

解決了上個問題,開始進行測試。Go使用的是Smhasher,一個Hash函數是否達標需要通過以下測試:

  1. Sanity,必須處理整個key
  2. AppendedZeros,填零,長度不同
  3. SmallKeys,所有小key(< 3位元組)組合
  4. Cyclic,迴圈,例如:11211->11121
  5. Sparse,稀疏,例如:0b00001和 0b00100
  6. Permutation,組合,每個block排列組合順序不同
  7. Avalanche,翻轉每個位
  8. Windowed,例如產生的hash值是32位的,那麼就20位相同,結果也需要不同
  9. Seed,種子變化會影響結果

每一次Smhasher報錯,我都開始看代碼哪裡出了問題,一般都是放錯寄存器這些低級錯誤……Go還會對map中bucket分配情況進行測試,如果某個bucket放了過多的資料,一樣也會出錯。不過,這部分還是比較順利的。

Go編譯器bug

搞定了這個,我又發現另一個坑。為了減少指令,我希望直接把寄存器中的資料直接載入到ARM64 Vector Lane

但是Go的編譯器沒辦法正確編譯不同的lane index。例如下面兩條指令,最終產生的指令碼是一樣……

VMOV R1, V2.D[0]VMOV R1, V2.D[1]

只好先報個bug。

cmd/asm: wrong implement vmov/vld on arm64

然後用原生位元組碼先頂著了,想知道如何做到可以直接拉到Tips

妖怪

終於,所有runtime的Smhasher和Hash測試都通過了,我開始試著運行src/all.bash來構建Go。

這時我拉到了海底的那隻妖怪……

構建日誌

$ ./all.bashBuilding Go cmd/dist using /usr/lib/go-1.6.Building Go toolchain1 using /usr/lib/go-1.6.Building Go bootstrap cmd/go (go_bootstrap) using Go toolchain1.Building Go toolchain2 using go_bootstrap and Go toolchain1.Building Go toolchain3 using go_bootstrap and Go toolchain2.# runtimeduplicate type..hashfunc.struct { "".full "".lfstack; "".empty "".lfstack; "".pad0 [64]uint8; "".wbufSpans struct { "".lock "".mutex; "".free "".mSpanList; "".busy "".mSpanList }; _ uint32; "".bytesMarked uint64; "".markrootNext uint32; "".markrootJobs uint32; "".nproc uint32; "".tstart int64; "".nwait uint32; "".ndone uint32; "".alldone "".note; "".helperDrainBlock bool; "".nFlushCacheRoots int; "".nDataRoots int; "".nBSSRoots int; "".nSpanRoots int; "".nStackRoots int; "".markrootDone bool; "".startSema uint32; "".markDoneSema uint32; "".bgMarkReady "".note; "".bgMarkDone uint32; "".mode "".gcMode; "".userForced bool; "".totaltime int64; "".initialHeapLive uint64; "".assistQueue struct { "".lock "".mutex; "".head "".guintptr; "".tail "".guintptr }; "".sweepWaiters struct { "".lock "".mutex; "".head "".guintptr }; "".cycles uint32; "".stwprocs int32; "".maxprocs int32; "".tSweepTerm int64; "".tMark int64; "".tMarkTerm int64; "".tEnd int64; "".pauseNS int64; "".pauseStart int64; "".heap0 uint64; "".heap1 uint64; "".heap2 uint64; "".heapGoal uint64 }build failed

咦……編譯器構建出錯??? 測試都通過了啊!?

再運行一次all.bash,發現出錯的地方還不一樣???

用gdb斷點在我寫的asm_arm64.s:aeshash,跟蹤執行流程,在對長字串(>129)雜湊也沒有問題。難道是編譯器的bug?

所以我開始跟蹤編譯器的動作,發現只有符號表(symbol)使用了map的功能。惡補了編譯器的基本原理和Go實現後,我才意識到,這個符號表也僅僅用了aeshashstr(對字元做hash)的功能。我把Smhasher中對aeshash的全部改裝成了aeshashstr,發現還是能奇蹟般地通過測試!手動校正了一次,發現就算把aeshash32和aeshash64都搞得和x86實現一樣,包括結果,還是報錯!

於是我把這怪異的問題發郵件,發文章,發群裡問遍了所有人。還是無解。

就這樣折騰了1個月業餘的時間,基本看完了編譯器的相關代碼,發現明明是兩個不同的符號(symbol)還是會被認為是同一個。最後還蛋疼地想用錢看看有沒有人願意幫debug一下。態度惹怒了不少人。我想我是被這bug整得腦子進水了吧……

出坑

直到最近,我才突然意識到沒準Smhasher測試並沒有覆蓋完所有情況?果然,仔細檢查後在aeshash129plus這一段有

SUBS$1, R1, R1BHSaesloop

這個SUBS是SUB再對比,在R1=1的時候,就退出了。但Smhasher僅僅對128個位元組做了完整的測試,所以測試能通過,但是編譯不了。而這個bug僅僅在256個位元組以上才會觸發(摔。

改進後是

SUB$1, R1, R1CBNZR1, aesloop

最終可以提交CL

runtime: implement aeshash for arm64 platform

注意,如果使用PRFM指令,速度能加快30-40MB左右(Hash1024)。可能是下次最佳化的重點(對齊和Cache)

name                  old speed      new speed      deltaHash5                 97.0MB/s ± 0%  97.0MB/s ± 0%  -0.03%  (p=0.008 n=5+5)Hash16                 329MB/s ± 0%   329MB/s ± 0%    ~     (p=0.302 n=4+5)Hash64                 858MB/s ±20%   890MB/s ±11%    ~     (p=0.841 n=5+5)Hash1024              3.50GB/s ±16%  3.57GB/s ± 7%    ~     (p=0.690 n=5+5)Hash65536             4.54GB/s ± 1%  4.57GB/s ± 0%    ~     (p=0.310 n=5+5)

Tips

如何用GNU組合語言產生原生ARM64指令位元組碼?

$cat vld.sld1 {v2.b}[14], [x0]   $as -o vld.o -al vld.lis vld.sAARCH64 GAS  vld.s                      page 1       1 0000 0218404D      ld1 {v2.b}[14], [x0] 

其中第三列就是產生的位元組碼,複製到go中就OK了

WORD $0x4D401802

其實還有工具asm2plan9s, 只是目前這個工具沒辦法編譯ARM64

感謝

最後非常感謝

  • Wei Xiao
  • Fangming
  • Keith Randall

對於我細緻的協助

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.