Golang实现仅1次内存分配的Unicode字符串反转及Grow分配差异解惑
Unicode字符串反转函数的内存分配优化与
strings.Builder.Grow参数解析 问题描述
我需要实现一个类似reverse.Reverse的Unicode字符串反转函数,当前代码如下:
func Reverse(input string) string { runes := []rune(input) var result strings.Builder result.Grow(len(runes)) for i := len(runes) - 1; i >= 0; i-- { result.WriteRune(runes[i]) } return result.String() }
基准测试显示每次操作产生2次内存分配:
cpu: 11th Gen Intel(R) Core(TM) i7-11850H @ 2.50GHz BenchmarkReverse BenchmarkReverse-16 297900 7014 ns/op 1792 B/op 2 allocs/op
我想知道如何将其优化为仅1次内存分配,同时疑惑为何result.Grow(len(runes))会导致多次分配,而result.Grow(len(input))仅产生1次分配。
一、优化至仅1次内存分配的实现
要把分配次数从2次降到1次,核心是砍掉[]rune(input)的额外分配,直接反向遍历原字符串的Unicode字符并写入预分配足够空间的strings.Builder:
func Reverse(input string) string { var result strings.Builder // 反转后的字符串总字节数和原字符串完全一致,预分配刚好足够的空间 result.Grow(len(input)) // 从字符串末尾反向解码每个Unicode字符 for i := len(input); i > 0; { r, size := utf8.DecodeLastRuneInString(input[:i]) result.WriteRune(r) i -= size } return result.String() }
优化逻辑
- 移除
[]rune(input)的分配:原代码里[]rune(input)会创建新切片存储所有Unicode字符,这是第一次内存开销。改用utf8.DecodeLastRuneInString直接从原字符串末尾解码字符,无需额外切片。 - 精准预分配缓冲区:反转前后字符串的总字节数完全相同(每个字符的UTF-8编码长度不变,只是顺序反转),用
len(input)作为Grow参数,确保strings.Builder一次性分配足够空间,避免后续扩容。 String()无额外开销:strings.Builder.String()通过unsafe直接将内部缓冲区转为字符串,不会触发内存复制。
优化后的基准测试会显示仅1次内存分配,性能也会明显提升。
二、关于strings.Builder.Grow参数的疑问解答
为什么result.Grow(len(runes))会导致多次分配?
len(runes)是Unicode字符的数量,但每个rune在UTF-8编码下需要1~4字节存储(比如ASCII字符占1字节,中文占3字节)。- 调用
Grow(len(runes))时,strings.Builder只分配了len(runes)字节的缓冲区,但实际写入每个rune时,所需字节数往往大于1,导致缓冲区空间不足。 strings.Builder空间不足时会自动扩容(通常按2倍大小扩容),每次扩容都会触发新的内存分配,因此最终出现多次分配的情况。
为什么result.Grow(len(input))仅产生1次分配?
- 反转Unicode字符串时,每个字符的UTF-8编码长度不变,只是顺序反转,因此反转后的字符串总字节数和原字符串的
len(input)完全一致。 - 用
len(input)作为Grow参数,strings.Builder会一次性分配刚好足够的缓冲区空间,后续写入过程中不需要扩容,因此仅需1次内存分配。
内容的提问来源于stack exchange,提问作者smisshin
相关产品推荐
相关产品推荐

