mirror of
https://github.com/tailscale/tailscale.git
synced 2025-05-01 21:21:04 +00:00
types/views: make SliceEqualAnyOrder also do short slice optimization
SliceEqualAnyOrderFunc had an optimization missing from SliceEqualAnyOrder. Now they share the same code and both have the optimization. Updates #14593 Change-Id: I550726e0964fc4006e77bb44addc67be989c131c Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
parent
8ee72cd33c
commit
7d5fe13d27
@ -330,6 +330,12 @@ func SliceEqual[T comparable](a, b Slice[T]) bool {
|
|||||||
return slices.Equal(a.ж, b.ж)
|
return slices.Equal(a.ж, b.ж)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// shortOOOLen (short Out-of-Order length) is the slice length at or
|
||||||
|
// under which we attempt to compare two slices quadratically rather
|
||||||
|
// than allocating memory for a map in SliceEqualAnyOrder and
|
||||||
|
// SliceEqualAnyOrderFunc.
|
||||||
|
const shortOOOLen = 5
|
||||||
|
|
||||||
// SliceEqualAnyOrder reports whether a and b contain the same elements, regardless of order.
|
// SliceEqualAnyOrder reports whether a and b contain the same elements, regardless of order.
|
||||||
// The underlying slices for a and b can be nil.
|
// The underlying slices for a and b can be nil.
|
||||||
func SliceEqualAnyOrder[T comparable](a, b Slice[T]) bool {
|
func SliceEqualAnyOrder[T comparable](a, b Slice[T]) bool {
|
||||||
@ -347,18 +353,15 @@ func SliceEqualAnyOrder[T comparable](a, b Slice[T]) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// count the occurrences of remaining values and compare
|
a, b = a.SliceFrom(diffStart), b.SliceFrom(diffStart)
|
||||||
valueCount := make(map[T]int)
|
cmp := func(v T) T { return v }
|
||||||
for i, n := diffStart, a.Len(); i < n; i++ {
|
|
||||||
valueCount[a.At(i)]++
|
// For a small number of items, avoid the allocation of a map and just
|
||||||
valueCount[b.At(i)]--
|
// do the quadratic thing.
|
||||||
|
if a.Len() <= shortOOOLen {
|
||||||
|
return unorderedSliceEqualAnyOrderSmall(a, b, cmp)
|
||||||
}
|
}
|
||||||
for _, count := range valueCount {
|
return unorderedSliceEqualAnyOrder(a, b, cmp)
|
||||||
if count != 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SliceEqualAnyOrderFunc reports whether a and b contain the same elements,
|
// SliceEqualAnyOrderFunc reports whether a and b contain the same elements,
|
||||||
@ -382,29 +385,66 @@ func SliceEqualAnyOrderFunc[T any, V comparable](a, b Slice[T], cmp func(T) V) b
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a, b = a.SliceFrom(diffStart), b.SliceFrom(diffStart)
|
||||||
// For a small number of items, avoid the allocation of a map and just
|
// For a small number of items, avoid the allocation of a map and just
|
||||||
// do the quadratic thing. We can also only check the items between
|
// do the quadratic thing.
|
||||||
// diffStart and the end.
|
if a.Len() <= shortOOOLen {
|
||||||
nRemain := a.Len() - diffStart
|
return unorderedSliceEqualAnyOrderSmall(a, b, cmp)
|
||||||
const shortOptLen = 5
|
}
|
||||||
if nRemain <= shortOptLen {
|
return unorderedSliceEqualAnyOrder(a, b, cmp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// unorderedSliceEqualAnyOrder reports whether a and b contain the same elements
|
||||||
|
// using a map. The cmp function maps from a T slice element to a comparable
|
||||||
|
// value.
|
||||||
|
func unorderedSliceEqualAnyOrder[T any, V comparable](a, b Slice[T], cmp func(T) V) bool {
|
||||||
|
if a.Len() != b.Len() {
|
||||||
|
panic("internal error")
|
||||||
|
}
|
||||||
|
if a.Len() == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
m := make(map[V]int)
|
||||||
|
for i := range a.Len() {
|
||||||
|
m[cmp(a.At(i))]++
|
||||||
|
m[cmp(b.At(i))]--
|
||||||
|
}
|
||||||
|
for _, count := range m {
|
||||||
|
if count != 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// unorderedSliceEqualAnyOrderSmall reports whether a and b (which must be the
|
||||||
|
// same length, and shortOOOLen or shorter) contain the same elements (using cmp
|
||||||
|
// to map from T to a comparable value) in some order.
|
||||||
|
//
|
||||||
|
// This is the quadratic-time implementation for small slices that doesn't
|
||||||
|
// allocate.
|
||||||
|
func unorderedSliceEqualAnyOrderSmall[T any, V comparable](a, b Slice[T], cmp func(T) V) bool {
|
||||||
|
if a.Len() != b.Len() || a.Len() > shortOOOLen {
|
||||||
|
panic("internal error")
|
||||||
|
}
|
||||||
|
|
||||||
// These track which elements in a and b have been matched, so
|
// These track which elements in a and b have been matched, so
|
||||||
// that we don't treat arrays with differing number of
|
// that we don't treat arrays with differing number of
|
||||||
// duplicate elements as equal (e.g. [1, 1, 2] and [1, 2, 2]).
|
// duplicate elements as equal (e.g. [1, 1, 2] and [1, 2, 2]).
|
||||||
var aMatched, bMatched [shortOptLen]bool
|
var aMatched, bMatched [shortOOOLen]bool
|
||||||
|
|
||||||
// Compare each element in a to each element in b
|
// Compare each element in a to each element in b
|
||||||
for i := range nRemain {
|
for i := range a.Len() {
|
||||||
av := cmp(a.At(i + diffStart))
|
av := cmp(a.At(i))
|
||||||
found := false
|
found := false
|
||||||
for j := range nRemain {
|
for j := range a.Len() {
|
||||||
// Skip elements in b that have already been
|
// Skip elements in b that have already been
|
||||||
// used to match an item in a.
|
// used to match an item in a.
|
||||||
if bMatched[j] {
|
if bMatched[j] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
bv := cmp(b.At(j + diffStart))
|
bv := cmp(b.At(j))
|
||||||
if av == bv {
|
if av == bv {
|
||||||
// Mark these elements as already
|
// Mark these elements as already
|
||||||
// matched, so that a future loop
|
// matched, so that a future loop
|
||||||
@ -422,26 +462,12 @@ func SliceEqualAnyOrderFunc[T any, V comparable](a, b Slice[T], cmp func(T) V) b
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Verify all elements were matched exactly once.
|
// Verify all elements were matched exactly once.
|
||||||
for i := range nRemain {
|
for i := range a.Len() {
|
||||||
if !aMatched[i] || !bMatched[i] {
|
if !aMatched[i] || !bMatched[i] {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// count the occurrences of remaining values and compare
|
|
||||||
valueCount := make(map[V]int)
|
|
||||||
for i, n := diffStart, a.Len(); i < n; i++ {
|
|
||||||
valueCount[cmp(a.At(i))]++
|
|
||||||
valueCount[cmp(b.At(i))]--
|
|
||||||
}
|
|
||||||
for _, count := range valueCount {
|
|
||||||
if count != 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -231,6 +231,83 @@ func TestSliceEqualAnyOrderFunc(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSliceEqualAnyOrderAllocs(t *testing.T) {
|
||||||
|
ss := func(s ...string) Slice[string] { return SliceOf(s) }
|
||||||
|
cmp := func(s string) string { return s }
|
||||||
|
|
||||||
|
t.Run("no-allocs-short-unordered", func(t *testing.T) {
|
||||||
|
// No allocations for short comparisons
|
||||||
|
short1 := ss("a", "b", "c")
|
||||||
|
short2 := ss("c", "b", "a")
|
||||||
|
if n := testing.AllocsPerRun(1000, func() {
|
||||||
|
if !SliceEqualAnyOrder(short1, short2) {
|
||||||
|
t.Fatal("not equal")
|
||||||
|
}
|
||||||
|
if !SliceEqualAnyOrderFunc(short1, short2, cmp) {
|
||||||
|
t.Fatal("not equal")
|
||||||
|
}
|
||||||
|
}); n > 0 {
|
||||||
|
t.Fatalf("allocs = %v; want 0", n)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("no-allocs-long-match", func(t *testing.T) {
|
||||||
|
long1 := ss("a", "b", "c", "d", "e", "f", "g", "h", "i", "j")
|
||||||
|
long2 := ss("a", "b", "c", "d", "e", "f", "g", "h", "i", "j")
|
||||||
|
|
||||||
|
if n := testing.AllocsPerRun(1000, func() {
|
||||||
|
if !SliceEqualAnyOrder(long1, long2) {
|
||||||
|
t.Fatal("not equal")
|
||||||
|
}
|
||||||
|
if !SliceEqualAnyOrderFunc(long1, long2, cmp) {
|
||||||
|
t.Fatal("not equal")
|
||||||
|
}
|
||||||
|
}); n > 0 {
|
||||||
|
t.Fatalf("allocs = %v; want 0", n)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("allocs-long-unordered", func(t *testing.T) {
|
||||||
|
// We do unfortunately allocate for long comparisons.
|
||||||
|
long1 := ss("a", "b", "c", "d", "e", "f", "g", "h", "i", "j")
|
||||||
|
long2 := ss("c", "b", "a", "e", "d", "f", "g", "h", "i", "j")
|
||||||
|
|
||||||
|
if n := testing.AllocsPerRun(1000, func() {
|
||||||
|
if !SliceEqualAnyOrder(long1, long2) {
|
||||||
|
t.Fatal("not equal")
|
||||||
|
}
|
||||||
|
if !SliceEqualAnyOrderFunc(long1, long2, cmp) {
|
||||||
|
t.Fatal("not equal")
|
||||||
|
}
|
||||||
|
}); n == 0 {
|
||||||
|
t.Fatalf("unexpectedly didn't allocate")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkSliceEqualAnyOrder(b *testing.B) {
|
||||||
|
b.Run("short", func(b *testing.B) {
|
||||||
|
b.ReportAllocs()
|
||||||
|
s1 := SliceOf([]string{"foo", "bar"})
|
||||||
|
s2 := SliceOf([]string{"bar", "foo"})
|
||||||
|
for range b.N {
|
||||||
|
if !SliceEqualAnyOrder(s1, s2) {
|
||||||
|
b.Fatal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
b.Run("long", func(b *testing.B) {
|
||||||
|
b.ReportAllocs()
|
||||||
|
s1 := SliceOf([]string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"})
|
||||||
|
s2 := SliceOf([]string{"c", "b", "a", "e", "d", "f", "g", "h", "i", "j"})
|
||||||
|
for range b.N {
|
||||||
|
if !SliceEqualAnyOrder(s1, s2) {
|
||||||
|
b.Fatal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestSliceEqual(t *testing.T) {
|
func TestSliceEqual(t *testing.T) {
|
||||||
a := SliceOf([]string{"foo", "bar"})
|
a := SliceOf([]string{"foo", "bar"})
|
||||||
b := SliceOf([]string{"foo", "bar"})
|
b := SliceOf([]string{"foo", "bar"})
|
||||||
|
Loading…
x
Reference in New Issue
Block a user