Go标准编译器对字符串和字节切片之间的转换操作所做的一些优化

以下文章来源于Go 101 ,作者老貘

我们知道,在Go中,一个字符串其实是一个只读字节序列。在正常情况下,

  • 从一个字符串转换为字节切片的过程中将把字符串中的字节序列复制一份存储在结果字节切片中,以防止因为修改结果字节切片的元素而导致字符串被修改。
  • 从一个字节切片转换为字符串的过程中也将把字节切片的底层元素序列复制一份存储在结果字符串中,以防止因为修改此字节切片的元素而导致结果字符串被修改。

Go标准编译器在某些情形下,对这两种转换进行了优化,从而避免了复制字符串或者字节切片的字节序列。下面将列出这些情形。

  1. 一个for-range循环中跟随range关键字的从字符串到字节切片的转换不会复制字符串中的字节序列。比如下面这个程序中,函数g比函数f运行效率高,因为函数g避免了复制一次字符串中的字节序列。

package main

import "fmt"
import "testing"

var s = "012345678901234567890123456789"
var S = s + s + s + s + s
var nf, ng int

func f() {
bs := []byte(S) // 复制底层字节序列
for _, b := range bs {
nf += int(b)
}
}

func g() {
for _, b := range []byte(S) { // 不复制底层字节序列
ng += int(b)
}
}

func main() {
// 1
fmt.Println(testing.AllocsPerRun(1, f))
// 0
fmt.Println(testing.AllocsPerRun(1, g))
}

2. 当一个从字节切片到字符串的转换被用做映射元素索引语法中的键值时,此转换不会复制字节切片的底层元素序列。比如下面这个程序中,函数g比函数f运行效率高。
```go

package main 

import "fmt" 
import "testing" 

var k1 = []byte("012345678901234567890123456789") 
var k2 = append(k1, k1...) 
var k4 = append(k2, k2...) 
var K = append(k4, k4...) 
var m = map[string]int {} 

func f() { 
  key := string(K) // 复制底层字节序列 
  _ = m[key] 
} 

func g() { 
  _ = m[string(K)] // 不复制底层字节序列 
} 

func main() { 
  // 1 
  fmt.Println(testing.AllocsPerRun(1, f)) 
  // 0 
  fmt.Println(testing.AllocsPerRun(1, g)) 
} </code></pre>
<ol start="3">
<li>当一个从字节切片到字符串的转换被用做一个字符串比较表达式中的一个比较值时,此转换不会复制字节切片的底层元素序列。比如下面这个函数中的两个转换都不会复制字节切片的底层元素序列。
<pre><code class="language-go"></code></pre></li>
</ol>
<p>func f(x, y []byte) bool {
return string(x) == string(y)
} </p>
<pre><code>4. 当一个字符串衔接表达式中至少有一个被衔接的字符串值为非空字符串常量时,此字符串衔接表达式中的从字节切片到字符串的转换不会单独开辟内存复制字节切片的底层元素序列,事实上整个字符串衔接表达式只会开辟一次内存。比如,在下面这个程序中,函数g比函数f运行效率高,因为函数g比函数f少复制了两次字节切片的底层元素序列,虽然函数g浪费了一个字节的内存。
```go

package main 

import "fmt" 
import "testing" 

var x = []byte{1024: 'x'} 
var y = []byte{1024: 'y'} 
var sf, sg string 

func f() { 
  // 每个转换都要开辟一次内存 
  sg = string(x) + string(y) 
} 

func g() { 
  // 两个转换都不会单独开辟内存 
  sf =(" " + string(x) + string(y)) 
  sf = sf[1:] 
} 

func main() { 
  // 3 
  fmt.Println(testing.AllocsPerRun(1, f)) 
  // 1 
  fmt.Println(testing.AllocsPerRun(1, g)) 
} 

此第4条优化更像是标准编译器优化不到位。在以后的版本中,函数g比函数f的性能差异可能会消除。


关于更多Go语言编程中的事实、细节和技巧,请访问《Go语言101》官方网站:https://gfw.go101.org (可点击下面的原文链接直接访问)。

1