perf: replace NthOrEmpty with direct loops in Zip2-Zip9 (#838)

Replace the single interleaved loop calling NthOrEmpty per element
with separate per-slice loops using direct index access. This
eliminates function call overhead (NthOrEmpty → sliceNth → bounds
check) and improves CPU cache locality by processing each input
slice contiguously.

The result slice is zero-initialized by make(), so out-of-bounds
elements (when slices have different lengths) are already zero —
no explicit zero-filling needed.

Benchstat (Apple M3, 6 runs, -cpu=1):

                    │   before    │           after            │
                    │   sec/op    │  sec/op    vs base         │
Zip2_Equal/n_10       65.89n ± 22%  52.35n ± 13%  -20.54% (p=0.002)
Zip2_Equal/n_100      440.6n ± 21%  382.3n ± 13%  -13.22% (p=0.004)
Zip2_Equal/n_1000     4.232µ ± 82%  3.173µ ± 12%  -25.02% (p=0.002)
Zip2_Unequal/n_10     69.35n ± 65%  46.16n ±  1%  -33.43% (p=0.002)
Zip2_Unequal/n_100    461.8n ±101%  293.1n ± 17%  -36.53% (p=0.002)
Zip2_Unequal/n_1000   3.623µ ± 26%  2.301µ ± 17%  -36.49% (p=0.002)
geomean               492.4n        354.3n        -28.05%
This commit is contained in:
Samuel Berthe
2026-03-06 22:57:44 +01:00
committed by GitHub
parent e9ad51b03a
commit d66010f2c1
2 changed files with 165 additions and 61 deletions
+32
View File
@@ -0,0 +1,32 @@
package benchmark
import (
"fmt"
"testing"
"github.com/samber/lo"
)
func BenchmarkZip2_Equal(b *testing.B) {
for _, n := range lengths {
a := genSliceInt(n)
s := genSliceString(n)
b.Run(fmt.Sprintf("n_%d", n), func(b *testing.B) {
for i := 0; i < b.N; i++ {
lo.Zip2(a, s)
}
})
}
}
func BenchmarkZip2_Unequal(b *testing.B) {
for _, n := range lengths {
a := genSliceInt(n)
s := genSliceString(n / 2)
b.Run(fmt.Sprintf("n_%d", n), func(b *testing.B) {
for i := 0; i < b.N; i++ {
lo.Zip2(a, s)
}
})
}
}
+133 -61
View File
@@ -101,13 +101,15 @@ func Unpack9[A, B, C, D, E, F, G, H, I any](tuple Tuple9[A, B, C, D, E, F, G, H,
// When collections are different sizes, the Tuple attributes are filled with zero value.
// Play: https://go.dev/play/p/jujaA6GaJTp
func Zip2[A, B any](a []A, b []B) []Tuple2[A, B] {
size := uint(Max([]int{len(a), len(b)}))
size := Max([]int{len(a), len(b)})
result := make([]Tuple2[A, B], size)
for index := uint(0); index < size; index++ {
result[index].A = NthOrEmpty(a, index)
result[index].B = NthOrEmpty(b, index)
for i := range a {
result[i].A = a[i]
}
for i := range b {
result[i].B = b[i]
}
return result
@@ -118,14 +120,18 @@ func Zip2[A, B any](a []A, b []B) []Tuple2[A, B] {
// When collections are different sizes, the Tuple attributes are filled with zero value.
// Play: https://go.dev/play/p/jujaA6GaJTp
func Zip3[A, B, C any](a []A, b []B, c []C) []Tuple3[A, B, C] {
size := uint(Max([]int{len(a), len(b), len(c)}))
size := Max([]int{len(a), len(b), len(c)})
result := make([]Tuple3[A, B, C], size)
for index := uint(0); index < size; index++ {
result[index].A = NthOrEmpty(a, index)
result[index].B = NthOrEmpty(b, index)
result[index].C = NthOrEmpty(c, index)
for i := range a {
result[i].A = a[i]
}
for i := range b {
result[i].B = b[i]
}
for i := range c {
result[i].C = c[i]
}
return result
@@ -136,15 +142,21 @@ func Zip3[A, B, C any](a []A, b []B, c []C) []Tuple3[A, B, C] {
// When collections are different sizes, the Tuple attributes are filled with zero value.
// Play: https://go.dev/play/p/jujaA6GaJTp
func Zip4[A, B, C, D any](a []A, b []B, c []C, d []D) []Tuple4[A, B, C, D] {
size := uint(Max([]int{len(a), len(b), len(c), len(d)}))
size := Max([]int{len(a), len(b), len(c), len(d)})
result := make([]Tuple4[A, B, C, D], size)
for index := uint(0); index < size; index++ {
result[index].A = NthOrEmpty(a, index)
result[index].B = NthOrEmpty(b, index)
result[index].C = NthOrEmpty(c, index)
result[index].D = NthOrEmpty(d, index)
for i := range a {
result[i].A = a[i]
}
for i := range b {
result[i].B = b[i]
}
for i := range c {
result[i].C = c[i]
}
for i := range d {
result[i].D = d[i]
}
return result
@@ -155,16 +167,24 @@ func Zip4[A, B, C, D any](a []A, b []B, c []C, d []D) []Tuple4[A, B, C, D] {
// When collections are different sizes, the Tuple attributes are filled with zero value.
// Play: https://go.dev/play/p/jujaA6GaJTp
func Zip5[A, B, C, D, E any](a []A, b []B, c []C, d []D, e []E) []Tuple5[A, B, C, D, E] {
size := uint(Max([]int{len(a), len(b), len(c), len(d), len(e)}))
size := Max([]int{len(a), len(b), len(c), len(d), len(e)})
result := make([]Tuple5[A, B, C, D, E], size)
for index := uint(0); index < size; index++ {
result[index].A = NthOrEmpty(a, index)
result[index].B = NthOrEmpty(b, index)
result[index].C = NthOrEmpty(c, index)
result[index].D = NthOrEmpty(d, index)
result[index].E = NthOrEmpty(e, index)
for i := range a {
result[i].A = a[i]
}
for i := range b {
result[i].B = b[i]
}
for i := range c {
result[i].C = c[i]
}
for i := range d {
result[i].D = d[i]
}
for i := range e {
result[i].E = e[i]
}
return result
@@ -175,17 +195,27 @@ func Zip5[A, B, C, D, E any](a []A, b []B, c []C, d []D, e []E) []Tuple5[A, B, C
// When collections are different sizes, the Tuple attributes are filled with zero value.
// Play: https://go.dev/play/p/jujaA6GaJTp
func Zip6[A, B, C, D, E, F any](a []A, b []B, c []C, d []D, e []E, f []F) []Tuple6[A, B, C, D, E, F] {
size := uint(Max([]int{len(a), len(b), len(c), len(d), len(e), len(f)}))
size := Max([]int{len(a), len(b), len(c), len(d), len(e), len(f)})
result := make([]Tuple6[A, B, C, D, E, F], size)
for index := uint(0); index < size; index++ {
result[index].A = NthOrEmpty(a, index)
result[index].B = NthOrEmpty(b, index)
result[index].C = NthOrEmpty(c, index)
result[index].D = NthOrEmpty(d, index)
result[index].E = NthOrEmpty(e, index)
result[index].F = NthOrEmpty(f, index)
for i := range a {
result[i].A = a[i]
}
for i := range b {
result[i].B = b[i]
}
for i := range c {
result[i].C = c[i]
}
for i := range d {
result[i].D = d[i]
}
for i := range e {
result[i].E = e[i]
}
for i := range f {
result[i].F = f[i]
}
return result
@@ -196,18 +226,30 @@ func Zip6[A, B, C, D, E, F any](a []A, b []B, c []C, d []D, e []E, f []F) []Tupl
// When collections are different sizes, the Tuple attributes are filled with zero value.
// Play: https://go.dev/play/p/jujaA6GaJTp
func Zip7[A, B, C, D, E, F, G any](a []A, b []B, c []C, d []D, e []E, f []F, g []G) []Tuple7[A, B, C, D, E, F, G] {
size := uint(Max([]int{len(a), len(b), len(c), len(d), len(e), len(f), len(g)}))
size := Max([]int{len(a), len(b), len(c), len(d), len(e), len(f), len(g)})
result := make([]Tuple7[A, B, C, D, E, F, G], size)
for index := uint(0); index < size; index++ {
result[index].A = NthOrEmpty(a, index)
result[index].B = NthOrEmpty(b, index)
result[index].C = NthOrEmpty(c, index)
result[index].D = NthOrEmpty(d, index)
result[index].E = NthOrEmpty(e, index)
result[index].F = NthOrEmpty(f, index)
result[index].G = NthOrEmpty(g, index)
for i := range a {
result[i].A = a[i]
}
for i := range b {
result[i].B = b[i]
}
for i := range c {
result[i].C = c[i]
}
for i := range d {
result[i].D = d[i]
}
for i := range e {
result[i].E = e[i]
}
for i := range f {
result[i].F = f[i]
}
for i := range g {
result[i].G = g[i]
}
return result
@@ -218,19 +260,33 @@ func Zip7[A, B, C, D, E, F, G any](a []A, b []B, c []C, d []D, e []E, f []F, g [
// When collections are different sizes, the Tuple attributes are filled with zero value.
// Play: https://go.dev/play/p/jujaA6GaJTp
func Zip8[A, B, C, D, E, F, G, H any](a []A, b []B, c []C, d []D, e []E, f []F, g []G, h []H) []Tuple8[A, B, C, D, E, F, G, H] {
size := uint(Max([]int{len(a), len(b), len(c), len(d), len(e), len(f), len(g), len(h)}))
size := Max([]int{len(a), len(b), len(c), len(d), len(e), len(f), len(g), len(h)})
result := make([]Tuple8[A, B, C, D, E, F, G, H], size)
for index := uint(0); index < size; index++ {
result[index].A = NthOrEmpty(a, index)
result[index].B = NthOrEmpty(b, index)
result[index].C = NthOrEmpty(c, index)
result[index].D = NthOrEmpty(d, index)
result[index].E = NthOrEmpty(e, index)
result[index].F = NthOrEmpty(f, index)
result[index].G = NthOrEmpty(g, index)
result[index].H = NthOrEmpty(h, index)
for i := range a {
result[i].A = a[i]
}
for i := range b {
result[i].B = b[i]
}
for i := range c {
result[i].C = c[i]
}
for i := range d {
result[i].D = d[i]
}
for i := range e {
result[i].E = e[i]
}
for i := range f {
result[i].F = f[i]
}
for i := range g {
result[i].G = g[i]
}
for i := range h {
result[i].H = h[i]
}
return result
@@ -240,21 +296,37 @@ func Zip8[A, B, C, D, E, F, G, H any](a []A, b []B, c []C, d []D, e []E, f []F,
// of the given slices, the second of which contains the second elements of the given slices, and so on.
// When collections are different sizes, the Tuple attributes are filled with zero value.
// Play: https://go.dev/play/p/jujaA6GaJTp
func Zip9[A, B, C, D, E, F, G, H, I any](a []A, b []B, c []C, d []D, e []E, f []F, g []G, h []H, i []I) []Tuple9[A, B, C, D, E, F, G, H, I] {
size := uint(Max([]int{len(a), len(b), len(c), len(d), len(e), len(f), len(g), len(h), len(i)}))
func Zip9[A, B, C, D, E, F, G, H, I any](a []A, b []B, c []C, d []D, e []E, f []F, g []G, h []H, in []I) []Tuple9[A, B, C, D, E, F, G, H, I] {
size := Max([]int{len(a), len(b), len(c), len(d), len(e), len(f), len(g), len(h), len(in)})
result := make([]Tuple9[A, B, C, D, E, F, G, H, I], size)
for index := uint(0); index < size; index++ {
result[index].A = NthOrEmpty(a, index)
result[index].B = NthOrEmpty(b, index)
result[index].C = NthOrEmpty(c, index)
result[index].D = NthOrEmpty(d, index)
result[index].E = NthOrEmpty(e, index)
result[index].F = NthOrEmpty(f, index)
result[index].G = NthOrEmpty(g, index)
result[index].H = NthOrEmpty(h, index)
result[index].I = NthOrEmpty(i, index)
for i := range a {
result[i].A = a[i]
}
for i := range b {
result[i].B = b[i]
}
for i := range c {
result[i].C = c[i]
}
for i := range d {
result[i].D = d[i]
}
for i := range e {
result[i].E = e[i]
}
for i := range f {
result[i].F = f[i]
}
for i := range g {
result[i].G = g[i]
}
for i := range h {
result[i].H = h[i]
}
for i := range in {
result[i].I = in[i]
}
return result