看代码突然想到一个问题:字符串在内存中是怎么表示的?花了大半天才理清,这里记录梳理下。

1. 字符

提到字符串需要先了解字符,没有字符哪能串起来呢。不像 int,float 这种直接在内存中以位数表示的类型,字符需要经过编码才能存在内存中。如字符 'A' 的 ASCII 编码为二进制 0100 0001,十进制 65,内存中存储的就是二进制码。

ASCII 编码用八个二进制位表示字符,这种编码方式只能表示英文字符,对于汉语等语言就明显不够用了,于是一种叫做 UTF-8 的编码方式就应运而生了,它实现了编码方式的大一统,从而得到广泛运用。

UTF-8 编码,当字符为 ASCII 码时占用一个字节,其它字符则占用 2-4 个字符。

详细了解编码可参看这里:字符编码笔记:ASCII,Unicode 和 UTF-8

2. 字符串

字符串是 UTF-8 字符的一个序列,如:

func main() {
var name string = "xia帅帅" lens := len(name)
n := []byte(name) fmt.Printf("len name: %d, %v\n", lens, n) } # result
len name: 9, [120 105 97 229 184 133 229 184 133]

字符串的长度是 9,其中,'帅' 字被编码为 3 个字节的十进制数 229,184,133。

使用 unsafe 查看字符串的字节数:

bytes := unsafe.Sizeof(name)
fmt.Printf("size name: %d\n", bytes) # result
size name: 16

奇怪这里为什么是 16 呢,查看 string 的定义发现,string 也是一种类型,一种结构体类型,它的结构表示为:

type StringHeader struct {
Data uintptr
Len int
}

可以看到,字符串结构由两部分组成:一,字符串指向的底层字节数组;二,字符串的字节长度。查看底层字节数组的所占字节:

bytes := unsafe.Sizeof(name)
fmt.Printf("string len: %d, size len: %d\n", bytes, (*reflect.StringHeader)(unsafe.Pointer(&name)).Len) # result:
string len: 16, size len: 9 // 底层数组所占字节大小是 9

3. 字符串拼接

字符串拼接有多种实现,这里分别介绍了每种实现的性能比较,详细了解可看这里:字符串拼接性能及原理

3.1 '+' 拼接字符串

func plusContact(n int, s string) string {
var joinString string
for i := 0; i < n; i++ {
joinString += s
}
return joinString
} func BenchmarkPlusContact(b *testing.B) {
for i := 0; i < b.N; i++ {
plusContact(1000, testString)
}
}

3.2 Sprintf 拼接字符串

func sprintContact(n int, s string) string {
var joinString string
for i := 0; i < n; i++ {
joinString = fmt.Sprintf("%s%s", joinString, s)
}
return joinString
} func BenchmarkSprintContact(b *testing.B) {
for i := 0; i < b.N; i++ {
sprintContact(1000, testString)
}
}

3.3 []byte 字节切片拼接字符串

func byteContact(n int, s string) string {
b := make([]byte, 0)
for i := 0; i < n; i++ {
b = append(b, s...) // 变长参数分配
}
return string(b)
} func BenchmarkByteContact(b *testing.B) {
for i := 0; i < b.N; i++ {
byteContact(1000, testString)
}
}

3.4 bytes.Buffer 拼接字符串

func bufferContact(n int, s string) string {
b := new(bytes.Buffer)
for i := 0; i < n; i++ {
b.WriteString(s)
}
return b.String()
} func BenchmarkBufferContact(b *testing.B) {
for i := 0; i < b.N; i++ {
bufferContact(1000, testString)
}
}

3.5 strings.Builder 拼接字符串

func builderContact(n int, s string) string {
var sb strings.Builder
for i := 0; i < n; i++ {
sb.WriteString(s)
}
return sb.String()
} func BenchmarkBuilderContact(b *testing.B) {
for i := 0; i < b.N; i++ {
builderContact(1000, testString)
}
}

3.6 预分配切片容量

在知道拼接的容量情况下也可以对切片容量进行预分配:

## string.Builder 预分配切片容量
func preBuilderContact(n int, s string) string {
var sb = new(strings.Builder)
sb.Grow(n * len(testString)) for i := 0; i < n; i++ {
sb.WriteString(s)
} return sb.String()
} # []byte 预分配切片容量
func preByteContact(n int, s string) string {
b := make([]byte, 0, n*len(letterBytes))
for i := 0; i < n; i++ {
b = append(b, s...)
}
return string(b)
}

使用 Benchmark 基准测试,测试各组实现方式的性能:

[root@chunqiu stringcontact]$ go test -run="none" -v -bench . -benchmem -cpu=2,4
goos: linux
goarch: amd64
pkg: stringcontact
BenchmarkPlusContact-2 247 4765499 ns/op 34526785 B/op 999 allocs/op
BenchmarkPlusContact-4 250 4801224 ns/op 34526794 B/op 999 allocs/op
BenchmarkSprintContact-2 198 6050137 ns/op 34660487 B/op 3021 allocs/op
BenchmarkSprintContact-4 195 6159586 ns/op 34780286 B/op 3027 allocs/op
BenchmarkByteContact-2 20110 59817 ns/op 350144 B/op 21 allocs/op
BenchmarkByteContact-4 19981 60303 ns/op 350144 B/op 21 allocs/op
BenchmarkBufferContact-2 28652 41743 ns/op 196288 B/op 11 allocs/op
BenchmarkBufferContact-4 28664 42712 ns/op 196288 B/op 11 allocs/op
BenchmarkPreByteContact-2 41622 29007 ns/op 188416 B/op 3 allocs/op
BenchmarkPreByteContact-4 39409 31205 ns/op 188416 B/op 3 allocs/op
BenchmarkBuilderContact-2 23012 53037 ns/op 284608 B/op 20 allocs/op
BenchmarkBuilderContact-4 22238 53029 ns/op 284608 B/op 20 allocs/op
BenchmarkPreBuilderContact-2 78255 15835 ns/op 65536 B/op 1 allocs/op
BenchmarkPreBuilderContact-4 76986 16251 ns/op 65536 B/op 1 allocs/op
PASS
ok stringcontact 23.194s

关于性能分析和原理,可参看这篇 文章,这里不再赘述。唯一的疑惑是相比于 Buffer,Builder 的性能要低,而不是如文章所言略快 10%,存疑。


GoLang 高性能编程之字符串拼接的更多相关文章

  1. golang的字符串拼接

    常用拼接方法 字符串拼接在日常开发中是很常见的需求,目前有两种普遍做法: 一种是直接用 += 来拼接 s1 := "Hello" s2 := "World" s ...

  2. golang中的字符串拼接

    go语言中支持的字符串拼接的方法有很多种,这里就来罗列一下 常用的字符串拼接方法 1.最常用的方法肯定是 + 连接两个字符串.这与python类似,不过由于golang中的字符串是不可变的类型,因此用 ...

  3. [Golang]字符串拼接方式的性能分析

    本文100%由本人(Haoxiang Ma)原创,如需转载请注明出处. 本文写于2019/02/16,基于Go 1.11.至于其他版本的Go SDK,如有出入请自行查阅其他资料. Overview 写 ...

  4. Golang拼接字符串的5种方法及其效率_Chrispink-CSDN博客_golang 字符串拼接效率 https://blog.csdn.net/m0_37422289/article/details/103362740

    Different ways to concatenate two strings in Golang - GeeksforGeeks https://www.geeksforgeeks.org/di ...

  5. golang字符串拼接

    四种拼接方案: 1,直接用 += 操作符, 直接将多个字符串拼接. 最直观的方法, 不过当数据量非常大时用这种拼接访求是非常低效的. 2,直接用 + 操作符,这个和+=其实一个意思了. 3,用字符串切 ...

  6. golang字符串拼接性能对比

    对比 +(运算符).strings.Join.sprintf.bytes.Buffer对字符串拼接的性能 package main import ( "bytes" "f ...

  7. 从字符串拼接看JS优化原则

    来自知乎的问题:JavaScript 怎样高效拼接字符串? 请把以下用于连接字符串的JavaScript代码修改为更高效的方式: var htmlString ='< div class=”co ...

  8. Netty高性能编程备忘录(下)

    估计很快就要被拍砖然后修改,因此转载请保持原文链接,否则视为侵权... http://calvin1978.blogcn.com/articles/netty-performance.html 前文再 ...

  9. Golang核心编程

    源码地址: https://github.com/mikeygithub/GoCode 第1章 1Golang 的学习方向 Go 语言,我们可以简单的写成 Golang 1.2Golang 的应用领域 ...

  10. Javascript字符串拼接小技巧

    在Javascript中经常会遇到字符串的问题,但是如果要拼接的字符串过长就比较麻烦了. 如果是在一行的,可读性差不说,如果要换行的,会直接报错. 在此介绍几种Javascript拼接字符串的技巧. ...

随机推荐

  1. XILINX HLS 入坑记录 之 写RAM 综合出 读取+写入Ram

    最近使用 Xilinx HLS 来开发 算法的IPcore,使用的Vitis 2021,发现光是 EDA 工具就存在很多的bug,比如: 1.经常C综合 停留在 Using flow_target ' ...

  2. 【Python】【OpenCV】边缘检测和创建自定义核

    对于使用OpenCV已有的算子,我们还可以自定义卷积核以达到不同的效果. filters.py 1 import cv2 2 import numpy 3 4 """ 定 ...

  3. ElasticSearch之cat trained model API

    命令样例如下: curl -X GET "https://localhost:9200/_cat/ml/trained_models?v=true&pretty" --ca ...

  4. [VMware]ESXI下硬盘的两种直通方式

    文章来自:https://rmbz.net/archives/vmware-esxi-passthrough 最近再搞ESXI,把原来的"黑群晖"改成ESXI:因为群晖里有数据,为 ...

  5. Recursion Function 递归笔记

    递归的解释: 递归(英语:Recursion),又译为递回, 在数学与计算机科学中,是指在函数的定义中使用函数自身的方法.(本文要讨论的重点) 递归一词还较常用于描述以自相似方法重复事物的过程.(指一 ...

  6. 升级高版本springboot2.6.x:org/springframework/boot/context/properties/ConfigurationBeanFactoryMetadata

    升级springboot高版本2.6.x 项目使用到了springcloud的oauth2依赖,直接升级springboot项目版本为最新 2.6.8(2022年6月16日)将会报以下错误: org/ ...

  7. Luogu P1298 最接近的分数 做题记录

    算是水紫,不过也学到一些有用的东西. 题意 给定正小数 \(N\).求分子不大于 \(n\),分母不大于 \(m\) 的分数 \(\dfrac{n}{m}\),使得 \(\dfrac{n}{m}\) ...

  8. MySQL 基础(四)锁

    解决并发事务带来的问题 写-写情况 任意一种事务隔离级别都不允许 "脏写" 的发生,因为这样会使得数据混乱.所以,当多个未提交的事务相继对一条记录进行改动时,就需要使得这些事务串行 ...

  9. 【Python】人工智能-机器学习——不调库手撕贝叶斯分类问题

    1. 作业内容描述 1.1 背景 数据集大小150 该数据有4个属性,分别如下 Sepal.Length:花萼长度(cm) Sepal.Width:花萼宽度单位(cm) Petal.Length:花瓣 ...

  10. 数仓专家面对面 | 为什么我选择GaussDB(DWS)

    摘要:你知道数仓是如何应运而生的吗?你了解数仓未来的发展趋势吗?想知道国内数仓专家的看法吗? 导语 数据仓库的发展一直是备受关注的议题,随着近年来技术的不断演进,数仓也在更新迭代. 你知道数仓是如何应 ...