Go语言中数组循环函数为何比普通循环快近50%?
关于Go语言两种哈希循环实现的性能疑问
编辑说明
- 编辑:事实证明该现象难以复现,推测可能与宿主操作系统或Linux发行版有关。在同一宿主上用Docker测试不同发行版,结果一致。
- 编辑2:基于评论中提到的GC影响推测,我修改了main.go代码。现在代码会覆盖旧值,理论上不会触发GC。
核心问题
我主要想理解两个问题:
- 为何最终实现相同功能的两个函数性能差异显著?其中一个预先分配对应哈希数量的数组(占用更多内存),另一个仅使用普通循环。
- 如何在不额外占用内存的前提下,优化普通循环函数以获得数组方案的性能优势?(若可行)
基准测试结果
数组方案函数耗时3.6秒,普通循环函数耗时6.4秒。
go version go1.19.2 linux/amd64 goos: linux goarch: amd64 pkg: github.com/gngenius02/shardedmapdb cpu: AMD Ryzen 9 5900X 12-Core Processor BenchmarkGetHashesArray10Million-24 1 3662003126 ns/op 2080037632 B/op 30000051 allocs/op BenchmarkGetHashesLoop10Million-24 1 6462627155 ns/op 1920001352 B/op 30000022 allocs/op PASS
涉及的两个函数为GetHashUsingArray和GetHashUsingLoop。
代码实现
main.go
package main import ( "crypto/sha256" "encoding/hex" ) type HashArray []string type HS struct { LastHash string HashList HashArray } func (h *HS) GetHashUsingArray() { hashit := func(s string) string { digest := sha256.Sum256([]byte(s)) return hex.EncodeToString(digest[:]) } hl := h.HashList for i := 1; i < len(hl); i++ { (hl)[i] = hashit((hl)[i-1]) } h.LastHash = hl[len(hl)-1] } func GetHashUsingLoop(s string, loops int) string { hashit := func(s *string) { digest := sha256.Sum256([]byte(*s)) *s = hex.EncodeToString(digest[:]) } hash := s for i := 0; i < loops; i++ { hashit(&hash) } return hash } func main() {}
main_test.go
package main import ( "testing" ) func BenchmarkGetHashUsingArray10Million(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { firstValue := "abc" hs := HS{"", make(HashArray, 10_000_001)} hs.HashList[0] = firstValue hs.GetHashUsingArray() if hs.LastHash != "bf34d93b4be2a313b06cdf9d805c5f3d140abd872c37199701fb1e43fe479923" { b.Error("Unexpected Result: " + hs.LastHash) } } } func BenchmarkGetHashUsingLoop10Million(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { firstValue := "abc" result := GetHashUsingLoop(firstValue, 10_000_000) if result != "bf34d93b4be2a313b06cdf9d805c5f3d140abd872c37199701fb1e43fe479923" { b.Error("Unexpected result: " + result) } } }
问题解答
1. 性能差异的核心原因
从基准数据看,两个实现的内存分配次数接近,但数组方案耗时少近一半,关键在于内存访问模式和CPU缓存利用率的差异:
- 数组方案使用预先分配的连续内存块,哈希结果直接写入数组对应索引位置,CPU的预取机制能充分发挥作用,缓存命中率极高,大幅减少了内存访问的等待时间。
- 普通循环方案中,Go的字符串是不可变类型,即使你试图覆盖旧值,每次
hex.EncodeToString仍会生成新字符串,变量的内存地址可能频繁变化,导致CPU无法有效预取数据,缓存miss率高,整体执行效率下降。此外,循环中每次调用hashit的指针传递开销,累计1000万次后也会形成性能损耗。
2. 无额外内存的优化方案
要在不额外占用内存的前提下优化普通循环函数,可从内存复用和减少开销入手:
- 复用固定大小的缓冲区:SHA256的hex编码结果长度固定为64字符,预先分配一个字节数组存储结果,避免每次生成新字符串的内存分配。
- 内联哈希逻辑:去掉嵌套的
hashit函数,将哈希和编码逻辑直接写入循环,减少函数调用开销。 - 用字节数组代替字符串:利用字节数组的可变性直接覆盖旧值,减少内存拷贝和分配。
优化后的GetHashUsingLoop示例:
func GetHashUsingLoopOptimized(s string, loops int) string { buf := make([]byte, 64) // 固定大小存储SHA256的hex结果 current := []byte(s) for i := 0; i < loops; i++ { digest := sha256.Sum256(current) hex.Encode(buf, digest[:]) current = buf // 复用缓冲区 } return string(current) }
这个版本仅占用64字节的缓冲区,内存远低于数组方案,同时连续的内存访问能提升CPU缓存命中率,性能可接近数组方案。
内容的提问来源于stack exchange,提问作者cigol on
相关产品推荐
相关产品推荐

