在go中讨论处理字符集和编码效率

举报
码乐 发表于 2024/03/20 07:14:19 2024/03/20
【摘要】 go1.20 的unsafe包添加了功能SliceData、String和StringData 。它们完成了独立于实现的切片和字符串操作的函数集。Go 的类型转换规则已扩展为允许 从 slice 直接转换为 array。语言规范现在定义了比较数组元素和结构字段的确切顺序。这阐明了在比较过程中出现恐慌时会发生什么。func SliceData(slice []ArbitraryType) *A...

go1.20 的unsafe包添加了功能SliceData、String和StringData 。

它们完成了独立于实现的切片和字符串操作的函数集。

Go 的类型转换规则已扩展为允许 从 slice 直接转换为 array。

语言规范现在定义了比较数组元素和结构字段的确切顺序。
这阐明了在比较过程中出现恐慌时会发生什么。

func SliceData(slice []ArbitraryType) *ArbitraryType

具体如何实现的,无法得知。 我们可以从使用者角度看它的性能与之前的做法有何变化。

1 utf8内容

这里说的所谓符文,也就是 rune,在前文有提到过。

RuneError==unicode.ReplacementChar和 MaxRune==unicode.MaxRune在测试中得到了验证。
在本地定义它们可以避免这个包依赖于unicode包。

1.1 编码基础。

"错误 "符文或 “Unicode替换字符”

		RuneError = '\uFFFD'      

RuneSelf以下的字符在一个字节中表示为它们自己。

	RuneSelf  = 0x80         

最大的有效Unicode码位。

	MaxRune   = '\U0010FFFF'

一个UTF-8编码的Unicode字符的最大字节数。

	UTFMax    = 4            

以下代号范围内的代码点对UTF-8是无效的。

	surrogateMin = 0xD800
	surrogateMax = 0xDFFF

默认的最低和最高延续字节。

	locb = 0b10000000
	hicb = 0b10111111

这些常量的名称是为了在下面的表格中提供良好的排列,下面的表格中。
第一个小数点是acceptRanges的索引,或F用于特殊的单字节情况。

特殊的一个字节的情况。第二个小数点是符文长度或特殊的单字节情况下的状态。

acceptRange给出了UTF-8中第二个字节的有效值范围,序列中第二个字节的有效值范围。

	var acceptRanges = [16]acceptRange{
		0: {locb, hicb},
		1: {0xA0, hicb},
		2: {locb, 0x9F},
		3: {0x90, hicb},
		4: {locb, 0x8F},
	}

1.2 FullRune返回参数p中的字节是否以一个完整的UTF-8编码的符文开始。

一个无效的编码被认为是一个完整的符文,因为它将转换为一个宽度为1的错误符文 rune。

func FullRune(p []byte) bool {...}

实例:

	buf := []byte{228, 184, 150} // 世
	fmt.Println(utf8.FullRune(buf))
	fmt.Println(utf8.FullRune(buf[:2]))
	// Output:
	// true
	// false

1.3 FullRuneInString与FullRune类似,但其输入是一个字符串。

func FullRuneInString(s string) bool {...}

实例:

	str := "世"
	fmt.Println(utf8.FullRuneInString(str))
	fmt.Println(utf8.FullRuneInString(str[:2]))
	// Output:
	// true
	// false

1.4 DecodeRune 解压编码

解压参数p中的第一个UTF-8编码,并返回符文和它的宽度(字节)。

如果p是空的,则返回(RuneError, 0)。否则,如果编码是无效的,则返回(RuneError, 1)。
这两种情况都是不可能的正确的、非空的UTF-8的结果。

如果一个编码是不正确的UTF-8,或者编码的符文是编码但是超出了范围,或者不是最短的UTF-8编码。
那么编码是无效的。不进行其他验证。

下面的代码模拟了对x == xx的额外检查,并相应地处理ASCII和无效的情况。
这种掩码和或其他的方法可以防止出现额外的分支。

func DecodeRune(p []byte) (r rune, size int) {...}

实例:

	b := []byte("Hello, 世界")
	for len(b) > 0 {
		r, size := utf8.DecodeRune(b)
		fmt.Printf("%c %v\n", r, size)
		b = b[size:]
	}
	// Output:
	// H 1
	// e 1
	// l 1
	// l 1
	// o 1
	// , 1
	//   1
	// 世 3
	// 界 3

1.5 DecodeRuneInString与DecodeRune类似,但其输入是一个字符串。

如果s是是空的,它会返回(RuneError, 0)。否则,如果编码是无效的,它会返回(RuneError, 1)。

对于正确的、非空的UTF-8来说,这两种结果都是不可能的。

如果一个编码是不正确的UTF-8,编码的符文是编码但是超出了范围,或者不是最短的UTF-8编码。
那么传入的编码是无效的。不进行其他验证。

func DecodeRuneInString(s string) (r rune, size int) {...}

	str := "Hello, 世界"
	for len(str) > 0 {
		r, size := utf8.DecodeRuneInString(str)
		fmt.Printf("%c %v\n", r, size)
		str = str[size:]
	}
	// Output:
	// H 1
	// e 1
	// l 1
	// l 1
	// o 1
	// , 1
	//   1
	// 世 3
	// 界 3

1.6 DecodeLastRune解压参数p中的最后一个UTF-8编码并返回符文和其宽度(字节)。

如果参数p是空的,则返回(RuneError, 0)。

否则,如果参数p的编码是无效的,则返回(RuneError, 1)。

这两种情况都是不可能的,正确的、非空的UTF-8的结果。

如果一个编码是不正确的UTF-8,编码的符文是编码,但是超出了范围,或者不是最短的UTF-8编码。
那么编码是无效的。不进行其他验证。

func DecodeLastRune(p []byte) (r rune, size int) {...}

使用实例:

	b := []byte("Hello, 世界")

	for len(b) > 0 {
		r, size := utf8.DecodeLastRune(b)
		fmt.Printf("%c %v\n", r, size)

		b = b[:len(b)-size]
	}
	// Output:
	// 界 3
	// 世 3
	//   1
	// , 1
	// o 1
	// l 1
	// l 1
	// e 1
	// H 1

1.7 DecodeLastRuneInString与DecodeLastRune类似,但其输入是一个字符串。

如果s是空的,则返回(RuneError, 0)。
否则,如果编码是无效的,它返回(RuneError, 1)。

对于正确的非空的UTF-8来说,这两种结果都是不可能的。

如果一个编码是不正确的UTF-8,编码的符文是编码但是超出了范围,或者不是最短的UTF-8编码。
那么编码是无效的。不进行其他验证。

func DecodeLastRuneInString(s string) (r rune, size int) {...}

实例:

	str := "Hello, 世界"
	for len(str) > 0 {
		r, size := utf8.DecodeLastRuneInString(str)
		fmt.Printf("%c %v\n", r, size)
		str = str[:len(str)-size]
	}
	// Output:
	// 界 3
	// 世 3
	//   1
	// , 1
	// o 1
	// l 1
	// l 1
	// e 1
	// H 1

1.8 RuneLen返回对符文进行编码所需的字节数。

如果符文不是一个可以用UTF-8编码的有效值,则返回-1。

func RuneLen(r rune) int {...}

实例:

	fmt.Println(utf8.RuneLen('a'))
	fmt.Println(utf8.RuneLen('界'))
	// Output:
	// 1
	// 3

1.9 EncodeRune将符文的UTF-8编码写进参数p(必须足够大

如果符文超出了范围,它就写出RuneError的编码,它返回写入的字节数。

func EncodeRune(p []byte, r rune) int {...}

实例: 正常使用

	r := '世'
	buf := make([]byte, 3)

	n := utf8.EncodeRune(buf, r)

	fmt.Println(buf)
	fmt.Println(n)
	// Output:
	// [228 184 150]
	// 3

实例:索引越界

	runes := []rune{
		// Less than 0, out of range.
		-1,
		// Greater than 0x10FFFF, out of range.
		0x110000,
		// The Unicode replacement character.
		utf8.RuneError,
	}
	for i, c := range runes {
		buf := make([]byte, 3)
		size := utf8.EncodeRune(buf, c)
		fmt.Printf("%d: %d %[2]s %d\n", i, buf, size)
	}
	// Output:
	// 0: [239 191 189] � 3
	// 1: [239 191 189] � 3
	// 2: [239 191 189] � 3

1.10 AppendRune将r的UTF-8编码附加到p的末尾,并且返回扩展的缓冲区。

如果符文超出了范围。

它将附加RuneError的编码。

func AppendRune(p []byte, r rune) []byte {...}

实例:

	buf1 := utf8.AppendRune(nil, 0x10000)
	buf2 := utf8.AppendRune([]byte("init"), 0x10000)
	fmt.Println(string(buf1))
	fmt.Println(string(buf2))
	// Output:
	// 𐀀
	// init𐀀

1.11 RuneCount返回p中符文的数量。

错误的和短小的编码被视为宽度为1字节的单一符文。

func RuneCount(p []byte) int {...}

实例:
	buf := []byte("Hello, 世界")
	fmt.Println("bytes =", len(buf))
	fmt.Println("runes =", utf8.RuneCount(buf))
	// Output:
	// bytes = 13
	// runes = 9

1.12 RuneCountInString与RuneCount类似

其输入是一个字符串, 返回符文数量。

func RuneCountInString(s string) (n int) {...}

实例:

	str := "Hello, 世界"
	fmt.Println("bytes =", len(str))
	fmt.Println("runes =", utf8.RuneCountInString(str))
	// Output:
	// bytes = 13
	// runes = 9

1.13 RuneStart 报告该字节是否为编码后的第一个字节,可能是无效的符文。

第二个和随后的字节总是将前两个位设置为10。

func RuneStart(b byte) bool { return b&0xC0 != 0x80 }

实例:

	buf := []byte("a界")
	fmt.Println(utf8.RuneStart(buf[0]))
	fmt.Println(utf8.RuneStart(buf[1]))
	fmt.Println(utf8.RuneStart(buf[2]))
	// Output:
	// true
	// true
	// false

1.14 Valid 报告参数p是否完全由有效的UTF-8编码的符文组成。

这种优化避免了在生成p[8:]的代码时重新计算容量的需要。

在生成p[8:]的代码时需要重新计算容量,使其与ValidString相媲美。

ValidString,后者在长ASCII字符串上快20%。

将两个32位的负载结合起来,可以使相同的代码用于32位和64位平台。

编译器可以为first32和second32生成一个32位的负载,在许多平台上。参见test/codegen/memcombine.go。

func Valid(p []byte) bool {...}

实例:

	valid := []byte("Hello, 世界")
	invalid := []byte{0xff, 0xfe, 0xfd}

	fmt.Println(utf8.Valid(valid))
	fmt.Println(utf8.Valid(invalid))
	// Output:
	// true
	// false

1.15 ValidString报告s是否完全由有效的UTF-8编码的符文组成。

每次迭代检查并跳过8个字节的ASCII字符。

将两个32位的负载结合起来,可以使相同的代码用于用于32位和64位平台。

编译器可以为first32和second32生成一个32位的负载。

在许多平台上。参见test/codegen/memcombine.go。

func ValidString(s string) bool {...}

实例:

	valid := "Hello, 世界"
	invalid := string([]byte{0xff, 0xfe, 0xfd})

	fmt.Println(utf8.ValidString(valid))
	fmt.Println(utf8.ValidString(invalid))
	// Output:
	// true
	// false

1.16 ValidRune报告r是否可以被合法地编码为UTF-8。

超出范围的代码点或代用半数是非法的。

func ValidRune(r rune) bool {...}

实例:

	valid := 'a'
	invalid := rune(0xfffffff)

	fmt.Println(utf8.ValidRune(valid))
	fmt.Println(utf8.ValidRune(invalid))
	// Output:
	// true
	// false

2 再谈类型和新版转换效率

2.1、类型定义

三者都是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编码。

2.2、类型转换性能操作再次实践

  • 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
  1. 可以看到从字节切片[]byte转字符串string,新版和旧版性能相当。

  2. 从字符串string转字节切片[]byte,旧版性能比新版正好快两倍。

由于[]byte转换到string时直接抛弃cap即可,因而可以直接通过unsafe.Pointer进行操作。

string转换到[]byte时,需要进行指针的拷贝,并将Cap设置为Len。此处是该方案的一个细节点,因为string是定长的,转换后data后续的数据是否可写是不确定的。

如果Cap大于Len,在进行append的时候不会触发slice的扩容,而且由于后续内存不可写,就会在运行时导致panic。

  • 2、UCA不一致

UCA定义在 unicode/tables.go 中,头部即定义了使用的UCA版本。

版本是Unicode的版本,表格是从该版本衍生出来的。

const Version = "13.0.0"

经过追溯,go 1 起的tables.go即使用了6.0.0的版本,位置与现在稍有不同。

小结

字符相关的其他内容。

    1. 对于ASCII(不包含扩展128+)字符,UTF-8编码、Unicode编码、ASCII码均相同(即单字节以0开头)
    1. 对于非ASCII(不包含扩展128+)字符,若字符有n个字节(编码后)。则首字节的开头为n个1和1个0,其余字节均以10开头。

除去这些开头固定位,其余位组合表示Unicode字符。

    1. 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的不一致基本不会造成影响.

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。