Open
Description
$ cat iter_test.go
package inlineiter
import (
"iter"
"testing"
)
var slice = make([]int, 1000)
func TestIterAllocs(t *testing.T) {
tests := []struct {
name string
body func() int
}{
{
"for-range-func",
func() int {
sum := 0
for v := range Iterator([]int{0, 1, 2}) {
sum += v
}
return sum
},
},
{
"callback-func",
func() int {
sum := 0
Iterator([]int{0, 1, 2})(func(v int) bool {
sum += v
return true
})
return sum
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
allocs := testing.Benchmark(func(b *testing.B) {
for range b.N {
sum := test.body()
if sum != 3 {
t.Errorf("sum %d, want 3", sum)
}
}
}).AllocsPerOp()
if allocs > 0 {
t.Errorf("Layout allocates %d, expected %d", allocs, 0)
}
})
}
}
func Iterator(ints []int) iter.Seq[int] {
return func(yield func(int) bool) {
for _, v := range ints {
if !yield(v) {
break
}
}
}
}
$ go test
PASS
ok seedhammer.com/inlineiter 4.616s
$ tinygo test
--- FAIL: TestIterAllocs (3.08s)
--- FAIL: TestIterAllocs/for-range-func (1.58s)
Layout allocates 5, expected 0
--- FAIL: TestIterAllocs/callback-func (1.50s)
Layout allocates 3, expected 0
FAIL
FAIL seedhammer.com/inlineiter 3.297s
$ tinygo test -opt 2
ok seedhammer.com/inlineiter 3.211s
The idiom for iterator constructors is to return a closure that captures the iterator arguments, if any. It's crucial for the performance of range-over-func that the constructor doesn't incur allocations. Therefore, it seems to me functions that (nearly) immediately return a closure should always be inlined, regardless of optimization level. Or, at the very least, any function that returns a function that can be ranged over.