I took Salvador's benchmark and added strings.Join() to it. His answer remains correct - str[:index] + string(replacement) + str[index+1:] is the fastest option for large strings assuming you want your original string preserved. strings.Join() is pretty close for small strings and very very close for large strings. I also added tests with even larger strings to see if strings.Join() becomes faster at any point - it doesn't seem to.
I also, just for funsies, hacked together two other implementations with unsafe and reflect
- One makes a copy, so it doesn't modify the original string, and has very interesting performance results - faster for small strings, much faster for medium strings, much much slower for large strings
- The other just does a mutable string and is, unsurprisingly, drastically faster for all purposes and runs in constant time, with the caveat that your original string is modified.
I also had to modify generateString() so it would the largest strings would generate in a reasonable timeframe ;)
Anyway, the code:
package main
import (
"reflect"
"strings"
"testing"
"unsafe"
)
func replaceAtIndex1(str string, replacement rune, index int) string {
out := []rune(str)
out[index] = replacement
return string(out)
}
func replaceAtIndex2(str string, replacement rune, index int) string {
return str[:index] + string(replacement) + str[index+1:]
}
func replaceAtIndex3(str string, replacement rune, index int) string {
return strings.Join([]string{str[:index], str[index + 1:]}, string(replacement))
}
func strToBytes(str string) []byte {
string_header := (*reflect.StringHeader)(unsafe.Pointer(&str))
bytes_header := &reflect.SliceHeader{
Data : string_header.Data,
Len : string_header.Len,
Cap : string_header.Len,
}
return *(*[]byte)(unsafe.Pointer(bytes_header))
}
func strToBytesCopy(str string) []byte {
bytes_unsafe := strToBytes(str)
bytes := make([]byte, len(bytes_unsafe))
copy(bytes, bytes_unsafe)
return bytes
}
func bytesToStr(bytes []byte) string {
bytes_header := (*reflect.SliceHeader)(unsafe.Pointer(&bytes))
string_header := &reflect.StringHeader{
Data : bytes_header.Data,
Len : bytes_header.Len,
}
return *(*string)(unsafe.Pointer(string_header))
}
func replaceAtIndex4(str string, replacement rune, index int) string {
bytes := strToBytesCopy(str)
bytes[index] = byte(replacement)
return bytesToStr(bytes)
}
func replaceAtIndex5(str string, replacement rune, index int) string {
bytes := strToBytes(str)
bytes[index] = byte(replacement)
return bytesToStr(bytes)
}
func generateString(n int) string{
var b strings.Builder
b.Grow(n)
for i := 0; i < n; i++ {
b.WriteRune('a')
}
return b.String()
}
func BenchmarkSmall1(b *testing.B) {
n := 10
str, index, replacement := generateString(n), n / 2, 'B'
b.ResetTimer()
for i := 0; i < b.N; i++ {
replaceAtIndex1(str, replacement, index)
}
}
func BenchmarkSmall2(b *testing.B) {
n := 10
str, index, replacement := generateString(n), n / 2, 'B'
b.ResetTimer()
for i := 0; i < b.N; i++ {
replaceAtIndex2(str, replacement, index)
}
}
func BenchmarkSmall3(b *testing.B) {
n := 10
str, index, replacement := generateString(n), n / 2, 'B'
b.ResetTimer()
for i := 0; i < b.N; i++ {
replaceAtIndex3(str, replacement, index)
}
}
func BenchmarkSmall4(b *testing.B) {
n := 10
str, index, replacement := generateString(n), n / 2, 'B'
b.ResetTimer()
for i := 0; i < b.N; i++ {
replaceAtIndex4(str, replacement, index)
}
}
func BenchmarkSmall5(b *testing.B) {
n := 10
str, index, replacement := generateString(n), n / 2, 'B'
b.ResetTimer()
for i := 0; i < b.N; i++ {
replaceAtIndex5(str, replacement, index)
}
}
func BenchmarkMedium1(b *testing.B) {
n := 100
str, index, replacement := generateString(n), n / 2, 'B'
b.ResetTimer()
for i := 0; i < b.N; i++ {
replaceAtIndex1(str, replacement, index)
}
}
func BenchmarkMedium2(b *testing.B) {
n := 100
str, index, replacement := generateString(n), n / 2, 'B'
b.ResetTimer()
for i := 0; i < b.N; i++ {
replaceAtIndex2(str, replacement, index)
}
}
func BenchmarkMedium3(b *testing.B) {
n := 100
str, index, replacement := generateString(n), n / 2, 'B'
b.ResetTimer()
for i := 0; i < b.N; i++ {
replaceAtIndex3(str, replacement, index)
}
}
func BenchmarkMedium4(b *testing.B) {
n := 100
str, index, replacement := generateString(n), n / 2, 'B'
b.ResetTimer()
for i := 0; i < b.N; i++ {
replaceAtIndex4(str, replacement, index)
}
}
func BenchmarkMedium5(b *testing.B) {
n := 100
str, index, replacement := generateString(n), n / 2, 'B'
b.ResetTimer()
for i := 0; i < b.N; i++ {
replaceAtIndex5(str, replacement, index)
}
}
func BenchmarkBig1(b *testing.B) {
n := 10000
str, index, replacement := generateString(n), n / 2, 'B'
b.ResetTimer()
for i := 0; i < b.N; i++ {
replaceAtIndex1(str, replacement, index)
}
}
func BenchmarkBig2(b *testing.B) {
n := 10000
str, index, replacement := generateString(n), n / 2, 'B'
b.ResetTimer()
for i := 0; i < b.N; i++ {
replaceAtIndex2(str, replacement, index)
}
}
func BenchmarkBig3(b *testing.B) {
n := 10000
str, index, replacement := generateString(n), n / 2, 'B'
b.ResetTimer()
for i := 0; i < b.N; i++ {
replaceAtIndex3(str, replacement, index)
}
}
func BenchmarkBig4(b *testing.B) {
n := 10000
str, index, replacement := generateString(n), n / 2, 'B'
b.ResetTimer()
for i := 0; i < b.N; i++ {
replaceAtIndex4(str, replacement, index)
}
}
func BenchmarkBig5(b *testing.B) {
n := 10000
str, index, replacement := generateString(n), n / 2, 'B'
b.ResetTimer()
for i := 0; i < b.N; i++ {
replaceAtIndex5(str, replacement, index)
}
}
func BenchmarkHuge2(b *testing.B) {
n := 100000
str, index, replacement := generateString(n), n / 2, 'B'
b.ResetTimer()
for i := 0; i < b.N; i++ {
replaceAtIndex2(str, replacement, index)
}
}
func BenchmarkHuge3(b *testing.B) {
n := 100000
str, index, replacement := generateString(n), n / 2, 'B'
b.ResetTimer()
for i := 0; i < b.N; i++ {
replaceAtIndex3(str, replacement, index)
}
}
func BenchmarkHuge4(b *testing.B) {
n := 100000
str, index, replacement := generateString(n), n / 2, 'B'
b.ResetTimer()
for i := 0; i < b.N; i++ {
replaceAtIndex4(str, replacement, index)
}
}
func BenchmarkHuge5(b *testing.B) {
n := 100000
str, index, replacement := generateString(n), n / 2, 'B'
b.ResetTimer()
for i := 0; i < b.N; i++ {
replaceAtIndex5(str, replacement, index)
}
}
func BenchmarkGargantuan2(b *testing.B) {
n := 10000000
str, index, replacement := generateString(n), n / 2, 'B'
b.ResetTimer()
for i := 0; i < b.N; i++ {
replaceAtIndex2(str, replacement, index)
}
}
func BenchmarkGargantuan3(b *testing.B) {
n := 10000000
str, index, replacement := generateString(n), n / 2, 'B'
b.ResetTimer()
for i := 0; i < b.N; i++ {
replaceAtIndex3(str, replacement, index)
}
}
func BenchmarkGargantuan4(b *testing.B) {
n := 10000000
str, index, replacement := generateString(n), n / 2, 'B'
b.ResetTimer()
for i := 0; i < b.N; i++ {
replaceAtIndex4(str, replacement, index)
}
}
func BenchmarkGargantuan5(b *testing.B) {
n := 10000000
str, index, replacement := generateString(n), n / 2, 'B'
b.ResetTimer()
for i := 0; i < b.N; i++ {
replaceAtIndex5(str, replacement, index)
}
}
func main(){}
And the results:
BenchmarkSmall1-8 20000000 99.9 ns/op
BenchmarkSmall2-8 50000000 29.5 ns/op
BenchmarkSmall3-8 20000000 58.1 ns/op
BenchmarkSmall4-8 50000000 32.0 ns/op
BenchmarkSmall5-8 1000000000 2.93 ns/op
BenchmarkMedium1-8 1000000 1034 ns/op
BenchmarkMedium2-8 20000000 68.4 ns/op
BenchmarkMedium3-8 20000000 78.8 ns/op
BenchmarkMedium4-8 30000000 49.3 ns/op
BenchmarkMedium5-8 1000000000 3.02 ns/op
BenchmarkBig1-8 20000 89557 ns/op
BenchmarkBig2-8 1000000 1204 ns/op
BenchmarkBig3-8 1000000 1257 ns/op
BenchmarkBig4-8 1000000 1200 ns/op
BenchmarkBig5-8 1000000000 2.93 ns/op
BenchmarkHuge2-8 200000 10260 ns/op
BenchmarkHuge3-8 200000 9908 ns/op
BenchmarkHuge4-8 100000 13628 ns/op
BenchmarkHuge5-8 1000000000 2.99 ns/op
BenchmarkGargantuan2-8 2000 822881 ns/op
BenchmarkGargantuan3-8 2000 807522 ns/op
BenchmarkGargantuan4-8 1000 2148387 ns/op
BenchmarkGargantuan5-8 1000000000 2.96 ns/op