strings.Builder is slower?

300 views
Skip to first unread message

T L

unread,
Jun 14, 2018, 9:42:43 AM6/14/18
to golang-nuts

package main

import "strings"
import "unsafe"
import "testing"

// a copu of the string.Join function
func joinx
(a []string, sep string) string {
   
switch len(a) {
   
case 0:
       
return ""
   
case 1:
       
return a[0]
   
case 2:
       
// Special case for common small values.
       
// Remove if golang.org/issue/6714 is fixed
       
return a[0] + sep + a[1]
   
case 3:
       
// Special case for common small values.
       
// Remove if golang.org/issue/6714 is fixed
       
return a[0] + sep + a[1] + sep + a[2]
   
}
    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)
}

func joiny
(a []string, sep string) string {
   
switch len(a) {
   
case 0:
       
return ""
   
case 1:
       
return a[0]
   
case 2:
       
// Special case for common small values.
       
// Remove if golang.org/issue/6714 is fixed
       
return a[0] + sep + a[1]
   
case 3:
       
// Special case for common small values.
       
// Remove if golang.org/issue/6714 is fixed
       
return a[0] + sep + a[1] + sep + a[2]
   
}
    n
:= len(sep) * (len(a) - 1)
   
for i := 0; i < len(a); i++ {
        n
+= len(a[i])
   
}

   
var b strings.Builder
    b
.Grow(n)
    b
.WriteString(a[0])
   
for _, s := range a[1:] {
        b
.WriteString(sep)
        b
.WriteString(s)
   
}
   
return b.String()
}

func joinz
(a []string, sep string) string {
   
switch len(a) {
   
case 0:
       
return ""
   
case 1:
       
return a[0]
   
case 2:
       
// Special case for common small values.
       
// Remove if golang.org/issue/6714 is fixed
       
return a[0] + sep + a[1]
   
case 3:
       
// Special case for common small values.
       
// Remove if golang.org/issue/6714 is fixed
       
return a[0] + sep + a[1] + sep + a[2]
   
}
    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)(unsafe.Pointer(&b))
}


var words = []string{"abcdefghijklmn", "opqrstuvwxyz", "abcdefghijklmn", "opqrstuvwxyz", "abcdefghijklmn", "opqrstuvwxyz", "abcdefghijklmn", "opqrstuvwxyz", "abcdefghijklmn", "opqrstuvwxyz", }
var x, y, z string

func
Benchmark_x(b *testing.B) {
   
for i := 0; i < b.N; i++ {
        x
= joinx(words, ",")
   
}
}

func
Benchmark_y(b *testing.B) {
   
for i := 0; i < b.N; i++ {
        y
= joiny(words, ",")
   
}
}

func
Benchmark_z(b *testing.B) {
   
for i := 0; i < b.N; i++ {
        y
= joinz(words, ",")
   
}
}




Benchmark_x-4        3000000           454 ns/op         288 B/op           2 allocs/op
Benchmark_y-4        3000000           530 ns/op         144 B/op           1 allocs/op
Benchmark_z-4        5000000           335 ns/op         144 B/op           1 allocs/op







Michael Jones

unread,
Jun 14, 2018, 10:16:43 AM6/14/18
to T L, golang-nuts
joinz will always be advantaged by its situational knowledge.

relative speed: 
167 => 132 = -21%
167 => 121 = -27%

celeste:join mtj$ go test -bench=.
goos: darwin
goarch: amd64
pkg: join
Benchmark_x-8    10000000        167 ns/op
Benchmark_y-8    10000000        131 ns/op
Benchmark_z-8    10000000        120 ns/op
PASS
ok  join 4.670s
celeste:join mtj$ go test -bench=. -benchtime=10s
goos: darwin
goarch: amd64
pkg: join
Benchmark_x-8    100000000        167 ns/op
Benchmark_y-8    100000000        132 ns/op
Benchmark_z-8    100000000        121 ns/op
PASS
ok  join 42.590s


--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.


--
Michael T. Jones
michae...@gmail.com

T L

unread,
Jun 14, 2018, 10:25:20 AM6/14/18
to golang-nuts
On my machine, if the separator argument is very short, then joiny is constantly slower than joinx.
If the separator argument has at least six bytes, then joiny is faster than joinx.

$ cat /proc/cpuinfo | grep 'model name' | uniq
model name    : Intel(R) Core(TM) i3-2350M CPU @ 2.30GHz

Michael Jones

unread,
Jun 14, 2018, 10:35:23 AM6/14/18
to T L, golang-nuts
Good observation: I should not have said "always:" because when the array is short or the strings are short then you are testing the wrapper code and not the string-joining code. That may fall either way.

T L

unread,
Jun 14, 2018, 10:42:02 AM6/14/18
to golang-nuts


On Thursday, June 14, 2018 at 10:35:23 AM UTC-4, Michael Jones wrote:
Good observation: I should not have said "always:" because when the array is short or the strings are short then you are testing the wrapper code and not the string-joining code. That may fall either way.

It is no problem that joinz is always the fastest.
 

T L

unread,
Jun 19, 2018, 11:39:54 AM6/19/18
to golang-nuts

I made a test on my local machine. If the copyCheck method is modified as the following one.
The efficiencies of fy and fz become almost the same.

name  old time/op  new time/op  delta
_x    
320ns ± 0%   341ns ± 1%   +6.62%  (p=0.000 n=9+10)
_y    
409ns ± 1%   268ns ± 1%  -34.31%  (p=0.000 n=10+10)
_z    
251ns ± 1%   264ns ± 0%   +4.88%  (p=0.000 n=10+9)

However, the benchmark results on my computer often some illogical.
So can anyone help me verify the above result?



func (b *Builder) copyCheck() {
      if b.addr != b {
             if b.addr != nil {
                    panic("strings: illegal use of non-zero Builder copied by value")
             }

              // This hack works around a failing of Go's escape analysis
             // that was causing b to escape and be heap allocated.
             // See issue 23382.
             // TODO: once issue 7921 is fixed, this should be reverted to
             // just "b.addr = b".
             b.addr = (*Builder)(noescape(unsafe.Pointer(b)))
      }
}

Manlio Perillo

unread,
Jun 19, 2018, 12:08:56 PM6/19/18
to golang-nuts
Il giorno martedì 19 giugno 2018 17:39:54 UTC+2, T L ha scritto:

I made a test on my local machine. If the copyCheck method is modified as the following one.
The efficiencies of fy and fz become almost the same.

name  old time/op  new time/op  delta
_x    
320ns ± 0%   341ns ± 1%   +6.62%  (p=0.000 n=9+10)
_y    
409ns ± 1%   268ns ± 1%  -34.31%  (p=0.000 n=10+10)
_z    
251ns ± 1%   264ns ± 0%   +4.88%  (p=0.000 n=10+9)

However, the benchmark results on my computer often some illogical.

I had the same issue with similar code.  Maybe the cause is the same?
 
So can anyone help me verify the above result?



func (b *Builder) copyCheck() {
      if b.addr != b {
             if b.addr != nil {
                    panic("strings: illegal use of non-zero Builder copied by value")
             }

              // This hack works around a failing of Go's escape analysis
             // that was causing b to escape and be heap allocated.
             // See issue 23382.
             // TODO: once issue 7921 is fixed, this should be reverted to
             // just "b.addr = b".
             b.addr = (*Builder)(noescape(unsafe.Pointer(b)))
      }
}




Manlio 

T L

unread,
Jun 19, 2018, 1:02:54 PM6/19/18
to golang-nuts


On Tuesday, June 19, 2018 at 12:08:56 PM UTC-4, Manlio Perillo wrote:
Il giorno martedì 19 giugno 2018 17:39:54 UTC+2, T L ha scritto:

I made a test on my local machine. If the copyCheck method is modified as the following one.
The efficiencies of fy and fz become almost the same.

name  old time/op  new time/op  delta
_x    
320ns ± 0%   341ns ± 1%   +6.62%  (p=0.000 n=9+10)
_y    
409ns ± 1%   268ns ± 1%  -34.31%  (p=0.000 n=10+10)
_z    
251ns ± 1%   264ns ± 0%   +4.88%  (p=0.000 n=10+9)

However, the benchmark results on my computer often some illogical.

I had the same issue with similar code.  Maybe the cause is the same?

I couldn't confirm whether or not the two are the same problem.
But the efficiency of Go code is often counter-intuitive.
 

T L

unread,
Jun 19, 2018, 1:16:20 PM6/19/18
to golang-nuts


On Tuesday, June 19, 2018 at 11:39:54 AM UTC-4, T L wrote:

I made a test on my local machine. If the copyCheck method is modified as the following one.
The efficiencies of fy and fz become almost the same.

name  old time/op  new time/op  delta
_x    
320ns ± 0%   341ns ± 1%   +6.62%  (p=0.000 n=9+10)
_y    
409ns ± 1%   268ns ± 1%  -34.31%  (p=0.000 n=10+10)
_z    
251ns ± 1%   264ns ± 0%   +4.88%  (p=0.000 n=10+9)

However, the benchmark results on my computer often some illogical.
So can anyone help me verify the above result?

sorry, in the benchmark, the old data is for Go 1.10.3, but the new data is for tip.
It looks the strings.Builder has been optimized on tip.


 
Reply all
Reply to author
Forward
0 new messages