GoLang 高性能编程之字符串拼接
看代码突然想到一个问题:字符串在内存中是怎么表示的?花了大半天才理清,这里记录梳理下。
1. 字符
提到字符串需要先了解字符,没有字符哪能串起来呢。不像 int,float 这种直接在内存中以位数表示的类型,字符需要经过编码才能存在内存中。如字符 'A' 的 ASCII 编码为二进制 0100 0001,十进制 65,内存中存储的就是二进制码。
ASCII 编码用八个二进制位表示字符,这种编码方式只能表示英文字符,对于汉语等语言就明显不够用了,于是一种叫做 UTF-8 的编码方式就应运而生了,它实现了编码方式的大一统,从而得到广泛运用。
UTF-8 编码,当字符为 ASCII 码时占用一个字节,其它字符则占用 2-4 个字符。
详细了解编码可参看这里:字符编码笔记:ASCII,Unicode 和 UTF-8。
2. 字符串
字符串是 UTF-8 字符的一个序列,如:
func main() {
var name string = "xia帅帅"
lens := len(name)
n := []byte(name)
fmt.Printf("len name: %d, %v\n", lens, n)
}
# result
len name: 9, [120 105 97 229 184 133 229 184 133]
字符串的长度是 9,其中,'帅' 字被编码为 3 个字节的十进制数 229,184,133。
使用 unsafe 查看字符串的字节数:
bytes := unsafe.Sizeof(name)
fmt.Printf("size name: %d\n", bytes)
# result
size name: 16
奇怪这里为什么是 16 呢,查看 string 的定义发现,string 也是一种类型,一种结构体类型,它的结构表示为:
type StringHeader struct {
Data uintptr
Len int
}
可以看到,字符串结构由两部分组成:一,字符串指向的底层字节数组;二,字符串的字节长度。查看底层字节数组的所占字节:
bytes := unsafe.Sizeof(name)
fmt.Printf("string len: %d, size len: %d\n", bytes, (*reflect.StringHeader)(unsafe.Pointer(&name)).Len)
# result:
string len: 16, size len: 9 // 底层数组所占字节大小是 9
3. 字符串拼接
字符串拼接有多种实现,这里分别介绍了每种实现的性能比较,详细了解可看这里:字符串拼接性能及原理。
3.1 '+' 拼接字符串
func plusContact(n int, s string) string {
var joinString string
for i := 0; i < n; i++ {
joinString += s
}
return joinString
}
func BenchmarkPlusContact(b *testing.B) {
for i := 0; i < b.N; i++ {
plusContact(1000, testString)
}
}
3.2 Sprintf 拼接字符串
func sprintContact(n int, s string) string {
var joinString string
for i := 0; i < n; i++ {
joinString = fmt.Sprintf("%s%s", joinString, s)
}
return joinString
}
func BenchmarkSprintContact(b *testing.B) {
for i := 0; i < b.N; i++ {
sprintContact(1000, testString)
}
}
3.3 []byte 字节切片拼接字符串
func byteContact(n int, s string) string {
b := make([]byte, 0)
for i := 0; i < n; i++ {
b = append(b, s...) // 变长参数分配
}
return string(b)
}
func BenchmarkByteContact(b *testing.B) {
for i := 0; i < b.N; i++ {
byteContact(1000, testString)
}
}
3.4 bytes.Buffer 拼接字符串
func bufferContact(n int, s string) string {
b := new(bytes.Buffer)
for i := 0; i < n; i++ {
b.WriteString(s)
}
return b.String()
}
func BenchmarkBufferContact(b *testing.B) {
for i := 0; i < b.N; i++ {
bufferContact(1000, testString)
}
}
3.5 strings.Builder 拼接字符串
func builderContact(n int, s string) string {
var sb strings.Builder
for i := 0; i < n; i++ {
sb.WriteString(s)
}
return sb.String()
}
func BenchmarkBuilderContact(b *testing.B) {
for i := 0; i < b.N; i++ {
builderContact(1000, testString)
}
}
3.6 预分配切片容量
在知道拼接的容量情况下也可以对切片容量进行预分配:
## string.Builder 预分配切片容量
func preBuilderContact(n int, s string) string {
var sb = new(strings.Builder)
sb.Grow(n * len(testString))
for i := 0; i < n; i++ {
sb.WriteString(s)
}
return sb.String()
}
# []byte 预分配切片容量
func preByteContact(n int, s string) string {
b := make([]byte, 0, n*len(letterBytes))
for i := 0; i < n; i++ {
b = append(b, s...)
}
return string(b)
}
使用 Benchmark 基准测试,测试各组实现方式的性能:
[root@chunqiu stringcontact]$ go test -run="none" -v -bench . -benchmem -cpu=2,4
goos: linux
goarch: amd64
pkg: stringcontact
BenchmarkPlusContact-2 247 4765499 ns/op 34526785 B/op 999 allocs/op
BenchmarkPlusContact-4 250 4801224 ns/op 34526794 B/op 999 allocs/op
BenchmarkSprintContact-2 198 6050137 ns/op 34660487 B/op 3021 allocs/op
BenchmarkSprintContact-4 195 6159586 ns/op 34780286 B/op 3027 allocs/op
BenchmarkByteContact-2 20110 59817 ns/op 350144 B/op 21 allocs/op
BenchmarkByteContact-4 19981 60303 ns/op 350144 B/op 21 allocs/op
BenchmarkBufferContact-2 28652 41743 ns/op 196288 B/op 11 allocs/op
BenchmarkBufferContact-4 28664 42712 ns/op 196288 B/op 11 allocs/op
BenchmarkPreByteContact-2 41622 29007 ns/op 188416 B/op 3 allocs/op
BenchmarkPreByteContact-4 39409 31205 ns/op 188416 B/op 3 allocs/op
BenchmarkBuilderContact-2 23012 53037 ns/op 284608 B/op 20 allocs/op
BenchmarkBuilderContact-4 22238 53029 ns/op 284608 B/op 20 allocs/op
BenchmarkPreBuilderContact-2 78255 15835 ns/op 65536 B/op 1 allocs/op
BenchmarkPreBuilderContact-4 76986 16251 ns/op 65536 B/op 1 allocs/op
PASS
ok stringcontact 23.194s
关于性能分析和原理,可参看这篇 文章,这里不再赘述。唯一的疑惑是相比于 Buffer,Builder 的性能要低,而不是如文章所言略快 10%,存疑。
芝兰生于空谷,不以无人而不芳。