递归函数与内存优化在Golang中的探讨
递归函数与内存优化在Golang中的探讨 我有一个递归函数,其调用次数可能高达 8 亿次。然而,调用的深度从未超过 150 层。
程序运行速度会变慢,资源管理器显示,随着调用次数接近 8 亿,内存使用量逐渐接近可用内存总量。
我的想法是,程序调用速度过快,以至于内存压缩无法及时从栈中回收内存(?)。机器内存为 64GB。
这说得通吗?如果每调用 5 万次就插入一个睡眠或类似操作,让它能跟上,会有帮助吗?
我正在学习使用性能分析器,但我猜测每次调用最多使用 5KB,在深度为 150 的情况下,总共也只有 0.75MB。
我还有一个映射,每调用 10 次就增加一个值。为它设置一个初始大小会有帮助吗?顺便说一下,这个映射目前的值结构体只包含 1 个布尔值(我知道即使使用值为 nil 的映射也可以进一步减少,我正准备这么做)。
感谢阅读。
更多关于递归函数与内存优化在Golang中的探讨的实战教程也可以访问 https://www.itying.com/category-94-b0.html
看起来你应该让函数变成非递归的。递归会有栈开销。 如果性能受到影响,你应该尝试另一种实现。
更多关于递归函数与内存优化在Golang中的探讨的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
栈空间通常在函数调用后立即被回收。你所描述的情况听起来像是在进行分配,而这需要垃圾回收器来回收。
你可以搜索“逃逸分析”,并通过使用“-m”标志编译代码(或使用VS Code插件)来分析你的代码。
如果你能避免堆分配,就应该能解决你的问题(如果我的直觉没错的话)。
// 代码示例:使用逃逸分析标志编译
// go build -gcflags="-m" main.go
答案无疑是使用性能分析器/基准测试。为你的映射指定初始容量提示可以提高性能,但在你确定这就是问题所在之前,它可能作用不大。当你说“每调用 50,000 次就休眠或做点什么,让它跟上”时,听起来你正在使用 goroutine,这也会带来额外的开销。你尝试过将 goroutine 的数量限制在一个合理的范围内并通过通道进行通信吗?
另一个需要注意的问题是:映射不会收缩。看看这篇文章。我假设你并不依赖映射的收缩,但这一点值得考虑。另外——我假设你使用映射而不是切片是因为查找性能很重要。
我写了一些基础的基准测试,给你一些如何入门的思路。首先是代码:
package main
var chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
var stringKeys []string
func init() {
// Create unique keys.
for _, c := range chars {
for _, c2 := range chars {
for _, c3 := range chars {
stringKeys = append(stringKeys, string(c)+string(c2)+string(c3))
}
}
}
}
func main() {
}
func stringMap() int {
var m = make(map[string]int)
for i, v := range stringKeys {
m[v] = i
}
return len(m)
}
func intMap() int {
var m = make(map[int]int)
for i := range stringKeys {
m[i] = i
}
return len(m)
}
func stringMapCapacityHint() int {
var m = make(map[string]int, len(stringKeys))
for i, v := range stringKeys {
m[v] = i
}
return len(m)
}
func intMapCapacityHint() int {
var m = make(map[int]int, len(stringKeys))
for i := range stringKeys {
m[i] = i
}
return len(m)
}
type data struct {
key string
value int
}
func sliceAppend() int {
var s = make([]data, 0)
for i, v := range stringKeys {
s = append(s, data{v, i})
}
return len(s)
}
func slicePreallocate() int {
var s = make([]data, len(stringKeys))
for i, v := range stringKeys {
s[i] = data{v, i}
}
return len(s)
}
以及基准测试:
package main
import "testing"
func BenchmarkStringMap(b *testing.B) {
for n := 0; n < b.N; n++ {
stringMap()
}
}
func BenchmarkIntMap(b *testing.B) {
for n := 0; n < b.N; n++ {
intMap()
}
}
func BenchmarkStringMapCapacityHint(b *testing.B) {
for n := 0; n < b.N; n++ {
stringMapCapacityHint()
}
}
func BenchmarkIntMapCapacityHint(b *testing.B) {
for n := 0; n < b.N; n++ {
intMapCapacityHint()
}
}
func BenchmarkSliceAppend(b *testing.B) {
for n := 0; n < b.N; n++ {
sliceAppend()
}
}
func BenchmarkSlicePreallocate(b *testing.B) {
for n := 0; n < b.N; n++ {
slicePreallocate()
}
}
…结果如下:
$ go test -bench=. -benchmem
goos: windows
goarch: amd64
pkg: mapperformance
cpu: Intel(R) Core(TM) i7-9700K CPU @ 3.60GHz
BenchmarkStringMap-8 87 13981674 ns/op 15480435 B/op 4725 allocs/op
BenchmarkIntMap-8 127 9309430 ns/op 10933689 B/op 4773 allocs/op
BenchmarkStringMapCapacityHint-8 206 5936893 ns/op 7241753 B/op 2 allocs/op
BenchmarkIntMapCapacityHint-8 255 4585321 ns/op 5038069 B/op 14 allocs/op
BenchmarkSliceAppend-8 130 9029515 ns/op 16402408 B/op 29 allocs/op
BenchmarkSlicePreallocate-8 841 1487319 ns/op 3375104 B/op 1 allocs/op
PASS
ok mapperformance 11.421s
总之,开始使用 -benchmem 运行基准测试。并使用性能分析器。最后:是的,任何时候,只要你能为映射提供一个相关的容量提示进行初始化,就请这样做。
这是一个典型的递归深度与调用次数导致的栈内存和堆内存问题。让我们直接分析并给出解决方案。
1. 栈内存分析
你的计算基本正确:150层深度 × 5KB/层 ≈ 0.75MB。但Golang的栈管理机制可能导致问题:
// 示例递归函数
func recursiveFunc(n int, depth int) {
if depth >= 150 || n <= 0 {
return
}
// 局部变量占用栈空间
var buffer [5120]byte // 约5KB
buffer[0] = byte(n % 256)
// 递归调用
recursiveFunc(n-1, depth+1)
}
Golang使用分段栈(1.13前)或连续栈(1.14+),递归调用可能导致栈扩容/缩容开销。8亿次调用会产生大量栈分配操作。
2. 映射优化
映射的初始容量设置确实能显著减少内存分配:
// 优化前
var myMap = make(map[int]bool)
// 优化后 - 预估容量
var myMap = make(map[int]bool, 80000000) // 8千万容量,根据实际需求调整
// 每10次调用添加一个值
func addToMap(key int) {
if key%10 == 0 {
myMap[key] = true
}
}
3. 递归转迭代
这是最有效的优化。将递归改为迭代栈:
// 递归版本
func recursiveProcess(n int) {
if n <= 0 {
return
}
// 处理逻辑
recursiveProcess(n - 1)
}
// 迭代版本 - 显式管理栈
func iterativeProcess(n int) {
type stackFrame struct {
n int
// 其他局部变量
data [5120]byte
}
stack := make([]stackFrame, 0, 150) // 预分配栈容量
stack = append(stack, stackFrame{n: n})
for len(stack) > 0 {
frame := stack[len(stack)-1]
stack = stack[:len(stack)-1]
if frame.n <= 0 {
continue
}
// 处理逻辑
frame.data[0] = byte(frame.n % 256)
// 模拟递归调用
stack = append(stack, stackFrame{n: frame.n - 1})
}
}
4. 尾递归优化
Golang没有自动尾递归优化,但可以手动实现:
// 尾递归形式
func tailRecursive(n int, acc int) int {
if n <= 0 {
return acc
}
// 所有计算在递归调用前完成
newAcc := acc + n
return tailRecursive(n-1, newAcc)
}
// 手动优化为循环
func iterativeTail(n int, acc int) int {
for n > 0 {
acc = acc + n
n--
}
return acc
}
5. 内存回收策略
添加睡眠不是解决方案。使用runtime.GC()或调整GC参数:
import "runtime"
// 定期手动触发GC
func processWithGC(n int) {
for i := 0; i < n; i++ {
// 处理逻辑
if i%1000000 == 0 { // 每100万次触发一次GC
runtime.GC()
}
}
}
// 设置GC百分比(环境变量)
// GOGC=50 表示堆增长50%后触发GC
6. 完整优化示例
package main
import (
"runtime"
"sync"
)
type ProcessState struct {
mu sync.RWMutex
cache map[int]bool
}
func NewProcessState(estimatedSize int) *ProcessState {
return &ProcessState{
cache: make(map[int]bool, estimatedSize),
}
}
func (ps *ProcessState) ProcessIterative(n int) {
// 使用切片作为显式栈
type frame struct {
n int
depth int
data [5120]byte
}
stack := make([]frame, 0, 150)
stack = append(stack, frame{n: n, depth: 0})
callCount := 0
for len(stack) > 0 {
// 弹出栈帧
f := stack[len(stack)-1]
stack = stack[:len(stack)-1]
if f.depth >= 150 || f.n <= 0 {
continue
}
// 处理逻辑
f.data[0] = byte(f.n % 256)
// 每10次调用更新映射
if callCount%10 == 0 {
ps.mu.Lock()
ps.cache[callCount] = true
ps.mu.Unlock()
}
// 压入新栈帧
stack = append(stack, frame{
n: f.n - 1,
depth: f.depth + 1,
})
callCount++
// 定期GC
if callCount%1000000 == 0 {
runtime.GC()
}
}
}
关键点:
- 递归转迭代消除栈深度问题
- 映射预分配减少堆分配
- 定期手动GC控制内存增长
- 使用sync.RWMutex保护并发访问
这些修改应该能解决你的内存问题。使用pprof验证内存分配和GC压力。

