For today and upcoming daily entries, we will discuss benchmark with memory metrics with the two functions below.
Both BuildMessageBatchGrowing and BuildMessageBatchPreallocated iterates a 2d array and collects them, but notice that the latter allocates the total bytes first before collecting the values.
func BuildMessageBatchGrowing(messages [][]byte) []byte {
var batch []byte
for _, message := range messages {
batch = append(batch, message...)
}
return batch
}
func BuildMessageBatchPreallocated(messages [][]byte) []byte {
totalBytes := 0
for _, message := range messages {
totalBytes += len(message)
}
batch := make([]byte, 0, totalBytes)
for _, message := range messages {
batch = append(batch, message...)
}
return batch
}
This is the benchmark test, with each function above receiving ~64KB data.
func BenchmarkBuildMessageBatch(b *testing.B) {
// Both implementations receive the same 1,000 existing 64-byte messages.
message := bytes.Repeat([]byte{'x'}, 64)
messages := make([][]byte, 1_000)
for i := range messages {
messages[i] = message
}
const outputBytes = 1_000 * 64
b.Run("implementation=growing", func(b *testing.B) {
var batch []byte
b.ReportAllocs()
b.SetBytes(outputBytes)
for b.Loop() {
batch = BuildMessageBatchGrowing(messages)
}
sinkMessageBatch = batch
})
b.Run("implementation=preallocated", func(b *testing.B) {
var batch []byte
b.ReportAllocs()
b.SetBytes(outputBytes)
for b.Loop() {
batch = BuildMessageBatchPreallocated(messages)
}
sinkMessageBatch = batch
})
}
With basic benchmarking comand below, we could say that has fewer allocation per operation (65536 B/op).
go test -run '^$' \
-bench '^BenchmarkBuildMessageBatch$'
goos: linux
goarch: arm64
pkg: codec01
BenchmarkBuildMessageBatch/implementation=growing-12 18085 66364 ns/op 964.38 MB/s 285381 B/op 19 allocs/op
BenchmarkBuildMessageBatch/implementation=preallocated-12 75366 17774 ns/op 3600.68 MB/s 65536 B/op 1 allocs/op
PASS
ok codec01 2.547s