Golang中GC和内联如何影响代码性能?
Golang中GC和内联如何影响代码性能?
package main
import (
"fmt"
"math"
"os"
"sync"
"text/tabwriter"
"time"
)
func producer(wg *sync.WaitGroup, l sync.Locker) {
defer wg.Done()
for i := 0; i < 15; i++ {
l.Lock()
l.Unlock()
time.Sleep(1)
}
}
func observer(wg *sync.WaitGroup, l sync.Locker) {
defer wg.Done()
l.Lock()
defer l.Unlock()
}
func test(count int, mutext, rwMutex sync.Locker) time.Duration {
var wg sync.WaitGroup
wg.Add(count + 1)
begin := time.Now()
go producer(&wg, mutext)
for i := 0; i < count; i++ {
go observer(&wg, rwMutex)
}
wg.Wait()
return time.Since(begin)
}
func main() {
tw := tabwriter.NewWriter(os.Stdout, 0, 1, 2, ' ', 0)
defer tw.Flush()
var m sync.RWMutex
fmt.Fprintf(tw, "Readers\tRWMutext\tMutex\n")
for i := 0; i < 18; i++ {
count := int(math.Pow(2, float64(i)))
fmt.Fprintf(tw, "%d\t%v\t%v\n", count, test(count, &m, m.RLocker()),
test(count, &m, &m))
}
}
输出结果如下: 读者数 RWMutext Mutex 1 30.173µs 33.075µs 2 6.991µs 67.438µs 4 31.798µs 46.757µs 8 15.977µs 7.22µs 16 99.907µs 38.771µs 32 57.922µs 42.454µs 64 203.127µs 86.813µs 128 98.211µs 140.335µs 256 149.461µs 225.813µs 512 107.206µs 130.003µs 1024 292.564µs 234.575µs 2048 467.862µs 509.994µs 4096 1.090972ms 914.308µs 8192 1.992554ms 1.798584ms 16384 4.314152ms 3.566635ms 32768 8.154836ms 7.483657ms 65536 16.807987ms 14.413483ms 131072 35.100699ms 28.784819ms
看最后一条指令:
fmt.Fprintf(tw, "%d\t%v\t%v\n", count, test(count, &m, m.RLocker()), test(count, &m, &m))
如果我将最后一条指令改为:
fmt.Fprintf(tw, "%d\t%v\t%v\n", count, test(count, &m, &m), test(count, &m, m.RLocker()))
那么我得到: 1 52.005µs 12.136µs 2 10.268µs 6.304µs 4 8.222µs 41.576µs 8 15.647µs 7.521µs 16 48.071µs 60.577µs 32 33.093µs 34.517µs 64 63.459µs 85.228µs 128 110.46µs 108.612µs 256 110.729µs 89.459µs 512 177.386µs 170.756µs 1024 275.454µs 245.833µs 2048 559.328µs 493.242µs 4096 1.029273ms 1.024745ms 8192 1.784677ms 2.066452ms 16384 3.640868ms 4.232521ms 32768 7.27125ms 8.054228ms 65536 14.409217ms 16.423354ms 131072 27.935498ms 32.925838ms
显然,RWMutex 应该产生更好的结果(获取锁的时间更短)。不幸的是,由于我认为是{编译优化、GC、内联}的原因,第二个 test() 调用具有优势。无论我用 RWMutex 还是普通的 Mutex 调用它,都是如此。
为什么会这样?
对于更具批判性思维的人,当我将那一行改为以下内容时会发生什么:
fmt.Fprintf(tw, "%d\t%v\t%v\n", count, test(count, &m, m.RLocker()), test(count, &m, m.RLocker()))
输出是: 1 75.217µs 45.262µs 2 20.555µs 5.608µs 4 23.769µs 33.54µs 8 11.955µs 31.59µs 16 34.788µs 20.266µs 32 180.366µs 111.972µs 64 72.018µs 94.648µs 128 212.692µs 263.827µs 256 114.372µs 76.824µs 512 252.35µs 218.792µs 1024 316.893µs 300.179µs 2048 582.167µs 552.773µs 4096 1.026949ms 1.024502ms 8192 2.043287ms 2.010938ms 16384 4.281553ms 4.092734ms 32768 8.212327ms 8.322347ms 65536 16.556263ms 16.702198ms 131072 33.212763ms 33.016844ms
更多关于Golang中GC和内联如何影响代码性能?的实战教程也可以访问 https://www.itying.com/category-94-b0.html
更多关于Golang中GC和内联如何影响代码性能?的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
这是一个典型的微基准测试陷阱,主要涉及CPU缓存局部性和编译器优化。GC和内联确实会影响性能,但在这个例子中,主要问题是测试顺序效应。
问题分析
1. CPU缓存预热
第一个test()调用会预热CPU缓存和分支预测器,第二个调用因此获得优势:
// 第一个test()调用 - 冷启动
test(count, &m, m.RLocker()) // 缓存未命中,分支预测未训练
// 第二个test()调用 - 热启动
test(count, &m, &m) // 缓存已预热,分支预测已训练
2. 编译器优化
Go编译器会对重复模式进行优化。第二个函数调用可能受益于:
- 内联决策已确定
- 逃逸分析结果已缓存
- 函数栈布局已优化
3. 内存分配模式
第一个调用会初始化内存分配器状态,第二个调用可以复用已分配的内存:
func test(count int, mutext, rwMutex sync.Locker) time.Duration {
var wg sync.WaitGroup // 栈分配
wg.Add(count + 1) // 可能触发堆分配
// 第一个调用:初始化分配器
// 第二个调用:复用已分配的内存块
}
验证示例
可以通过交换测试顺序来验证:
package main
import (
"fmt"
"math"
"os"
"sync"
"text/tabwriter"
"time"
)
// 添加内存屏障防止过度优化
func test(count int, mutext, rwMutex sync.Locker, warmup bool) time.Duration {
var wg sync.WaitGroup
wg.Add(count + 1)
// 强制内存分配
if warmup {
dummy := make([]byte, 1024)
_ = dummy
}
begin := time.Now()
go func() {
defer wg.Done()
for i := 0; i < 15; i++ {
mutext.Lock()
mutext.Unlock()
time.Sleep(1 * time.Nanosecond)
}
}()
for i := 0; i < count; i++ {
go func() {
defer wg.Done()
rwMutex.Lock()
defer rwMutex.Unlock()
}()
}
wg.Wait()
return time.Since(begin)
}
func main() {
tw := tabwriter.NewWriter(os.Stdout, 0, 1, 2, ' ', 0)
defer tw.Flush()
var m sync.RWMutex
fmt.Fprintf(tw, "Readers\tRWMutex\tMutex\tRWMutex2\tMutex2\n")
for i := 0; i < 10; i++ {
count := int(math.Pow(2, float64(i)))
// 预热运行
test(count, &m, m.RLocker(), true)
test(count, &m, &m, true)
// 实际测试 - 多次运行取平均
var rwTotal, mTotal time.Duration
runs := 5
for j := 0; j < runs; j++ {
rwTotal += test(count, &m, m.RLocker(), false)
mTotal += test(count, &m, &m, false)
}
fmt.Fprintf(tw, "%d\t%v\t%v\n",
count,
rwTotal/time.Duration(runs),
mTotal/time.Duration(runs))
}
}
关键影响因素
GC影响
// GC可能在不同时间触发
func producer(wg *sync.WaitGroup, l sync.Locker) {
defer wg.Done()
for i := 0; i < 15; i++ {
l.Lock()
l.Unlock()
time.Sleep(1) // 这里可能触发GC
// 创建临时对象可能增加GC压力
_ = fmt.Sprintf("iteration %d", i)
}
}
内联影响
// 小函数可能被内联
func observer(wg *sync.WaitGroup, l sync.Locker) {
defer wg.Done() // defer可能阻止内联
l.Lock()
defer l.Unlock() // 第二个defer进一步降低内联可能性
}
// 优化版本:提高内联可能性
func observerOptimized(wg *sync.WaitGroup, l sync.Locker) {
l.Lock()
l.Unlock()
wg.Done() // 移除defer,提高内联机会
}
正确的基准测试方法
func benchmarkMutex(b *testing.B) {
var m sync.Mutex
var wg sync.WaitGroup
b.ResetTimer()
for i := 0; i < b.N; i++ {
wg.Add(1)
go func() {
m.Lock()
// 临界区操作
m.Unlock()
wg.Done()
}()
}
wg.Wait()
}
func benchmarkRWMutex(b *testing.B) {
var m sync.RWMutex
var wg sync.WaitGroup
b.ResetTimer()
for i := 0; i < b.N; i++ {
wg.Add(1)
go func() {
m.RLock()
// 只读操作
m.RUnlock()
wg.Done()
}()
}
wg.Wait()
}
使用go test -bench . -benchmem -cpuprofile=cpu.pprof进行分析,可以准确测量GC和内联的影响。

