strings.Builder 源码阅读与分析
strings.Builder源码阅读与分析
背景之字符串拼接
在 Go 语言中,对于字符串的拼接处理有很多种方法,那么那种方法才是效率最高的呢?
str := []string{"aa", "bb", "cc"}
ss := ""
for _, s := range str {ss += s
}
fmt.Println(ss)
相信大部分人都会使用+
操作符或者fmt.Sprinf
进行拼接,但要注意的是,在 Go 语言中字符串是不可变的,也就是说每次修改都会导致字符串创建、销毁、内存分配、数据拷贝等操作,在高并发系统中不得不考虑更优的解决方案。所以一开始我经常使用bytes.Buffer
。
使用 bytes.Buffer
str := []string{"aa", "bb", "cc"}
var buf bytes.Buffer
for _, s := range str {buf.WriteString(s)
}
fmt.Println(buf.String())
bytes.Buffer
内部使用[]byte
来存储写入的数据(包括string
、byte
、rune
类型),从而一定程度避免了每次数据写入都重新分配内存和数据拷贝操作。
但要注意buf.String()
方法会进行[]byte
到string
的类型转换,最终还是会导致一次内存申请和数据拷贝。看一下源码实现:
func (b *Buffer) String() string {if b == nil {// Special case, useful in debugging.return "<nil>"}return string(b.buf[b.off:]) //发生类型转换
}
接下来就该strings.Builder
出场了。
使用 strings.Builder
为了改进bytes.Buffer
拼接的性能,在 Go 1.10 及以后,我们可以使用性能更强的 strings.Builder
完成字符串的拼接操作。
var builder strings.Builder
for _, s := range str {builder.WriteString(s)
}
fmt.Println(builder.String())
Benchmark
这里我们做下以上使用方式的性能对比:
func BenchmarkPlus(b *testing.B) {var s stringfor i := 0; i < b.N; i++ {s += "hello world"_ = s}
}func BenchmarkFormat(b *testing.B) {var s stringfor i := 0; i < b.N; i++ {s = fmt.Sprintf("%s%s", s, "hello world")_ = s}
}
func BenchmarkBuffer(b *testing.B) {var buf bytes.Bufferfor i := 0; i < b.N; i++ {buf.WriteString("hello world")_ = buf.String()}
}//buf.Bytes()仅作为对比buf.String()
func BenchmarkBufferBytes(b *testing.B) {var buf bytes.Bufferfor i := 0; i < b.N; i++ {buf.WriteString("hello world")_ = buf.Bytes()}
}func BenchmarkBuilder(b *testing.B) {var builder strings.Builderfor i := 0; i < b.N; i++ {builder.WriteString("hello world")_ = builder.String()}
}go test -benchmem -run=^$ -bench=. -v -count=1BenchmarkPlus-4 110467 123486 ns/op 611592 B/op 1 allocs/op
BenchmarkFormat-4 62427 158490 ns/op 692120 B/op 4 allocs/op
BenchmarkBuffer-4 87292 104293 ns/op 484132 B/op 1 allocs/op
BenchmarkBufferBytes-4 47784844 26.4 ns/op 26 B/op 0 allocs/op
BenchmarkBuilder-4 59271824 35.4 ns/op 66 B/op 0 allocs/op
从压测结果来看,strings.Builder
性能最强,BenchmarkBufferBytes
和BenchmarkBuffer
为啥差别这么大,原因就在于buf.String()
会发生一次类型转换,比较耗性能,开发中我们可以使用buf.Bytes()
返回字节切片来规避这个问题。
接下来,我们看下strings.Builder
底层是如何实现的。
源码阅读
源码文件在github.com/golang/go/src/strings/builder.go
。
strings.Builder
支持的方法是 bytes.Buffer
的子集,仔细看了一下,它实现了io.Writer
接口,而 bytes.Buffer
实现了io.Reader
和io.Writer
两个接口。
strings.Builder 结构体
type Builder struct {addr *Builder // of receiver, to detect copies by valuebuf []byte
}
从结构体可以看出数据是存在[]byte
中的,与 bytes.Buffer
思路类似,既然 string
在构建过程中,会不断地被销毁和重建,那么就通过底层使用一个 buf []byte
来存放字符串的内容,从而尽量避免这个问题。
注意里面还有个addr
字段,等下会讲。
写入操作方法
提供了四种写入方法:
func (b *Builder) WriteString(s string) (int, error) { //写入字符串b.copyCheck()b.buf = append(b.buf, s...)return len(s), nil
}
func (b *Builder) Write(p []byte) (int, error) //写入字节切片
func (b *Builder) WriteByte(c byte) error //写入字节
func (b *Builder) WriteRune(r rune) (int, error) //写入Rune
对于写操作,其实就是简单的把数据追加到buf []byte
中,利用append
来进行底层的自动扩容。
注意这里的每个写入方法开头都调用了copyCheck
,和上面的addr
字段是一回事,我们等会讲。
Grow 扩容
和bytes.Buffer
类似,strings.Builder
也提供了Grow
方法,可以让我们手动进行底层空间的扩容。当然,Grow
会先判断空间是否够用,不够的话会进行扩容:
buf := make([]byte, len(b.buf), 2*cap(b.buf)+n)
copy(buf, b.buf)
b.buf = buf
String() 黑科技
strings.Builder
之所以性能高,原因就在这了,其他的和bytes.Buffer
并无太大差别。
// String returns the accumulated string.
func (b *Builder) String() string {return *(*string)(unsafe.Pointer(&b.buf))
}
解决 bytes.Buffer
存在的 []byte
到 string
类型转换和内存拷贝问题,这里使用了一个 unsafe.Pointer
的指针转换操作,实现了直接将 buf []byte
转换为 string
类型,同时避免了内存申请、分配和销毁的问题。
当然我们也可以进行string
到[]byte
的零内存拷贝和申请转换:
func StringToBytes(str string) []byte {s := (*[2]uintptr)(unsafe.Pointer(&str))h := [3]uintptr{s[0], s[1], s[1]}return *(*[]byte)(unsafe.Pointer(&h))
}
Reset()
strings.Builder
同样提供了Reset方法
,但和bytes.Buffer()
实现的方式不同:
// Reset resets the Builder to be empty.
func (b *Builder) Reset() {b.addr = nilb.buf = nil
}
看下bytes.Buffer()
的实现:
// Reset resets the buffer to be empty,
// but it retains the underlying storage for use by future writes.
// Reset is the same as Truncate(0).
func (b *Buffer) Reset() {b.buf = b.buf[:0]b.off = 0b.lastRead = opInvalid
}
所以,我们没办法对strings.Builder
申请的内存进行复用。
不允许复制
开头我们提到了结构体里面的addr
字段,这里我们详细说下,首先看下copyCheck()
方法的实现:
func (b *Builder) copyCheck() {if b.addr == nil {b.addr = (*Builder)(noescape(unsafe.Pointer(b)))} else if b.addr != b {panic("strings: illegal use of non-zero Builder copied by value")}
}
上面我们说到,程序在每次进行写入操作时,都会调用copyCheck()
来检查:
- 当第一次调用时,会把当前
strings.Builder
实例的指针存入addr
; - 后续每次调用,都会检查当前实例的指针是否和
addr
相等,不相等会发生panic
;
为什么要做这个限制?
我的理解是和String()
方法的实现是分不开的,String()
底层调用了unsafe.Pointer()
使用指针直接操作内存,从而规避了内存申请和拷贝,但同时也是有风险的。由于在Go中字符串是不可修改的,所以通过指针进行底层转换后,string
和[]byte
共享了底层数据,这时如果另一个实例对[]byte
数据进行了修改,可能会发生panic
。
当然,从源码来看,在调用任何写入方法之前是可以进行copy的,此时还未进行copyCheck
检查。
最佳实践
一般 Go 标准库中使用的方式都是会逐步被推广的,成为某些场景下的最佳实践方式。
// 在不进行内存分配的情况下,将 []byte 转换为 string
func BytesToString(b []byte) string {return *(*string)(unsafe.Pointer(&b))
}// 在不进行内存分配的情况下,将 string 转换为 []byte
func StringToBytes(str string) []byte {s := (*[2]uintptr)(unsafe.Pointer(&str))h := [3]uintptr{s[0], s[1], s[1]}return *(*[]byte)(unsafe.Pointer(&h))
}
小结
本文通过在日常开发中使用到的字符接拼接方式进行了性能对比测试,抛出各个使用方式的问题点,从而引出从Go1.10官方发布的高性能strings.Builder
,最后对strings.Builder
源码和底层实现进行了解析。
关于究竟使用哪种方式呢?各有利弊,我认为:bytes.Buffer
和strings.Builder
使用哪种都可以。
推荐使用bytes.Buffer
优点:
- 支持方法更全面,实现了
io.Reader
和io.Writer
接口 - 可以对内存进行复用
缺点:
String()
会进行一次类型转换,当然也可以使用Bytes()
方法来规避
推荐使用strings.Builder
优点:
- 实现原理和
bytes.Buffer
类似 String()
零申请和拷贝类型转换,强能强悍
缺点:
- 支持方法少,只实现了
io.Writer
接口 - 无法复用申请的内存
参考
- Go语言字符串高效拼接
- 雨痕的Go性能优化技巧
strings.Builder 源码阅读与分析相关推荐
- disruptor源码阅读与分析---RingBuffer与Sequence
首先,我们还是编写一个测试例子吧,根据测试例子去分析代码更为直观: public static void main(String[] args){EventFactory<EventObject ...
- LightGBM源码阅读+理论分析(处理特征类别,缺省值的实现细节)
前言 关于LightGBM,网上已经介绍的很多了,笔者也零零散散的看了一些,有些写的真的很好,但是最终总觉的还是不够清晰,一些细节还是懵懵懂懂,大多数只是将原论文翻译了一下,可是某些技术具体是怎么做的 ...
- 如何模拟一个XMLHttpRequest请求用于单元测试——nise源码阅读与分析
概述 在我们进行单元测试的过程中,如果我们需要对一些HTTP接口进行相关的业务测试,那么我们就需要来模拟HTTP请求的发送与响应,否则我们就无法完成测试的闭环. 目前,有许许多多的测试框架都提供了模拟 ...
- 分布式文件系统KFS源码阅读与分析(四):RPC实现机制(KfsClient端)
上一篇博客介绍了KFS中RPC实现机制MetaServer端的实现,下面接着介绍一下KfsClient端的实现框架. KfsClient是为应用程序暴露的接口类,它是在应用程序代码和KFS Serve ...
- Zookeeper源码阅读(一)Jute和传输协议
前言 最近开始了Zookeeper的源码阅读和分析,也从现在开始把之前和现在学习到的一些Zookeeper的源码知识和我的一些理解放到博客上.不得不说这是自己第一次去完整的看一个开源项目的完整源码,从 ...
- 源码阅读分析 View的Touch事件分发
其实 Android 事件分发机制在早几年一直都困扰着我,那时候处理事件分发的自定义 View 脑子都是一片白,老感觉处理不好.后来自己看了 android 源码,也阅读了很多大牛的文章才算彻底明白, ...
- 代码分析:NASM源码阅读笔记
NASM源码阅读笔记 NASM(Netwide Assembler)的使用文档和代码间的注释相当齐全,这给阅读源码 提供了很大的方便.按作者的说法,这是一个模块化的,可重用的x86汇编器, 而且能够被 ...
- NJ4X源码阅读分析笔记系列(一)——项目整体分析
NJ4X源码阅读分析笔记系列(一)--项目整体分析 NJ4X是什么 参见NJ4X的官网:http://www.nj4x.com/ Java and .Net interfaces to support ...
- NJ4X源码阅读分析笔记系列(三)—— nj4x-ts深入分析
NJ4X源码阅读分析笔记系列(三)-- nj4x-ts深入分析 一.系统的工作流程图(模块级) 其工作流程如下(以行情获取为例): 应用端向Application Server发起连接 应用服务器调用 ...
最新文章
- POJ 2186 Tarjan
- Markdown创建页面和目录?
- mysql数据库文件的真实的物理存储位置
- 【分析】腾讯年终总结:微信用户一天到晚都在干啥
- js页面排序-----基础篇
- Struts2 简介
- 【BZOJ1001】[BeiJing2006]狼抓兔子
- php自定义通讯协议,PHP自定义协议攻击 by L0st
- CAP、BASE、ACID基本概念
- java swing html_Swing中如何使用HTML按钮
- 修复iPhonex不出声的左扬声器
- Javascript的简单介绍,只作为个人笔记,不作为知识参考,如果想要学习,请找其他文章
- Python开发手册
- treeTable树结构表格的使用
- word无法在公式编辑器中输入字符
- 《那些年啊,那些事——一个程序员的奋斗史》六
- 空间换时间,轻松提高性能100倍
- 高等数学-考试常用的三角函数公式
- eclipse windows 窗口背景颜色 保护视力
- 机器学习与算法(12)--最小角回归(LARS)
热门文章
- python数据集处理一些方法备份(长期更新)
- Delphi 获取系统时间分隔符
- 桌面上打开计算机有延迟感觉,电脑中右击操作反应慢如何解决|解决右键菜单弹出延迟的方法...
- Sqlite3并发读写注意事项
- css+js实现自动伸缩导航栏
- Ice飞冰《配置总结》
- Nebula Graph|信息图谱在携程酒店的应用
- 【FPGA——工具篇】:Modelsim SE-64 10.4下载、破解、安装过程
- SEER区块链database_api更新 支持通过txid查询交易所在区块信息
- win10系统关闭哪些服务器,win10.1系统哪些服务可以关闭掉?