From 2b4b3933a613e3c3257c2d23031a8074f39357ae Mon Sep 17 00:00:00 2001 From: Sebastien Binet Date: Mon, 4 Apr 2022 17:21:41 +0200 Subject: [PATCH] sliceop{,/f64s}: introduce generics sliceop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move and generalize code from sliceop/f64s into sliceop. name old time/op new time/op delta Take/Len=2-8 2.69ns ± 1% 2.71ns ± 2% +0.75% (p=0.043 n=18+18) Take/Len=4-8 3.18ns ± 1% 3.23ns ± 3% +1.27% (p=0.005 n=19+20) Take/Len=8-8 3.83ns ± 1% 3.73ns ± 1% -2.69% (p=0.000 n=20+19) Take/Len=128-8 44.1ns ± 4% 35.9ns ± 2% -18.61% (p=0.000 n=20+20) Take/Len=1024-8 337ns ± 2% 280ns ± 6% -16.73% (p=0.000 n=19+20) Take/Len=1048576-8 1.37ms ± 3% 1.31ms ± 5% -3.97% (p=0.000 n=19+20) name old alloc/op new alloc/op delta Take/Len=2-8 0.00B 0.00B ~ (all equal) Take/Len=4-8 0.00B 0.00B ~ (all equal) Take/Len=8-8 0.00B 0.00B ~ (all equal) Take/Len=128-8 0.00B 0.00B ~ (all equal) Take/Len=1024-8 0.00B 0.00B ~ (all equal) Take/Len=1048576-8 0.00B 0.00B ~ (all equal) name old allocs/op new allocs/op delta Take/Len=2-8 0.00 0.00 ~ (all equal) Take/Len=4-8 0.00 0.00 ~ (all equal) Take/Len=8-8 0.00 0.00 ~ (all equal) Take/Len=128-8 0.00 0.00 ~ (all equal) Take/Len=1024-8 0.00 0.00 ~ (all equal) Take/Len=1048576-8 0.00 0.00 ~ (all equal) Updates go-hep/hep#868. --- sliceop/f64s/f64s.go | 86 ++------------------ sliceop/f64s/f64s_test.go | 7 ++ sliceop/sliceop.go | 119 +++++++++++++++++++++++++++ sliceop/sliceop_example_test.go | 51 ++++++++++++ sliceop/sliceop_test.go | 140 ++++++++++++++++++++++++++++++++ 5 files changed, 322 insertions(+), 81 deletions(-) create mode 100644 sliceop/sliceop.go create mode 100644 sliceop/sliceop_example_test.go create mode 100644 sliceop/sliceop_test.go diff --git a/sliceop/f64s/f64s.go b/sliceop/f64s/f64s.go index b6b456b52..37365f39b 100644 --- a/sliceop/f64s/f64s.go +++ b/sliceop/f64s/f64s.go @@ -5,33 +5,13 @@ // Package f64s provides common operations on float64 slices. package f64s -import ( - "fmt" -) - -var ( - errLength = fmt.Errorf("f64s: length mismatch") - errSortedIndices = fmt.Errorf("f64s: indices not sorted") - errDuplicateIndices = fmt.Errorf("f64s: duplicate indices") -) +import "go-hep.org/x/hep/sliceop" // Filter creates a slice with all the elements x_i of src for which f(x_i) is true. // Filter uses dst as work buffer, storing elements at the start of the slice. // Filter clears dst if a slice is passed, and allocates a new slice if dst is nil. func Filter(dst, src []float64, f func(v float64) bool) []float64 { - - if dst == nil { - dst = make([]float64, 0, len(src)) - } - - dst = dst[:0] - for _, x := range src { - if f(x) { - dst = append(dst, x) - } - } - - return dst + return sliceop.Filter(dst, src, f) } // Map creates a slice with all the elements f(x_i) where x_i are elements from src. @@ -39,38 +19,14 @@ func Filter(dst, src []float64, f func(v float64) bool) []float64 { // Map allocates a new slice if dst is nil. // Map will panic if the lengths of src and dst differ. func Map(dst, src []float64, f func(v float64) float64) []float64 { - - if dst == nil { - dst = make([]float64, len(src)) - } - - if len(src) != len(dst) { - panic(errLength) - } - - for i, x := range src { - dst[i] = f(x) - } - return dst + return sliceop.Map(dst, src, f) } // Find creates a slice with all indices corresponding to elements for which f(x) is true. // Find uses dst as work buffer, storing indices at the start of the slice. // Find clears dst if a slice is passed, and allocates a new slice if dst is nil. func Find(dst []int, src []float64, f func(v float64) bool) []int { - - if dst == nil { - dst = make([]int, 0, len(src)) - } - - dst = dst[:0] - for i, x := range src { - if f(x) { - dst = append(dst, i) - } - } - - return dst + return sliceop.Find(dst, src, f) } // Take creates a sub-slice of src with all elements indiced by the provided indices. @@ -80,37 +36,5 @@ func Find(dst []int, src []float64, f func(v float64) bool) []int { // Take will panic if length of indices is larger than length of src. // Take will panic if length of indices is different from length of dst. func Take(dst, src []float64, indices []int) []float64 { - - if len(indices) > len(src) { - panic(errLength) - } - - if dst == nil { - dst = make([]float64, len(indices)) - } - - if len(dst) != len(indices) { - panic(errLength) - } - - if len(indices) == 0 { - return dst - } - - dst[0] = src[indices[0]] - for i := 1; i < len(indices); i++ { - v0 := indices[i-1] - v1 := indices[i] - switch { - case v0 < v1: - // ok. - case v0 == v1: - panic(errDuplicateIndices) - case v0 > v1: - panic(errSortedIndices) - } - dst[i] = src[v1] - } - - return dst + return sliceop.Take(dst, src, indices) } diff --git a/sliceop/f64s/f64s_test.go b/sliceop/f64s/f64s_test.go index 812d12dd6..6ee076ca3 100644 --- a/sliceop/f64s/f64s_test.go +++ b/sliceop/f64s/f64s_test.go @@ -27,12 +27,19 @@ func panics(t *testing.T, want error) func() { } func TestMap(t *testing.T) { + var errLength = fmt.Errorf("sliceop: length mismatch") defer panics(t, errLength)() _ = Map(make([]float64, 3), make([]float64, 5), nil) } func TestTake(t *testing.T) { + var ( + errLength = fmt.Errorf("sliceop: length mismatch") + errSortedIndices = fmt.Errorf("sliceop: indices not sorted") + errDuplicateIndices = fmt.Errorf("sliceop: duplicate indices") + ) + for _, tc := range []struct { dst, src []float64 inds []int diff --git a/sliceop/sliceop.go b/sliceop/sliceop.go new file mode 100644 index 000000000..4a73530fe --- /dev/null +++ b/sliceop/sliceop.go @@ -0,0 +1,119 @@ +// Copyright ©2021 The go-hep Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.18 + +// Package sliceop provides operations on slices not available in the stdlib +// slices package. +package sliceop // import "go-hep.org/x/hep/sliceop" + +import ( + "fmt" +) + +var ( + errLength = fmt.Errorf("sliceop: length mismatch") + errSortedIndices = fmt.Errorf("sliceop: indices not sorted") + errDuplicateIndices = fmt.Errorf("sliceop: duplicate indices") +) + +// Filter creates a slice with all the elements x_i of src for which f(x_i) is true. +// Filter uses dst as work buffer, storing elements at the start of the slice. +// Filter clears dst if a slice is passed, and allocates a new slice if dst is nil. +func Filter[T any](dst, src []T, f func(v T) bool) []T { + + if dst == nil { + dst = make([]T, 0, len(src)) + } + + dst = dst[:0] + for _, x := range src { + if f(x) { + dst = append(dst, x) + } + } + + return dst +} + +// Map creates a slice with all the elements f(x_i) where x_i are elements from src. +// Map uses dst as work buffer, storing elements at the start of the slice. +// Map allocates a new slice if dst is nil. +// Map will panic if the lengths of src and dst differ. +func Map[T, U any](dst []U, src []T, f func(v T) U) []U { + + if dst == nil { + dst = make([]U, len(src)) + } + + if len(src) != len(dst) { + panic(errLength) + } + + for i, x := range src { + dst[i] = f(x) + } + return dst +} + +// Find creates a slice with all indices corresponding to elements for which f(x) is true. +// Find uses dst as work buffer, storing indices at the start of the slice. +// Find clears dst if a slice is passed, and allocates a new slice if dst is nil. +func Find[T any](dst []int, src []T, f func(v T) bool) []int { + + if dst == nil { + dst = make([]int, 0, len(src)) + } + + dst = dst[:0] + for i, x := range src { + if f(x) { + dst = append(dst, i) + } + } + + return dst +} + +// Take creates a sub-slice of src with all elements indiced by the provided indices. +// Take uses dst as work buffer, storing elements at the start of the slice. +// Take clears dst if a slice is passed, and allocates a new slice if dst is nil. +// Take will panic if indices is not sorted or has duplicates. +// Take will panic if length of indices is larger than length of src. +// Take will panic if length of indices is different from length of dst. +func Take[T any](dst, src []T, indices []int) []T { + + if len(indices) > len(src) { + panic(errLength) + } + + if dst == nil { + dst = make([]T, len(indices)) + } + + if len(dst) != len(indices) { + panic(errLength) + } + + if len(indices) == 0 { + return dst + } + + dst[0] = src[indices[0]] + for i := 1; i < len(indices); i++ { + v0 := indices[i-1] + v1 := indices[i] + switch { + case v0 < v1: + // ok. + case v0 == v1: + panic(errDuplicateIndices) + case v0 > v1: + panic(errSortedIndices) + } + dst[i] = src[v1] + } + + return dst +} diff --git a/sliceop/sliceop_example_test.go b/sliceop/sliceop_example_test.go new file mode 100644 index 000000000..4ac1b6a15 --- /dev/null +++ b/sliceop/sliceop_example_test.go @@ -0,0 +1,51 @@ +// Copyright ©2022 The go-hep Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package sliceop_test + +import ( + "fmt" + + f64s "go-hep.org/x/hep/sliceop" +) + +// An example of slice filtering +func ExampleFilter() { + slice := []float64{1, 2, -99, 4, 5, -99, 7} + condition := func(x float64) bool { return x > 0 } + fmt.Println(f64s.Filter(nil, slice, condition)) + + // Output: + // [1 2 4 5 7] +} + +// An example of slice mapping +func ExampleMap() { + slice := []float64{1, 2, -99, 4, 5, -99, 7} + operation := func(x float64) float64 { return x * x } + fmt.Println(f64s.Map(nil, slice, operation)) + + // Output: + // [1 4 9801 16 25 9801 49] +} + +// An example of slice finding +func ExampleFind() { + slice := []float64{1, 2, -99, 4, 5, -99, 7} + condition := func(x float64) bool { return x == -99 } + fmt.Println(f64s.Find(nil, slice, condition)) + + // Output: + // [2 5] +} + +// An example of taking a sub-slice defined by indices +func ExampleTake() { + slice := []float64{1, 2, -99, 4, 5, -99, 7} + indices := []int{2, 5} + fmt.Println(f64s.Take(nil, slice, indices)) + + // Output: + // [-99 -99] +} diff --git a/sliceop/sliceop_test.go b/sliceop/sliceop_test.go new file mode 100644 index 000000000..799d640c5 --- /dev/null +++ b/sliceop/sliceop_test.go @@ -0,0 +1,140 @@ +// Copyright ©2021 The go-hep Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.18 + +package sliceop + +import ( + "fmt" + "math/rand" + "reflect" + "testing" +) + +func panics(t *testing.T, want error) func() { + t.Helper() + return func() { + err := recover() + if err == nil { + t.Fatalf("expected a panic") + } + if got, want := err.(error).Error(), want.Error(); got != want { + t.Fatalf("invalid panic message.\ngot= %v\nwant=%v", + got, want, + ) + } + } +} + +func TestMap(t *testing.T) { + defer panics(t, errLength)() + + _ = Map(make([]float64, 3), make([]float64, 5), nil) +} + +func TestTake(t *testing.T) { + for _, tc := range []struct { + dst, src []float64 + inds []int + want []float64 + panics error + }{ + { + dst: nil, + src: []float64{1, 2, 3}, + inds: []int{1}, + want: []float64{2}, + }, + { + dst: make([]float64, 1), + src: []float64{1, 2, 3}, + inds: []int{1}, + want: []float64{2}, + }, + { + dst: make([]float64, 0), + src: []float64{1, 2, 3}, + inds: []int{}, + want: []float64{}, + }, + { + dst: []float64{}, + src: []float64{1, 2, 3}, + inds: nil, + want: []float64{}, + }, + { + dst: make([]float64, 2), + src: []float64{1, 2, 3}, + inds: []int{1, 2}, + want: []float64{2, 3}, + }, + { + dst: nil, + src: []float64{1, 2, 3}, + inds: []int{1, 0}, + panics: errSortedIndices, + }, + { + dst: nil, + src: []float64{1, 2, 3}, + inds: []int{0, 1, 2, 3}, + panics: errLength, + }, + { + dst: make([]float64, 1), + src: []float64{1, 2, 3}, + inds: []int{0, 1}, + panics: errLength, + }, + { + dst: make([]float64, 4), + src: []float64{1, 2, 3}, + inds: []int{0, 1}, + panics: errLength, + }, + { + dst: nil, + src: []float64{1, 2, 3}, + inds: []int{1, 1}, + panics: errDuplicateIndices, + }, + } { + t.Run("", func(t *testing.T) { + if tc.panics != nil { + defer panics(t, tc.panics)() + } + got := Take(tc.dst, tc.src, tc.inds) + if !reflect.DeepEqual(got, tc.want) { + t.Fatalf("got= %v\nwant=%v", got, tc.want) + } + }) + } +} + +var takeSink []float64 + +func BenchmarkTake(b *testing.B) { + for _, size := range []int{2, 4, 8, 128, 1024, 1024 * 1024} { + b.Run(fmt.Sprintf("Len=%d", size), func(b *testing.B) { + src := make([]float64, size) + ind := make([]int, 0, len(src)) + rnd := rand.New(rand.NewSource(0)) + for i := range src { + src[i] = rnd.Float64() + if rnd.Float64() > 0.5 { + ind = append(ind, i) + } + } + dst := make([]float64, len(ind)) + b.ReportAllocs() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + takeSink = Take(dst, src, ind) + } + }) + } +}