再谈go语言中字符转换效率问题

本文涉及的产品
Serverless 应用引擎 SAE,800核*时 1600GiB*时
性能测试 PTS,5000VUM额度
云原生网关 MSE Higress,422元/月
简介: 【5月更文挑战第20天】本文讨论了Go语言中类型转换的效率,特别是`byte`、`rune`和`string`之间的转换。`性能测试显示,从`[]byte`到`string`的转换,新版与旧版性能相当;但从`string`到`[]byte`,旧版快于新版两倍。此外,文章提到了Unicode校对算法(UCA)的版本差异可能带来的排序和大小写转换不一致问题,这在多语言环境中需要注意。

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

在go中byteuint8别名,runeint32`别名,用于区分字节和字符值。转换操作涉及到内存拷贝,可能影响性能。

question_ans.png

旧版转换方法通过unsafe包实现,而Go 1.20引入的新版转换函数unsafe.SliceDataunsafe.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
    
    1. 可以看到从字节切片[]byte转字符串string,新版和旧版性能相当。
    1. 从字符串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 小结

字符相关的其他内容。

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

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助     相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
目录
相关文章
|
5天前
|
JSON 测试技术 Go
零值在go语言和初始化数据
【7月更文挑战第10天】本文介绍在Go语言中如何初始化数据,未初始化的变量会有对应的零值:bool为`false`,int为`0`,byte和string为空,pointer、function、interface及channel为`nil`,slice和map也为`nil`。。本文档作为指南,帮助理解Go的数据结构和正确使用它们。
53 22
零值在go语言和初始化数据
|
5天前
|
JSON Java Go
Go 语言性能优化技巧
在Go语言中优化性能涉及数字字符串转换(如用`strconv.Itoa()`代替`fmt.Sprintf()`)、避免不必要的字符串到字节切片转换、预分配切片容量、使用`strings.Builder`拼接、有效利用并发(`goroutine`和`sync.WaitGroup`)、减少内存分配、对象重用(`sync.Pool`)、无锁编程、I/O缓冲、正则预编译和选择高效的序列化方法。这些策略能显著提升代码执行效率和系统资源利用率。
41 13
|
1天前
|
Cloud Native Java Go
为什么要学习Go语言?
GO logo的核心理念,即简单胜于复杂。使用现代斜体无衬线字体与三条简单的运动线相结合,形成一个类似于快速运动的两个轮子的标记,传达速度和效率。字母的圆形暗示了GO地鼠的眼睛,创造了一个熟悉的形状,让标记和吉祥物很好地搭配在一起。
12 4
|
5天前
|
设计模式 Go
Go语言设计模式:使用Option模式简化类的初始化
在Go语言中,面对构造函数参数过多导致的复杂性问题,可以采用Option模式。Option模式通过函数选项提供灵活的配置,增强了构造函数的可读性和可扩展性。以`Foo`为例,通过定义如`WithName`、`WithAge`、`WithDB`等设置器函数,调用者可以选择性地传递所需参数,避免了记忆参数顺序和类型。这种模式提升了代码的维护性和灵活性,特别是在处理多配置场景时。
41 8
|
5天前
|
存储 Go
go语言中fmt格式化包和内置函数汇总
【7月更文挑战第10天】本文介绍fmt包和`Errorf`用于创建格式化的错误消息。`fmt`包还涉及一些接口,如`Formatter`、`GoStringer`、`ScanState`、`Scanner`和`Stringer`,支持自定义格式化和输入/输出处理。
17 1
|
5天前
|
Go
go语言中格式化输出的占位符
【7月更文挑战第10天】`fmt` 包在 Go 语言中用于格式化输出,包括不同类型的占位符:%v(默认格式)、%+v(带字段名的结构体)、%#v(Go语法表示)、%T(类型表示)、%%(百分号)。布尔值用%t,整数有%b、%c、%d、%o、%q、%x、%X和%U。浮点数和复数用%b、%e、%E、%f、%g、%G。字符串和字节切片用%s、%q、%x、%X。指针用%p。占位符可配合+、-、#、空格和0进行调整。宽度和精度控制输出格式,例如 %.4g 控制小数精度。Go 没有 `%u`,但无符号整数默认打印为正数。运算符包括逻辑、比较、加减、乘除、移位、按位和按位异或等。
17 1
|
7天前
|
存储 Go 索引
在go语言中自定义泛型的变长参数
【7月更文挑战第8天】在Go语言中,由于官方1.18以前的版本不支持泛型,可以通过空接口和反射模拟泛型。泛型适用于通用数据结构和函数,虽牺牲了一些性能,但提高了代码复用和类型安全性。
41 1
|
3天前
|
安全 Go
Go语言map并发安全,互斥锁和读写锁谁更优?
Go并发编程中,`sync.Mutex`提供独占访问,适合读写操作均衡或写操作频繁的场景;`sync.RWMutex`允许多个读取者并行,适用于读多写少的情况。明智选择锁可提升程序性能和稳定性。示例展示了如何在操作map时使用这两种锁。
6 0
|
3天前
|
安全 Go 开发者
Go语言map并发安全使用的正确姿势
在Go并发编程中,由于普通map不是线程安全的,多goroutine访问可能导致数据竞态。为保证安全,可使用`sync.Mutex`封装map或使用从Go 1.9开始提供的`sync.Map`。前者通过加锁手动同步,后者内置并发控制,适用于多goroutine共享。选择哪种取决于具体场景和性能需求。
6 0
|
3天前
|
存储 安全 Java
Go语言中的map为什么默认不是并发安全的?
Go语言的map默认不保证并发安全,以优化性能和简洁性。官方建议在需要时使用`sync.Mutex`保证安全。从Go 1.6起,并发读写map会导致程序崩溃,鼓励开发者显式处理并发问题。这样做的哲学是让代码更清晰,并避免不必要的性能开销。
4 0

热门文章

最新文章

  • 1
    在会议系统工程中,Python可以用于多种任务,如网络请求(用于视频会议的连接和会议数据的传输)、数据分析(用于分析会议参与者的行为或会议效果)等。
    9
  • 2
    在可视会议系统工程中,系统工程方法可以帮助我们系统地规划、设计和实现一个高效、可靠的可视会议系统。
    10
  • 3
    我们可以从系统工程的角度来讨论如何优化组织架构,并给出一些可能涉及的Python应用领域的示例。
    7
  • 4
    在环境治理领域,污染治理系统工程旨在通过系统的方法来解决环境污染问题。这通常包括污染源的识别、污染物的监测、治理技术的选择、治理效果的评估等多个环节。
    13
  • 5
    我将提供一个简化的Python代码示例和详解,以展示如何使用Python和Django框架来构建智能化小区综合物业管理系统的一部分功能。
    8
  • 6
    在系统工程中,软件测试是一个至关重要的环节,它确保软件的质量、可靠性和性能。软件测试通常包括多个阶段,如单元测试、集成测试、系统测试和验收测试等。
    14
  • 7
    在软件部署阶段,系统工程的目标是确保软件能够顺利、稳定地部署到目标环境中,并满足用户的需求。
    11
  • 8
    航空航天领域,系统工程被用于设计复杂的飞行器和系统。这包括飞行器的结构、推进系统、控制系统等。
    12
  • 9
    在通讯系统工程中,这通常包括硬件、软件、网络协议、数据传输等多个方面的设计和实现。
    9
  • 10
    以下是一个简化的环境监测系统工程概述,并附带有Python代码示例或详解。
    15