Benchmark Report I: Go 1.23’s Iter package.
This post will test the performance of the solutions outlined here.
One of the dangerous and beautiful aspects of programming is that you’re greeted with a blank screen. Within this screen, you can paint the picture that is your program. There are no limitations, just the 104 keys you can press on your keyboard and the rules laid out by your language’s interpreter or compiler.
One thing I realized late in my career is: all that shines green during compilation is not a sign of good code. There are runtime related issues to take into consideration as well.
In one of my most recent posts, Go 1.23’s Iter package, I attempt to offer an alternative way to to guarantee the iteration order of a map. In this post, I’m going to use the tools offered by Go to determine which approach is more efficient in terms of memory usage and operation time.
Setup the Test Environment
For this test, I’ll write two functions:
- A function that will iterate over a map with a custom defined iterator. Here is the code of said function:
// This function uses the custom iterator defined in
// previous Go 1.23 iter... post.
func ProcessTotalWithIter(m CustomMapType) int {
result := int(0)
for _, v := range m.SortedKeys() {
result += v
}
return result
}
- A function that will use the old fashion way of creating a data structure to guarantee a map’s iteration order. Here is the code of said function:
// This function uses the classical approach
// of keeping the keys in a separate structure.
func ProcessTotalWithSepDataStructure(m CustomMapType) int {
result := int(0)
sortedKeys := SortMapKeysByNumericalOrder(m)
for _, v := range sortedKeys {
result += m[v]
}
return result
}
With the functions in place, I’ll proceed by writing 2 benchmark tests. These benchmarks will track memory allocation and average time-to-complete per operation (range cycle). The naming convention of a benchmark function is as follows:
BenchmarkXxx
Xxx
is replaced by your actual function name. And I don’t think the tooling will work if the functions aren’t named in this manner. For this post, I’ll have the following benchmark functions:
func BenchmarkProcessTotalWithIter(b *testing.B) {
m := CustomMapType{
"2": 500,
"1": 1000,
"3": 400,
"6": 300,
"0.1": 200,
}
b.ReportAllocs() // Include memory allocation statistics
for i := 0; i < b.N; i++ {
ProcessTotalWithIter(m)
}
}
func BenchmarkProcessTotalWithSepDataStructure(b *testing.B) {
m := CustomMapType{
"2": 500,
"1": 1000,
"3": 400,
"6": 300,
"0.1": 200,
}
b.ReportAllocs() // Include memory allocation statistics
for i := 0; i < b.N; i++ {
ProcessTotalWithSepDataStructure(m)
}
}
With the benchmark code in place, I can begin the test.
Running the Test
To run the benchmark functions, I’ll run the go test
command and add the following flags:
-bench=.
: Setting this flag’s value to.
will run all of the benchmark functions in the package.-benchmem
: This flag will add memory allocation statistics to the benchmark results.-benchtime=2s
: This specifies the duration each benchmark should run. In this case, I’ve set it to 2 seconds because I want to see how many times the given functions can run. The function that “runs more” can be deemed the more efficient one.
One odd thing I noticed was that the benchmark results would change based on the flags supplied to this command.
Here is the command I plan on running in the terminal:
go test -bench=. -benchmem -benchtime=2s
And.. Here are the results:
goos: linux
goarch: amd64
pkg: itermedium
cpu: [REDACTED FOR SECURITY]
BenchmarkProcessTotalWithIter-4 855826 2652 ns/op 315 B/op 9 allocs/op
BenchmarkProcessTotalWithSepDataStructure-4 1000000 2571 ns/op 315 B/op 10 allocs/op
PASS
ok itermedium 4.912s
Here is another result:
goos: linux
goarch: amd64
pkg: itermedium
cpu: [REDACTED FOR SECURITY]
BenchmarkProcessTotalWithIter-4 880027 2586 ns/op 315 B/op 10 allocs/op
BenchmarkProcessTotalWithSepDataStructure-4 714020 2811 ns/op 315 B/op 10 allocs/op
PASS
ok itermedium 4.357s
Most of the time, BenchmarkProcessTotalWithSepDataStructure
performs better. Which brings me back to my previous statement: all that shines green with the compiler may not be ideal at runtime.
Conclusion
The function with the custom iterator rarely executes faster than the classical approach. Each function allocates the same amount of memory on each run. This is interesting, because choosing which approach to go with boils down to aesthetic reasons.
The difference in time per iteration is typically under 1000 nanoseconds and is often negligible. However, in scenarios where performance optimization is critical, even minor differences may accumulate over millions of requests.
For most practical cases, the decision can be guided by code readability and maintainability rather than performance.
Thank you for reading!
Sources
Go 1.23’s Iter package — https://medium.com/@cheikhhseck/go-1-23s-iter-package-f6b44dfb9b7b
Benchmark code and optimized version of program— https://github.com/cheikhsimsol/go_proj/tree/main/iter