字符串连接哪一种方式最高效

Go里面string是最基础的类型,是一个只读类型,针对他的每一个操作都会创建一个新的string


所以,如果我在不知道结果是多少长字符串的情况下不断的连接字符串,怎么样的方式是最好的呢?


最常用的可能是如下这样:


s := ""
for i := 0; i < 1000; i++ {
s += otherString()
}
return s

但是这样好像非常的慢

已邀请:

sheepbao - 流媒体,分布式,即时通信

赞同来自: astaxie lwhile davidcai1993 wida haohongfan 小蚂蚁 bvaccc raindylong huangz更多 »

package main

import (
"bytes"
"fmt"
"strings"
"time"
)

var way map[int]string

func benchmarkStringFunction(n int, index int) (d time.Duration) {
v := "ni shuo wo shi bu shi tai wu liao le a?"
var s string
var buf bytes.Buffer

t0 := time.Now()
for i := 0; i < n; i++ {
switch index {
case 0: // fmt.Sprintf
s = fmt.Sprintf("%s[%s]", s, v)
case 1: // string +
s = s + "[" + v + "]"
case 2: // strings.Join
s = strings.Join([]string{s, "[", v, "]"}, "")
case 3: // stable bytes.Buffer
buf.WriteString("[")
buf.WriteString(v)
buf.WriteString("]")
}

}
d = time.Since(t0)
if index == 3 {
s = buf.String()
}
fmt.Printf("string len: %d\t", len(s))
fmt.Printf("time of [%s]=\t %v\n", way[index], d)
return d
}

func main() {
way = make(map[int]string, 5)
way[0] = "fmt.Sprintf"
way[1] = "+"
way[2] = "strings.Join"
way[3] = "bytes.Buffer"

k := 4
d := [5]time.Duration{}
for i := 0; i < k; i++ {
d[i] = benchmarkStringFunction(10000, i)
}
}

结果:


string len: 410000      time of [fmt.Sprintf]=   426.001476ms                                                      
string len: 410000 time of [+]= 307.044147ms
string len: 410000 time of [strings.Join]= 738.44362ms
string len: 410000 time of [bytes.Buffer]= 742.248µs


  • strings.Join 最慢

  • fmt.Sprintf 和 string + 差不多

  • bytes.Buffer又比上者快约500倍

如果是少量小文本拼接,用 “+” 就好


如果是大量小文本拼接,用 strings.Join


如果是大量大文本拼接,用 bytes.Buffer

stevewang

赞同来自: judas sundyli gostar wawayi

应该用strings.Join,而且和+bytes.Buffer相比,strings.Join是最快的,bytes.Buffer的性能略差但是接近,+最慢是strings.Join执行时间的两倍还多。


为什么strings.Join最快?看看它的源码就知道了,很难用go语言实现一个更快的函数了。它的实现是先计算出要分配的内存空间,然后依次复制字符串。几乎完全没有多余的内存分配也没有多余的字符串拷贝,还能再怎么快呢?
另外,测试还是应该用testing.B而不是自己去写测试框架;而且,测试性能要避免I/O的影响。


我的测试结果是:


strings.Join:
10000000 275 ns/op
bytes.Buffer:
5000000 388 ns/op
+:
2000000 985 ns/op

测试代码如下:


package main

import(
"bytes"
"fmt"
"strings"
"testing"
)

var (
strs = []string{
"one",
"two",
"three",
"four",
"five",
"six",
"seven",
"eight",
"nine",
"ten",
}
)

func TestStringsJoin(b *testing.B) {
for i := 0; i < b.N; i++ {
strings.Join(strs, "")
}
}

func TestStringsPlus(b *testing.B) {
for i := 0; i < b.N; i++ {
var s string
for j := 0; j < len(strs); j++ {
s += strs[j]
}
}
}

func TestBytesBuffer(b *testing.B) {
for i := 0; i < b.N; i++ {
var b bytes.Buffer
for j := 0; j < len(strs); j++ {
b.WriteString(strs[j])
}
}
}

func main() {
fmt.Println("strings.Join:")
fmt.Println(testing.Benchmark(TestStringsJoin))
fmt.Println("bytes.Buffer:")
fmt.Println(testing.Benchmark(TestBytesBuffer))
fmt.Println("+:")
fmt.Println(testing.Benchmark(TestStringsPlus))
}

lwhile

赞同来自: sheepbao cz000 sundyli

分享StackOverflow上的一个类似问题下的答案.在作者的测试中,bytes.Buffer的性能并不是最好的.
How to efficiently concatenate strings in Go?

tupunco

赞同来自: kylefeng

学习过 JAVA 或者 C# 就知道, 拼长字符串用 StringBuilder/StringBuffer.
gobytes.Buffer 性能最好.

mintzhao - 区块链开发者

赞同来自: tupunco

fmt.Sprintf("%s%s", "abc", "def")

在99%的业务场景中,考虑这些完全是吹毛求疵吧

willee

赞同来自: CodyGuo

利用bytes.buffer,消耗时间最短

lwhile

赞同来自:

strings.Join()并不是用来做字符串拼接的.


如果对字符串的拼接有效率要求,那么最好转换成字节来操作.


s1 := ""
for i := 0; i < 100000; i++ {
s1 += "test"
}
//6.824335291s

s2 := ""
var b2 []byte
b2 = append(b2,[]byte(s2)...)
for i:=0; i < 100000; i++ {
b2 = append(b2, []byte("test")...)
}
s2 = string(b2)
//5.885215ms

2gua - 码农2gua

赞同来自:

推荐strings.Join。

philc - https://github.com/philchia

赞同来自:


func JoinStrings(strs ...string) string {
ln := 0
for i := 0; i < len(strs); i++ {
ln += len(strs[i])
}
bts := make([]byte, ln)
ln = 0
for _, str := range strs {
ln += copy(bts[ln:], str)
}

return string(bts)
}

ianwoolf

赞同来自:

我测过 +连接是最快的 快了很多 fmt最慢,非常慢 buffer和strings处于中间,相差不是很多

seeyoup

赞同来自:

strings.Join要使用slice,slice长度不固定的话,每次append字符串,内部都要重新改变数组,这样也有影响吗?

toyijiu - 编程爱好者

赞同来自:



  • 还没有写代码具体测试过,个人感觉是如果一次把所有要添加的string[]搞进去的话,Join比较快。如果是每次只添加一个string,bytes.Buffer的WriteString比较快,但是当buffer太大时可能会报panic



  • 源码里面Join会先计算总的结果长度,只需要make一次。bytes.Buffer的WriteString每次只能加一个string,只有当前添加后总的长度大于cap后会重新申请内存。Sprintf的核心是doPrintf函数,看了下比较复杂,IO流对象的函数一般都很慢,涉及很多判断,还要根据字符做for循环,应该很慢


// Join concatenates the elements of a to create a single string.   The separator string
// sep is placed between elements in the resulting string.
func Join(a []string, sep string) string {
if len(a) == 0 {
return ""
}
if len(a) == 1 {
return a[0]
}
n := len(sep) * (len(a) - 1)
for i := 0; i < len(a); i++ {
n += len(a[i])
}

b := make([]byte, n)
bp := copy(b, a[0])
for _, s := range a[1:] {
bp += copy(b[bp:], sep)
bp += copy(b[bp:], s)
}
return string(b)
}

    // WriteString appends the contents of s to the buffer.  The return
// value n is the length of s; err is always nil.
// If the buffer becomes too large, WriteString will panic with
// ErrTooLarge.
func (b *Buffer) WriteString(s string) (n int, err error) {
b.lastRead = opInvalid
m := b.grow(len(s))
return copy(b.buf[m:], s), nil
}

func (b *Buffer) grow(n int) int {
m := b.Len()
// If buffer is empty, reset to recover space.
if m == 0 && b.off != 0 {
b.Truncate(0)
}
if len(b.buf)+n > cap(b.buf) {
var buf []byte
if b.buf == nil && n <= len(b.bootstrap) {
buf = b.bootstrap[0:]
} else {
// not enough space anywhere
buf = makeSlice(2*cap(b.buf) + n)
copy(buf, b.buf[b.off:])
}
b.buf = buf
b.off = 0
}
b.buf = b.buf[0 : b.off+m+n]
return b.off + m
}

995 func (p *pp) doPrintf(format string, a []interface{}) {
996 end := len(format)
997 fieldnum := 0 // we process one field per non-trivial format
998 for i := 0; i < end; {
999 lasti := i
1000 for i < end && format[i] != '%' {
1001 i++
1002 }
1003 if i > lasti {
1004 p.buf.WriteString(format[lasti:i])
1005 }
1006 if i >= end {
1007 // done processing format string
1008 break
1009 }
1010
1011 // Process one verb
1012 i++
1013 // flags and widths
1014 p.fmt.clearflags()
1015 F:
1016 for ; i < end; i++ {
1017 switch format[i] {
1018 case '#':
1019 p.fmt.sharp = true
1020 case '0':
1021 p.fmt.zero = true
1022 case '+':
1023 p.fmt.plus = true
1024 case '-':
1025 p.fmt.minus = true
1026 case ' ':
1027 p.fmt.space = true
1028 default:
1029 break F
1030 }
1031 }
1032 // do we have width?
1033 if i < end && format[i] == '*' {
1034 p.fmt.wid, p.fmt.widPresent, i, fieldnum = intFromArg(a, end, i, fieldnum)
1035 if !p.fmt.widPresent {
1036 p.buf.Write(widthBytes)
1037 }
1038 } else {
1039 p.fmt.wid, p.fmt.widPresent, i = parsenum(format, i, end)
1040 }
1041 // do we have precision?
1042 if i < end && format[i] == '.' {
1043 if format[i+1] == '*' {
1044 p.fmt.prec, p.fmt.precPresent, i, fieldnum = intFromArg(a, end, i+1, fieldnum)
1045 if !p.fmt.precPresent {
1046 p.buf.Write(precBytes)
1047 }
1048 } else {
1049 p.fmt.prec, p.fmt.precPresent, i = parsenum(format, i+1, end)
1050 if !p.fmt.precPresent {
1051 p.fmt.prec = 0
1052 p.fmt.precPresent = true
1053 }
1054 }
1055 }
1056 if i >= end {
1057 p.buf.Write(noVerbBytes)
1058 continue
1059 }
1060 c, w := utf8.DecodeRuneInString(format[i:])
1061 i += w
1062 // percent is special - absorbs no operand
1063 if c == '%' {
1064 p.buf.WriteByte('%') // We ignore width and prec.
1065 continue
1066 }
1067 if fieldnum >= len(a) { // out of operands
1068 p.buf.WriteByte('%')
1069 p.add(c)
1070 p.buf.Write(missingBytes)
1071 continue
1072 }
1073 field := a[fieldnum]
1074 fieldnum++
1075
1076 goSyntax := c == 'v' && p.fmt.sharp
1077 plus := c == 'v' && p.fmt.plus
1078 p.printField(field, c, plus, goSyntax, 0)
1079 }
1080
1081 if fieldnum < len(a) {
1082 p.buf.Write(extraBytes)
1083 for ; fieldnum < len(a); fieldnum++ {
1084 field := a[fieldnum]
1085 if field != nil {
1086 p.buf.WriteString(reflect.TypeOf(field).String())
1087 p.buf.WriteByte('=')
1088 }
1089 p.printField(field, 'v', false, false, 0)
1090 if fieldnum+1 < len(a) {
1091 p.buf.Write(commaSpaceBytes)
1092 }
1093 }
1094 p.buf.WriteByte(')')
1095 }
1096 }

qi19901212 - 一个喜欢讲产品代码敲的不怎么样的.......

赞同来自:


strings.Join(a []string, sep string)


sheepbao - 流媒体,分布式,即时通信

赞同来自:

我记得有人测试过“+”,应该也不慢,可以试试fmt.Sprintf()

要回复问题请先登录注册