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

本文涉及的产品
容器镜像服务 ACR,镜像仓库100个 不限时长
应用实时监控服务-可观测链路OpenTelemetry版,每月50GB免费额度
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
简介: 【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的不一致基本不会造成影响.

相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
MySQL数据库入门学习
本课程通过最流行的开源数据库MySQL带你了解数据库的世界。   相关的阿里云产品:云数据库RDS MySQL 版 阿里云关系型数据库RDS(Relational Database Service)是一种稳定可靠、可弹性伸缩的在线数据库服务,提供容灾、备份、恢复、迁移等方面的全套解决方案,彻底解决数据库运维的烦恼。 了解产品详情: https://www.aliyun.com/product/rds/mysql 
目录
相关文章
|
2月前
|
人工智能 自然语言处理 算法
Go语言统计字符串中每个字符出现的次数 — 简易频率分析器
本案例实现一个字符统计程序,支持中文、英文及数字,可统计用户输入文本中各字符的出现次数,并以整洁格式输出。内容涵盖应用场景、知识点讲解、代码实现与拓展练习,适合学习文本分析及Go语言基础编程。
|
7月前
|
编译器 Go
揭秘 Go 语言中空结构体的强大用法
Go 语言中的空结构体 `struct{}` 不包含任何字段,不占用内存空间。它在实际编程中有多种典型用法:1) 结合 map 实现集合(set)类型;2) 与 channel 搭配用于信号通知;3) 申请超大容量的 Slice 和 Array 以节省内存;4) 作为接口实现时明确表示不关注值。此外,需要注意的是,空结构体作为字段时可能会因内存对齐原因占用额外空间。建议将空结构体放在外层结构体的第一个字段以优化内存使用。
|
7月前
|
运维 监控 算法
监控局域网其他电脑:Go 语言迪杰斯特拉算法的高效应用
在信息化时代,监控局域网成为网络管理与安全防护的关键需求。本文探讨了迪杰斯特拉(Dijkstra)算法在监控局域网中的应用,通过计算最短路径优化数据传输和故障检测。文中提供了使用Go语言实现的代码例程,展示了如何高效地进行网络监控,确保局域网的稳定运行和数据安全。迪杰斯特拉算法能减少传输延迟和带宽消耗,及时发现并处理网络故障,适用于复杂网络环境下的管理和维护。
|
1月前
|
数据采集 Go API
Go语言实战案例:多协程并发下载网页内容
本文是《Go语言100个实战案例 · 网络与并发篇》第6篇,讲解如何使用 Goroutine 和 Channel 实现多协程并发抓取网页内容,提升网络请求效率。通过实战掌握高并发编程技巧,构建爬虫、内容聚合器等工具,涵盖 WaitGroup、超时控制、错误处理等核心知识点。
|
1月前
|
数据采集 JSON Go
Go语言实战案例:实现HTTP客户端请求并解析响应
本文是 Go 网络与并发实战系列的第 2 篇,详细介绍如何使用 Go 构建 HTTP 客户端,涵盖请求发送、响应解析、错误处理、Header 与 Body 提取等流程,并通过实战代码演示如何并发请求多个 URL,适合希望掌握 Go 网络编程基础的开发者。
|
2月前
|
JSON 前端开发 Go
Go语言实战:创建一个简单的 HTTP 服务器
本篇是《Go语言101实战》系列之一,讲解如何使用Go构建基础HTTP服务器。涵盖Go语言并发优势、HTTP服务搭建、路由处理、日志记录及测试方法,助你掌握高性能Web服务开发核心技能。
|
2月前
|
Go
如何在Go语言的HTTP请求中设置使用代理服务器
当使用特定的代理时,在某些情况下可能需要认证信息,认证信息可以在代理URL中提供,格式通常是:
207 0
|
3月前
|
JSON 编解码 API
Go语言网络编程:使用 net/http 构建 RESTful API
本章介绍如何使用 Go 语言的 `net/http` 标准库构建 RESTful API。内容涵盖 RESTful API 的基本概念及规范,包括 GET、POST、PUT 和 DELETE 方法的实现。通过定义用户数据结构和模拟数据库,逐步实现获取用户列表、创建用户、更新用户、删除用户的 HTTP 路由处理函数。同时提供辅助函数用于路径参数解析,并展示如何设置路由器启动服务。最后通过 curl 或 Postman 测试接口功能。章节总结了路由分发、JSON 编解码、方法区分、并发安全管理和路径参数解析等关键点,为更复杂需求推荐第三方框架如 Gin、Echo 和 Chi。
|
4月前
|
分布式计算 Go C++
初探Go语言RPC编程手法
总的来说,Go语言的RPC编程是一种强大的工具,让分布式计算变得简单如同本地计算。如果你还没有试过,不妨挑战一下这个新的编程领域,你可能会发现新的世界。
106 10
|
7月前
|
存储 缓存 监控
企业监控软件中 Go 语言哈希表算法的应用研究与分析
在数字化时代,企业监控软件对企业的稳定运营至关重要。哈希表(散列表)作为高效的数据结构,广泛应用于企业监控中,如设备状态管理、数据分类和缓存机制。Go 语言中的 map 实现了哈希表,能快速处理海量监控数据,确保实时准确反映设备状态,提升系统性能,助力企业实现智能化管理。
109 3

热门文章

最新文章