1 再谈类型和新版转换效率
在go中byte是
uint8别名,
rune是
int32`别名,用于区分字节和字符值。转换操作涉及到内存拷贝,可能影响性能。
旧版转换方法通过unsafe
包实现,而Go 1.20引入的新版转换函数unsafe.SliceData
和unsafe.StringData
在某些场景下提高了转换效率。
2 类型定义
三者都是Go中的内置类型,在 builtin 包中有类型定义
byte是uint8的一个别名,在所有方面都等同于uint8。按照惯例,它被用来区分字节值和8位无符号整数值。
type byte = uint8
rune是int32的一个别名,在所有方面都等同于int32。它在习惯上用于区分字符值和整数值。
type rune = int32
字符串是所有8位字节的字符串的集合,习惯上但不必须代表UTF-8编码的文本。
一个字符串可以是空的,但不能为零。
字符串类型的值是不可改变的。
type string string
从官方概念来看,string表示的是byte的集合,即八位的一个字节的集合,通常情况下使用UTF-8的编码方式,但不绝对。
而rune表示用四个字节组成的一个字符,rune值为字符的Unicode编码。
3 类型转换性能操作再次实践
- 1、类型转换性能优化
Go底层对[]byte和string的转化都需要进行内存拷贝,因而在部分需要频繁转换的场景下,大量的内存拷贝会导致性能下降。
type stringStruct struct {
str unsafe.Pointer
len int
}
type slice struct {
array unsafe.Pointer
len int
cap int
}
本质上底层数据存储都是基于uintptr,可见string与[]byte的区别在于[]byte额外有一个cap去指定slice的容量。
所以string可以看作[2]uintptr,[]byte看作[3]uintptr,类型转换只需要转换成对应的uintptr数组即可,不需要进行底层数据的频繁拷贝。
以下是基于此思想提供的一个解决方案,用于string与[]byte的高性能转换方案 (fasthttp)。
b2s将字节片转换为字符串,无需分配内存。
请注意,如果字符串和/或片头发生变化,在未来的Go版本中,它可能会中断。
https://groups.google.com/forum/#!msg/Golang-Nuts/ENgbUzYvCuU/90yGx7GUAgAJ .
旧版
func byte2str(b []byte) string {
/* #nosec G103 */
return *(*string)(unsafe.Pointer(&b))
}
1.20 新版
func byte2str(b []byte) string {
return unsafe.String(unsafe.SliceData(b), len(b))
}
s2b将字符串转换为字节片,无需分配内存。
注意,如果字符串和/或片头在未来的go版本中发生变化,在未来的go版本中可能会中断,它可能会失效。
旧版:
func str2byte(s string) (b []byte) {
/* #nosec G103 */
bh := (*reflect.SliceHeader)(unsafe.Pointer(&b))
/* #nosec G103 */
sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
bh.Data = sh.Data
bh.Cap = sh.Len
bh.Len = sh.Len
return b
}
1.20 新版:
func str2byte(s string) []byte {
return unsafe.Slice(unsafe.StringData(s), len(s))
}
SliceData 返回一个指向参数的底层数组的指针slice。
// - 如果cap(slice) > 0, SliceData返回&slice[:1][0]。
// - 如果slice == nil, SliceData返回nil。
// - 否则,SliceData返回一个非空的指针,指向一个 未指定的内存地址。
性能对比:
旧版字符串转字节切片
BenchmarkString2Bytes BenchmarkString2Bytes-2 1000000000 0.3060 ns/op BenchmarkString2Bytes-2 1000000000 0.3096 ns/op BenchmarkString2Bytes-4 1000000000 0.3011 ns/op BenchmarkString2Bytes-4 1000000000 0.3093 ns/op BenchmarkString2Bytes-8 1000000000 0.3106 ns/op BenchmarkString2Bytes-8 1000000000 0.3050 ns/op BenchmarkString2Bytes-32 1000000000 0.3191 ns/op BenchmarkString2Bytes-32 1000000000 0.3100 ns/op
新版1.20的字符串转字节切片
BenchmarkS2B BenchmarkS2B-2 1000000000 0.6182 ns/op BenchmarkS2B-2 1000000000 0.6181 ns/op BenchmarkS2B-4 1000000000 0.6372 ns/op BenchmarkS2B-4 1000000000 0.6450 ns/op BenchmarkS2B-8 1000000000 0.6360 ns/op BenchmarkS2B-8 1000000000 0.6449 ns/op BenchmarkS2B-32 1000000000 0.6071 ns/op BenchmarkS2B-32 1000000000 0.6171 ns/op
旧版字节切片转字符串
BenchmarkBytes2String BenchmarkBytes2String-2 1000000000 0.6216 ns/op BenchmarkBytes2String-2 1000000000 0.6353 ns/op BenchmarkBytes2String-4 1000000000 0.6252 ns/op BenchmarkBytes2String-4 1000000000 0.6620 ns/op BenchmarkBytes2String-8 1000000000 0.5936 ns/op BenchmarkBytes2String-8 1000000000 0.6246 ns/op BenchmarkBytes2String-32 1000000000 0.6032 ns/op BenchmarkBytes2String-32 1000000000 0.5866 ns/op
新版1.20字节切片转字符串
BenchmarkB2S BenchmarkB2S-2 1000000000 0.6531 ns/op BenchmarkB2S-2 1000000000 0.6351 ns/op BenchmarkB2S-4 1000000000 0.6146 ns/op BenchmarkB2S-4 1000000000 0.6276 ns/op BenchmarkB2S-8 1000000000 0.6346 ns/op BenchmarkB2S-8 1000000000 0.6551 ns/op BenchmarkB2S-32 1000000000 0.6266 ns/op BenchmarkB2S-32 1000000000 0.6116 ns/op
-
- 可以看到从字节切片[]byte转字符串string,新版和旧版性能相当。
-
- 从字符串string转字节切片[]byte,旧版性能比新版正好快两倍。
由于[]byte转换到string时直接抛弃cap即可,因而可以直接通过unsafe.Pointer进行操作。
string转换到[]byte时,需要进行指针的拷贝,并将Cap设置为Len。此处是该方案的一个细节点,因为string是定长的,转换后data后续的数据是否可写是不确定的。
如果Cap大于Len,在进行append的时候不会触发slice的扩容,而且由于后续内存不可写,就会在运行时导致panic。
3、UCA不一致
UCA定义在 unicode/tables.go 中,头部即定义了使用的UCA版本。
版本是Unicode的版本,表格是从该版本衍生出来的。
const Version = "13.0.0"
经过追溯,go 1 起的tables.go即使用了6.0.0的版本,位置与现在稍有不同。
4 小结
字符相关的其他内容。
-
- 对于ASCII(不包含扩展128+)字符,UTF-8编码、Unicode编码、ASCII码均相同(即单字节以0开头)
-
- 对于非ASCII(不包含扩展128+)字符,若字符有n个字节(编码后)。则首字节的开头为n个1和1个0,其余字节均以10开头。
除去这些开头固定位,其余位组合表示Unicode字符。
-
- UCA(Unicode Collation Algorithm)
UCA是Unicode字符的校对算法,目前最新版本15.0.0(2022-05-03 12:36)。
以14.0.0为准,数据文件主要包含两个部分, 即 allkeys 和 decomps,表示字符集的排序、大小写、分解关系等,详细信息可阅读Unicode官方文档。
不同版本之间的UCA是存在差异的,如两个字符,在14.0.0中定义了大小写关系,但在5.0.0中是不具备大小写关系的。
在仅支持5.0.0的应用中,14.0.0 增加的字符是可能以硬编码的方式存在的,具体情况要看实现细节。
因而对于跨平台,多语言的业务,各个服务使用的UCA很可能不是同一个版本。
因而对于部分字符,其排序规则、大小写转换的不同,有可能会产生不一致的问题。
比如根据MySQL官方文档关于UCA的相关内容
MySQL使用不同编码,UCA的版本并不相同,因而很大概率会存在底层数据库使用的UCA与业务层使用的UCA不一致的情况。
在一些大小写不敏感的场景下,可能会出现字符的识别问题。
如业务层认为两个字符为一对大小写字符,而由于MySQL使用的UCA版本较低,导致MySQL通过小写进行不敏感查询无法查询到大写的数据。
由于常用字符集基本不会发生变化,所以对于普通业务,UCA的不一致基本不会造成影响.