Перевод/заметки fmt.Sprintf vs String Concat
Конкатенация строк не всегда является самым элегантным решением:
getFieldName := gf.tableName + "+" + gf.ColName
Однако она гораздо быстрее, чем fmt.Sprintf
:
getFieldName := fmt.Sprintf("%s+%s", gf.tableName, gf.ColName)
Недавно мы столкнулись с распространённым мнением о том, что строковые операции выполняются нечасто и не занимают много времени по сравнению с накладными расходами на сеть и ввод-вывод. Для серверов в ДЦ это, конечно, верно. Но переход от Sprintf
к +
улучшает производительность Dolt в целом для локального сервера на 1-3%, (бенчмарки здесь):
goos: darwin
goarch: arm64
pkg: github.com/dolthub/dolt/go/performance/microsysbench
│ before.txt │ after.txt │
│ sec/op │ sec/op vs base │
OltpPointSelect-12 22.85µ ± 13% 21.61µ ± 1% -5.44% (p=0.002 n=6)
OltpJoinScan-12 267.5µ ± 2% 259.2µ ± 1% -3.13% (p=0.002 n=6)
ProjectionAggregation-12 11.76m ± 14% 11.67m ± 9% ~ (p=0.818 n=6)
SelectRandomPoints-12 74.46µ ± 1% 71.45µ ± 1% -4.05% (p=0.002 n=6)
SelectRandomRanges-12 101.69µ ± 1% 98.14µ ± 4% -3.49% (p=0.015 n=6)
geomean 222.4µ 214.9µ -3.39%
│ before.txt │ after.txt │
│ B/op │ B/op vs base │
OltpPointSelect-12 19.88Ki ± 0% 19.46Ki ± 0% -2.13% (p=0.002 n=6)
OltpJoinScan-12 164.6Ki ± 0% 162.8Ki ± 0% -1.08% (p=0.002 n=6)
ProjectionAggregation-12 2.930Mi ± 14% 2.945Mi ± 8% ~ (p=0.937 n=6)
SelectRandomPoints-12 68.56Ki ± 0% 67.87Ki ± 0% -1.01% (p=0.002 n=6)
SelectRandomRanges-12 99.81Ki ± 0% 99.46Ki ± 0% -0.36% (p=0.002 n=6)
geomean 146.4Ki 145.2Ki -0.82%
│ before.txt │ after.txt │
│ allocs/op │ allocs/op vs base │
OltpPointSelect-12 443.0 ± 0% 416.0 ± 0% -6.09% (p=0.002 n=6)
OltpJoinScan-12 6.019k ± 0% 5.903k ± 0% -1.93% (p=0.002 n=6)
ProjectionAggregation-12 70.30k ± 14% 70.69k ± 8% ~ (p=0.937 n=6)
SelectRandomPoints-12 1.255k ± 0% 1.211k ± 0% -3.51% (p=0.002 n=6)
SelectRandomRanges-12 2.375k ± 0% 2.353k ± 0% -0.93% (p=0.002 n=6)
geomean 3.544k 3.458k -2.41%
Не всегда легко добиться повышения производительности на несколько процентов, особенно когда требуется внести изменения всего лишь в 4 строках кода:
func (p *GetField) String() string {
- return fmt.Sprintf("%s.%s", p.table, p.name)
+ return p.table + "." + p.name
}
Как работает fmt.Sprintf
Функция принимает на вход строку для форматирования и несколько аргументов типа any
.
Упрощенный псевдокод Sprintf
может выглядеть следующим образом:
type formatter struct {
buf []byte
lastI int
argI int
}
func (f *formatter) Sprintf(format string, args []any) string {
for i := 0; i < len(format); i++ {
if format[i] == '%' {
if i != f.lastI {
f.buf = append(f.buf, format[f.lastI:i])
}
f.lastI = i
i++
switch format[i] {
case 's':
f.lastI = i+1
f.fmtString(args[f.argI])
f.argI++
case 'd':
f.lastI = i+1
f.fmtInt(args[f.argI])
f.argI++
case '%': // percent literal, passthrough
default:
panic("unknown format sequence")
}
}
i++
}
if f.lastI != len(format) {
f.buf = append(f.buf, format[f.lastI:])
}
return string(f.buf)
}
Мы ограничились рассмотрением только символов %s
и %d
, не углубляясь в более сложные детали форматирования и не объединяя объекты formatter
. Тем не менее, основной цикл наглядно демонстрирует суть процесса:
- Найти следующий форматирующий символ
- Записать строку между текущим и последним форматирующим символом
- Записать следующий аргумент, основываясь на текущем символе
- Запись префиксной строки
Помимо отслеживания состояний, значительная часть сложности связана с форматорами, которые зависят от типа:
func (f *formatter) fmtString(a any) {
switch s := a.(type) {
case string:
copy(f.buf[f.bufI:], s)
f.bufI += len(s)
case fmt.Stringer:
sS := s.String()
copy(f.buf[f.bufI:], sS)
f.bufI += len(sS)
default:
panic("unable to format %T as string")
}
}
Почему fmt.Sprintf
медленный?
Первая сложность связана с интерфейсом any
. Эта 8-байтовая обертка вмещает любой тип данных, который мы хотели бы вывести, и автоматически использует интерфейс Stringer
.
Вторая проблема заключается в аллокации объектов для отслеживания состояния форматирования и промежуточных буферов для записи новых строк.
Наконец, третья проблема — это объем и гибкость кода, необходимые для обеспечения полной корректности. Стандартная библиотека содержит несколько сотен строк кода, который сильно разветвлён.
Почему конкатенация строк быстрая?
Оператор +
в языке Go преобразуется в операцию accumulate/reduct. Все аргументы собираются в стеке и передаются оператору runtime.stringconcat
. stringconcat
подсчитывает общее количество байт в строке и использует его, чтобы определить, поместится ли результат в стек для возврата [32]byte
или потребуется выделить буфер в куче.
s, b := rawstringtmp(buf, strLength)
for _, x := range args {
copy(b, x)
b = b[len(x):]
}
return s
Если идти по быстрому пути и сохранять размер строк небольшим, то конкатенация будет выполняться очень быстрой.
Комментарии в Telegram-группе!