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来存储写入的数据(包括stringbyterune类型),从而一定程度避免了每次数据写入都重新分配内存和数据拷贝操作。

但要注意buf.String()方法会进行[]bytestring的类型转换,最终还是会导致一次内存申请和数据拷贝。看一下源码实现:

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性能最强,BenchmarkBufferBytesBenchmarkBuffer为啥差别这么大,原因就在于buf.String()会发生一次类型转换,比较耗性能,开发中我们可以使用buf.Bytes()返回字节切片来规避这个问题。

接下来,我们看下strings.Builder底层是如何实现的。

源码阅读

源码文件在github.com/golang/go/src/strings/builder.go

strings.Builder 支持的方法是 bytes.Buffer 的子集,仔细看了一下,它实现了io.Writer接口,而 bytes.Buffer 实现了io.Readerio.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 存在的 []bytestring 类型转换和内存拷贝问题,这里使用了一个 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.Bufferstrings.Builder使用哪种都可以

推荐使用bytes.Buffer

优点:

  • 支持方法更全面,实现了io.Readerio.Writer接口
  • 可以对内存进行复用

缺点:

  • String()会进行一次类型转换,当然也可以使用Bytes()方法来规避
推荐使用strings.Builder

优点:

  • 实现原理和bytes.Buffer类似
  • String()零申请和拷贝类型转换,强能强悍

缺点:

  • 支持方法少,只实现了io.Writer接口
  • 无法复用申请的内存

参考

  • Go语言字符串高效拼接
  • 雨痕的Go性能优化技巧

strings.Builder 源码阅读与分析相关推荐

  1. disruptor源码阅读与分析---RingBuffer与Sequence

    首先,我们还是编写一个测试例子吧,根据测试例子去分析代码更为直观: public static void main(String[] args){EventFactory<EventObject ...

  2. LightGBM源码阅读+理论分析(处理特征类别,缺省值的实现细节)

    前言 关于LightGBM,网上已经介绍的很多了,笔者也零零散散的看了一些,有些写的真的很好,但是最终总觉的还是不够清晰,一些细节还是懵懵懂懂,大多数只是将原论文翻译了一下,可是某些技术具体是怎么做的 ...

  3. 如何模拟一个XMLHttpRequest请求用于单元测试——nise源码阅读与分析

    概述 在我们进行单元测试的过程中,如果我们需要对一些HTTP接口进行相关的业务测试,那么我们就需要来模拟HTTP请求的发送与响应,否则我们就无法完成测试的闭环. 目前,有许许多多的测试框架都提供了模拟 ...

  4. 分布式文件系统KFS源码阅读与分析(四):RPC实现机制(KfsClient端)

    上一篇博客介绍了KFS中RPC实现机制MetaServer端的实现,下面接着介绍一下KfsClient端的实现框架. KfsClient是为应用程序暴露的接口类,它是在应用程序代码和KFS Serve ...

  5. Zookeeper源码阅读(一)Jute和传输协议

    前言 最近开始了Zookeeper的源码阅读和分析,也从现在开始把之前和现在学习到的一些Zookeeper的源码知识和我的一些理解放到博客上.不得不说这是自己第一次去完整的看一个开源项目的完整源码,从 ...

  6. 源码阅读分析 View的Touch事件分发

    其实 Android 事件分发机制在早几年一直都困扰着我,那时候处理事件分发的自定义 View 脑子都是一片白,老感觉处理不好.后来自己看了 android 源码,也阅读了很多大牛的文章才算彻底明白, ...

  7. 代码分析:NASM源码阅读笔记

    NASM源码阅读笔记 NASM(Netwide Assembler)的使用文档和代码间的注释相当齐全,这给阅读源码 提供了很大的方便.按作者的说法,这是一个模块化的,可重用的x86汇编器, 而且能够被 ...

  8. NJ4X源码阅读分析笔记系列(一)——项目整体分析

    NJ4X源码阅读分析笔记系列(一)--项目整体分析 NJ4X是什么 参见NJ4X的官网:http://www.nj4x.com/ Java and .Net interfaces to support ...

  9. NJ4X源码阅读分析笔记系列(三)—— nj4x-ts深入分析

    NJ4X源码阅读分析笔记系列(三)-- nj4x-ts深入分析 一.系统的工作流程图(模块级) 其工作流程如下(以行情获取为例): 应用端向Application Server发起连接 应用服务器调用 ...

最新文章

  1. POJ 2186 Tarjan
  2. Markdown创建页面和目录?
  3. mysql数据库文件的真实的物理存储位置
  4. 【分析】腾讯年终总结:微信用户一天到晚都在干啥
  5. js页面排序-----基础篇
  6. Struts2 简介
  7. 【BZOJ1001】[BeiJing2006]狼抓兔子
  8. php自定义通讯协议,PHP自定义协议攻击 by L0st
  9. CAP、BASE、ACID基本概念
  10. java swing html_Swing中如何使用HTML按钮
  11. 修复iPhonex不出声的左扬声器
  12. Javascript的简单介绍,只作为个人笔记,不作为知识参考,如果想要学习,请找其他文章
  13. Python开发手册
  14. treeTable树结构表格的使用
  15. word无法在公式编辑器中输入字符
  16. 《那些年啊,那些事——一个程序员的奋斗史》六
  17. 空间换时间,轻松提高性能100倍
  18. 高等数学-考试常用的三角函数公式
  19. eclipse windows 窗口背景颜色 保护视力
  20. 机器学习与算法(12)--最小角回归(LARS)

热门文章

  1. python数据集处理一些方法备份(长期更新)
  2. Delphi 获取系统时间分隔符
  3. 桌面上打开计算机有延迟感觉,电脑中右击操作反应慢如何解决|解决右键菜单弹出延迟的方法...
  4. Sqlite3并发读写注意事项
  5. css+js实现自动伸缩导航栏
  6. Ice飞冰《配置总结》
  7. Nebula Graph|信息图谱在携程酒店的应用
  8. 【FPGA——工具篇】:Modelsim SE-64 10.4下载、破解、安装过程
  9. SEER区块链database_api更新 支持通过txid查询交易所在区块信息
  10. win10系统关闭哪些服务器,win10.1系统哪些服务可以关闭掉?