zoukankan      html  css  js  c++  java
  • 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%,存疑。


    芝兰生于空谷,不以无人而不芳。
  • 相关阅读:
    windows命令行下导入excel数据到SQLite数据库
    Android Studio如何提示函数用法
    在不root手机的情况上读取Data目录上的文件
    OSI七层模型
    设计模式之代理模式
    Android中Javascript中的调用
    cf #205 B Codeforces Round #205 (Div. 2) B. Two Heaps
    uva 10600 次小生成树
    防2B && 图论模板 && 7788
    最大匹配 && 最佳完美匹配 模板
  • 原文地址:https://www.cnblogs.com/xingzheanan/p/15581199.html
Copyright © 2011-2022 走看看