Go: performance issue about empty string check with `str == ""` in go1.14.6

Created on 21 Jul 2020  ·  4Comments  ·  Source: golang/go

What version of Go are you using (go version)?

$ go version

go version go1.14.6 darwin/amd64

Does this issue reproduce with the latest release?

yes, it does

What operating system and processor architecture are you using (go env)?

go env Output

$ go env

GO111MODULE=""
GOARCH="amd64"
GOBIN=""
GOCACHE="/Users/songjiayang/Library/Caches/go-build"
GOENV="/Users/songjiayang/Library/Application Support/go/env"
GOEXE=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="darwin"
GOINSECURE=""
GOOS="darwin"
GOPATH="/Users/songjiayang/.gvm/pkgsets/go1.14.6/global"
GOPROXY="https://goproxy.io,direct"
GOROOT="/Users/songjiayang/.gvm/gos/go1.14.6"
GOSUMDB="sum.golang.org"
GOTMPDIR=""
GOTOOLDIR="/Users/songjiayang/.gvm/gos/go1.14.6/pkg/tool/darwin_amd64"
GCCGO="gccgo"
AR="ar"
CC="clang"
CXX="clang++"
CGO_ENABLED="1"
GOMOD="/Users/songjiayang/workspace/golang/benchmark/go.mod"
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fdebug-prefix-map=/var/folders/zb/p5mbxpss5j19k76c9kd3wy5jxfrpjg/T/go-build631736981=/tmp/go-build -gno-record-gcc-switches -fno-common"

What did you do?

I run benchmark to test empty string check, the code is :

package benchmark

import (
    "log"
    "testing"
)

func BenchmarkStringEmptyCheckWithDefault(b *testing.B) {
    var foo string
    for i := 0; i < b.N; i++ {
        if foo != "" {
            log.Panicf("empty string should be default")
        }
    }
}

func BenchmarkStringEmptyCheckWithLen(b *testing.B) {
    var foo string
    for i := 0; i < b.N; i++ {
        if len(foo) != 0 {
            log.Panicf("empty string length should be 0")
        }
    }
}

run benchmark with go1.14.6:

go test -bench=BenchmarkStringEmptyCheck

goos: darwin
goarch: amd64
pkg: benchmark
BenchmarkStringEmptyCheckWithDefault-8      1000000000           0.613 ns/op
BenchmarkStringEmptyCheckWithLen-8          1000000000           0.309 ns/op
PASS
ok      benchmark   1.040s

run it with go1.10.8:

goos: darwin
goarch: amd64
BenchmarkStringEmptyCheckWithDefault-8      2000000000           0.30 ns/op
BenchmarkStringEmptyCheckWithLen-8          2000000000           0.30 ns/op
PASS

What did you expect to see?

running benchmark with go1.14.6 should behavior like go1.10.8.

What did you see instead?

but in go1.14.6, str == "" is slower than len(str) ==0 to check empty string.

Performance WaitingForInfo

Most helpful comment

Those benchmarks are not very meaningful. Both are optimized to an empty loop:

go tool objdump -s StringEmpty test.test 
TEXT _/Users/martisch/test.BenchmarkStringEmptyCheckWithDefault(SB) /Users/martisch/test/main_test.go
  main_test.go:10   0x110abc0       488b442408      MOVQ 0x8(SP), AX    
  main_test.go:10   0x110abc5       31c9            XORL CX, CX     
  main_test.go:10   0x110abc7       eb03            JMP 0x110abcc       
  main_test.go:10   0x110abc9       48ffc1          INCQ CX         
  main_test.go:10   0x110abcc       48398870010000      CMPQ CX, 0x170(AX)  
  main_test.go:10   0x110abd3       7ff4            JG 0x110abc9        
  main_test.go:10   0x110abd5       c3          RET         

TEXT _/Users/martisch/test.BenchmarkStringEmptyCheckWithLen(SB) /Users/martisch/test/main_test.go
  main_test.go:19   0x110abe0       488b442408      MOVQ 0x8(SP), AX    
  main_test.go:19   0x110abe5       31c9            XORL CX, CX     
  main_test.go:19   0x110abe7       eb03            JMP 0x110abec       
  main_test.go:19   0x110abe9       48ffc1          INCQ CX         
  main_test.go:19   0x110abec       48398870010000      CMPQ CX, 0x170(AX)  
  main_test.go:19   0x110abf3       7ff4            JG 0x110abe9        
  main_test.go:19   0x110abf5       c3          RET     

The difference in performance might be explained by branch alignment/code position differences due to patches in go1.14. Both run at the same speed of 1 clock cycle per iteration on my machine (darwin/amd64 Intel Ice Lake) with go tip (11f92e9dae), go1.14.6 and go1.14.5.

For others: see if changing the order of the benchmark functions in the source file makes a difference.

I do not think there is anything todo or can be improved between the versions as they both generate the same code.

All 4 comments

This appears to be something between 1.14.5 and 1.14.6

» go1.14.5 test -bench .
goos: linux
goarch: amd64
pkg: go.seankhliao.com/testrepo-113
BenchmarkStringEmptyCheckWithDefault-4      1000000000           0.317 ns/op
BenchmarkStringEmptyCheckWithLen-4          1000000000           0.323 ns/op
PASS
ok      go.seankhliao.com/testrepo-113  0.714s

» go1.14.6 test -bench .
goos: linux
goarch: amd64
pkg: go.seankhliao.com/testrepo-113
BenchmarkStringEmptyCheckWithDefault-4      1000000000           0.642 ns/op
BenchmarkStringEmptyCheckWithLen-4          1000000000           0.635 ns/op
PASS
ok      go.seankhliao.com/testrepo-113  1.421s

@seankhliao I test go 1.14.5 with OS darwin, str == "" is slower than len(str) == 0 as go1.14.6.

Those benchmarks are not very meaningful. Both are optimized to an empty loop:

go tool objdump -s StringEmpty test.test 
TEXT _/Users/martisch/test.BenchmarkStringEmptyCheckWithDefault(SB) /Users/martisch/test/main_test.go
  main_test.go:10   0x110abc0       488b442408      MOVQ 0x8(SP), AX    
  main_test.go:10   0x110abc5       31c9            XORL CX, CX     
  main_test.go:10   0x110abc7       eb03            JMP 0x110abcc       
  main_test.go:10   0x110abc9       48ffc1          INCQ CX         
  main_test.go:10   0x110abcc       48398870010000      CMPQ CX, 0x170(AX)  
  main_test.go:10   0x110abd3       7ff4            JG 0x110abc9        
  main_test.go:10   0x110abd5       c3          RET         

TEXT _/Users/martisch/test.BenchmarkStringEmptyCheckWithLen(SB) /Users/martisch/test/main_test.go
  main_test.go:19   0x110abe0       488b442408      MOVQ 0x8(SP), AX    
  main_test.go:19   0x110abe5       31c9            XORL CX, CX     
  main_test.go:19   0x110abe7       eb03            JMP 0x110abec       
  main_test.go:19   0x110abe9       48ffc1          INCQ CX         
  main_test.go:19   0x110abec       48398870010000      CMPQ CX, 0x170(AX)  
  main_test.go:19   0x110abf3       7ff4            JG 0x110abe9        
  main_test.go:19   0x110abf5       c3          RET     

The difference in performance might be explained by branch alignment/code position differences due to patches in go1.14. Both run at the same speed of 1 clock cycle per iteration on my machine (darwin/amd64 Intel Ice Lake) with go tip (11f92e9dae), go1.14.6 and go1.14.5.

For others: see if changing the order of the benchmark functions in the source file makes a difference.

I do not think there is anything todo or can be improved between the versions as they both generate the same code.

Closing given that the emitted instructions are the same and it is not consistently reproducible across machines. Feel free to reopen with a new example if there is a measurable change in a more complex benchmark or whole program runtime between sub versions of go1.14 that can be classified as performance regressions.

Was this page helpful?
0 / 5 - 0 ratings