Golang performance benchmark and deep into the middle code: a string operation example

Golang performance benchmark and deep into the middle code: a string operation example

Context

Recently, I’m looking for efficient string manipulation in Golang, during the process of that, I used Golang benchmark to test the execution time and checked the Golang middle code to know the underlying mechanism between different solutions.

The problem

Given a long string, replace the given character with the other given character in it and return the new string.

For example, the string is “aaaaaa”, replace “a” with “b”, after replacement, the return string will be “bbbbbb”.

All the following code can be found at Github Gist.

Solution 1: string concatenation

1
2
3
4
5
6
7
8
9
10
11
func replaceChar1(str string, ch, replaceCh byte) string {
var result string
for i := 0; i < len(str); i++ {
if str[i] == ch {
result = result + string(replaceCh)
} else {
result = result + string(result[i])
}
}
return result
}

Solution 2: Byte array

1
2
3
4
5
6
7
8
9
func replaceChar2(str string, ch, replaceCh byte) string {
bytes := []byte(str)
for i := 0; i < len(str); i++ {
if bytes[i] == ch {
bytes[i] = replaceCh
}
}
return string(bytes)
}

Solution 3: String builder

1
2
3
4
5
6
7
8
9
10
11
12
13
func replaceChar3(str string, ch, replaceCh byte) string {
var strBuilder strings.Builder
strBuilder.Grow(len(str))

for i := 0; i < len(str); i++ {
if str[i] == ch {
strBuilder.WriteByte(replaceCh)
} else {
strBuilder.WriteByte(str[i])
}
}
return strBuilder.String()
}

Performance Comparison

With 3 solutions right now, I want to know which one is more efficient, Golang benchmark is a good way of comparing the operation efficiency of these.

  1. Add a test file with _test suffix
  2. Add functions with Benchmark as prefix like
1
2
3
4
5
6
7
8
9
10
11
func BenchmarkStringReplace1(b *testing.B) {
// generate a 100,000-long string
n := 100_000
str := generateString(n)

// reset the timer to remove the execution time of generateString(100_000)
b.ResetTimer()
for i := 0; i < b.N; i++ {
replaceChar1(str, 'a', 'b')
}
}
  1. Run the benchmark
    The benchmark function must run the target code b.N times. During benchmark execution, b.N is adjusted until the benchmark function lasts long enough to be timed reliably.
1
2
3
4
5
6
$ go test -bench=. string_op_benchmark.go string_op_benchmark_test.go
goos: darwin
goarch: arm64
BenchmarkStringReplace1-10 3 389948722 ns/op
BenchmarkStringReplace2-10 18235 62494 ns/op
BenchmarkStringReplace3-10 5534 217069 ns/op

The output means that the loop BenchmarkStringReplace1-10 ran 3 times at a speed of 389948722 ns per loop.

The BenchmarkStringReplace2 behaved the best and BenchmarkStringReplace1-10 (String concatenation) behaved the worst.

One step further

What if I’m still curious about the behind scene of the code? I want to know what replaceChar2 did and the middle code it executed. The following command will generate the middle code of the replaceChar2 function.

1
2
3
$ GOSSAFUNC=replaceChar2 go build string_op_benchmark.go
# command-line-arguments
dumped SSA to ./ssa.html

Open the ssa.html, we can see the original code of replaceChar2 and its middle code, like

bytes := []byte(str) is mapped to runtime.stringtoslicebyte, return string(bytes) is mapped to runtime.slicebytetostring

SSA

If we want to make the string operation faster, we might reduce the memory copy or find the other operations we can eliminate.

Reference

  1. Golang benchmark
  2. Golang design and implementation

Comments

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×