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%,存疑。


posted @ 2021-11-20 14:30  hxia043  阅读(570)  评论(0编辑  收藏  举报