-
Notifications
You must be signed in to change notification settings - Fork 18.5k
Description
Background
#66056 gave us iterator APIs on reflect.Value, which are great. I went ahead and started using them in our code, which led to simpler code.
What I didn't notice at the time is that I actually added a number of allocations per iteration: https://review.gerrithub.io/c/cue-lang/cue/+/1226673
Below is a set of benchmarks which shows this problem:
package p
import (
"reflect"
"testing"
)
var slice = []string{"a", "b", "c", "d", "e", "f", "g"}
func foreachSlice(i int, elem reflect.Value) {
// we need an integer index here
}
func BenchmarkSliceValueIndex(b *testing.B) {
for b.Loop() {
val := reflect.ValueOf(slice)
for i := range val.Len() {
elem := val.Index(i)
foreachSlice(i, elem)
}
}
}
func BenchmarkSliceValueSeq2WithIndex(b *testing.B) {
for b.Loop() {
val := reflect.ValueOf(slice)
i := 0
for _, elem := range val.Seq2() {
foreachSlice(i, elem)
i++
}
}
}
func BenchmarkSliceValueSeq2(b *testing.B) {
for b.Loop() {
val := reflect.ValueOf(slice)
for ival, elem := range val.Seq2() {
i := int(ival.Int())
foreachSlice(i, elem)
}
}
}
As of go version go1.26-devel_d55ecea9e5 2025-11-14 15:13:00 -0800 linux/amd64, I see:
goos: linux
goarch: amd64
pkg: test
cpu: AMD Ryzen AI 9 HX 370 w/ Radeon 890M
BenchmarkSliceValueIndex-24 66029337 18.00 ns/op 0 B/op 0 allocs/op
BenchmarkSliceValueSeq2WithIndex-24 13339879 86.85 ns/op 104 B/op 5 allocs/op
BenchmarkSliceValueSeq2-24 14808429 76.92 ns/op 88 B/op 5 allocs/op
In other words, the classic integer iteration doesn't allocate and is pretty fast, but both of the two reasonable ways to use Seq2 to iterate over the slice values seem to allocate and are significantly slower.
I also cannot use Seq here, as it iterates over the slice indices, not the elements, much like for i := range slice {...}.
When dealing with reflect.Slice, but also similar "indexed sequence" containers like reflect.Array, and reflect.String, it's common to want to iterate over their elements via iter.Seq2[int, Value], much like for i, v := range slice {...} gives you (int, T). Seq2 is fine, but its iter.Seq2[Value, Value] seems most appropriate for cases where the key can be of any type, such as a map or an iterator func.
Proposal
I propose that we add this new API:
// Elems returns an iter.Seq2[int, Value] that loops over the elements of v.
// v's kind must be Array, Slice, or String
func (v Value) Elems() iter.Seq2[int, Value]
The advantages of this API are two-fold:
- Performance; see above how trying to use
Seq2is slower and allocates more. - Usability; the code sample below is shorter and simpler than the three existing ones above. Or one could even imagine that
foreachSlicecould take theiter.Seq2[int, reflect.Value]directly, as no wrapping is necessary anymore.
func BenchmarkSliceValueSeq2(b *testing.B) {
for b.Loop() {
val := reflect.ValueOf(slice)
for i, elem := range val.Elems() {
foreachSlice(i, elem)
}
}
}
Alternatives
If we improved reflect.Value.Seq2 such that the original benchmarks were all roughly equivalent, then point 1 (performance) would be largely moot. I'm not sure if that is possible. However, I'd argue that point 2 (usability) is still valid; I don't find any of the existing three approaches particularly nice.