From 9ad1925f0c0b20b6de25dce17c70829a00cc7b00 Mon Sep 17 00:00:00 2001 From: proost Date: Sat, 5 Sep 2026 14:59:19 +0900 Subject: [PATCH] perf: use value type instead of pointer --- frequencies/items_sketch.go | 12 +- frequencies/items_sketch_test.go | 15 ++- frequencies/longs_sketch.go | 10 +- frequencies/longs_sketch_test.go | 8 +- frequencies/row.go | 8 +- frequencies/row_benchmark_test.go | 199 ++++++++++++++++++++++++++++++ 6 files changed, 225 insertions(+), 27 deletions(-) create mode 100644 frequencies/row_benchmark_test.go diff --git a/frequencies/items_sketch.go b/frequencies/items_sketch.go index 211b24b..cf40ce8 100644 --- a/frequencies/items_sketch.go +++ b/frequencies/items_sketch.go @@ -283,7 +283,7 @@ func (i *ItemsSketch[C]) frequencies(item C) (est, lower, upper int64, err error // threshold to include items in the result list // errorType determines whether no false positives or no false negatives are desired. // an array of frequent items -func (i *ItemsSketch[C]) GetFrequentItemsWithThreshold(threshold int64, errorType errorType) ([]*RowItem[C], error) { +func (i *ItemsSketch[C]) GetFrequentItemsWithThreshold(threshold int64, errorType errorType) ([]RowItem[C], error) { finalThreshold := i.GetMaximumError() if threshold > finalThreshold { finalThreshold = threshold @@ -291,12 +291,12 @@ func (i *ItemsSketch[C]) GetFrequentItemsWithThreshold(threshold int64, errorTyp return i.sortItems(finalThreshold, errorType) } -// GetFrequentItems returns an array of Row that include frequent items, estimates, upper and +// GetFrequentItems returns an array of RowItem that include frequent items, estimates, upper and // lower bounds given an ErrorCondition and the default threshold. // This is the same as GetFrequentItemsWithThreshold(getMaximumError(), errorType) // // errorType determines whether no false positives or no false negatives are desired. -func (i *ItemsSketch[C]) GetFrequentItems(errorType errorType) ([]*RowItem[C], error) { +func (i *ItemsSketch[C]) GetFrequentItems(errorType errorType) ([]RowItem[C], error) { return i.sortItems(i.GetMaximumError(), errorType) } @@ -514,8 +514,8 @@ func (i *ItemsSketch[C]) String() string { return sb.String() } -func (i *ItemsSketch[C]) sortItems(threshold int64, errorType errorType) ([]*RowItem[C], error) { - rowList := make([]*RowItem[C], 0) +func (i *ItemsSketch[C]) sortItems(threshold int64, errorType errorType) ([]RowItem[C], error) { + rowList := make([]RowItem[C], 0, i.hashMap.numActive) iter := i.hashMap.iterator() if errorType == ErrorTypeEnum.NoFalseNegatives { for iter.next() { @@ -541,7 +541,7 @@ func (i *ItemsSketch[C]) sortItems(threshold int64, errorType errorType) ([]*Row } } - slices.SortFunc(rowList, func(a, b *RowItem[C]) int { + slices.SortFunc(rowList, func(a, b RowItem[C]) int { if a.est > b.est { return -1 } diff --git a/frequencies/items_sketch_test.go b/frequencies/items_sketch_test.go index d4d3db4..e4745f6 100644 --- a/frequencies/items_sketch_test.go +++ b/frequencies/items_sketch_test.go @@ -399,8 +399,7 @@ func TestMisc(t *testing.T) { assert.Equal(t, row.GetUpperBound(), int64(1)) s := row.String() t.Log(s) - var nullRow *RowItem[int64] - assert.NotEqual(t, row, nullRow) + assert.NotEqual(t, RowItem[int64]{}, row) } func TestToString(t *testing.T) { @@ -417,7 +416,7 @@ func TestFrequentItems1(t *testing.T) { rows, err := fis.GetFrequentItems(ErrorTypeEnum.NoFalsePositives) assert.NoError(t, err) row := rows[0] - assert.NotNil(t, row) + assert.NotEqual(t, RowItem[int64]{}, row) assert.Equal(t, row.GetItem(), int64(1)) assert.Equal(t, row.GetEstimate(), int64(1)) assert.Equal(t, row.GetUpperBound(), int64(1)) @@ -610,8 +609,8 @@ func benchmarkItemsSketchToSlice[C comparable]( } } -func generateTestRowItems(n int) []*RowItem[string] { - items := make([]*RowItem[string], n) +func generateTestRowItems(n int) []RowItem[string] { + items := make([]RowItem[string], n) for i := 0; i < n; i++ { est := rand.Int63n(10000) items[i] = newRowItem( @@ -634,7 +633,7 @@ func BenchmarkSortSliceRow(b *testing.B) { b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { - rowList := make([]*RowItem[string], len(original)) + rowList := make([]RowItem[string], len(original)) copy(rowList, original) sort.Slice(rowList, func(i, j int) bool { @@ -655,10 +654,10 @@ func BenchmarkSlicesSortFuncRow(b *testing.B) { b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { - rowList := make([]*RowItem[string], len(original)) + rowList := make([]RowItem[string], len(original)) copy(rowList, original) - slices.SortFunc(rowList, func(a, b *RowItem[string]) int { + slices.SortFunc(rowList, func(a, b RowItem[string]) int { if a.est > b.est { return -1 } diff --git a/frequencies/longs_sketch.go b/frequencies/longs_sketch.go index 7c0c924..152acf6 100644 --- a/frequencies/longs_sketch.go +++ b/frequencies/longs_sketch.go @@ -342,7 +342,7 @@ func (s *LongsSketch) GetUpperBound(item int64) (int64, error) { // threshold to include items in the result list // errorType determines whether no false positives or no false negatives are desired. // an array of frequent items -func (s *LongsSketch) GetFrequentItemsWithThreshold(threshold int64, errorType errorType) ([]*Row, error) { +func (s *LongsSketch) GetFrequentItemsWithThreshold(threshold int64, errorType errorType) ([]Row, error) { finalThreshold := s.GetMaximumError() if threshold > finalThreshold { finalThreshold = threshold @@ -355,7 +355,7 @@ func (s *LongsSketch) GetFrequentItemsWithThreshold(threshold int64, errorType e // This is the same as GetFrequentItemsWithThreshold(getMaximumError(), errorType) // // errorType determines whether no false positives or no false negatives are desired. -func (s *LongsSketch) GetFrequentItems(errorType errorType) ([]*Row, error) { +func (s *LongsSketch) GetFrequentItems(errorType errorType) ([]Row, error) { return s.sortItems(s.GetMaximumError(), errorType) } @@ -555,8 +555,8 @@ func (s *LongsSketch) String() string { return sb.String() } -func (s *LongsSketch) sortItems(threshold int64, errorType errorType) ([]*Row, error) { - rowList := make([]*Row, 0) +func (s *LongsSketch) sortItems(threshold int64, errorType errorType) ([]Row, error) { + rowList := make([]Row, 0, s.hashMap.numActive) iter := s.hashMap.iterator() if errorType == ErrorTypeEnum.NoFalseNegatives { for iter.next() { @@ -582,7 +582,7 @@ func (s *LongsSketch) sortItems(threshold int64, errorType errorType) ([]*Row, e } } - slices.SortFunc(rowList, func(a, b *Row) int { + slices.SortFunc(rowList, func(a, b Row) int { if a.est > b.est { return -1 } diff --git a/frequencies/longs_sketch_test.go b/frequencies/longs_sketch_test.go index 03edcb3..4b6fe11 100644 --- a/frequencies/longs_sketch_test.go +++ b/frequencies/longs_sketch_test.go @@ -23,8 +23,9 @@ import ( "strings" "testing" - "github.com/apache/datasketches-go/internal" "github.com/stretchr/testify/assert" + + "github.com/apache/datasketches-go/internal" ) func TestFrequentItemsStringSerial(t *testing.T) { @@ -533,9 +534,8 @@ func printRows(t *testing.T, fls *LongsSketch, errorType errorType) { s2 := row.String() fmt.Println(s2) } - if len(rows) > 0 { //check equals null case - var nullRow *Row - assert.NotEqual(t, rows[0], nullRow) + if len(rows) > 0 { + assert.NotEqual(t, Row{}, rows[0]) } } diff --git a/frequencies/row.go b/frequencies/row.go index c13e4a4..f864682 100644 --- a/frequencies/row.go +++ b/frequencies/row.go @@ -35,8 +35,8 @@ type RowItem[C comparable] struct { lb int64 } -func newRow(item int64, estimate int64, ub int64, lb int64) *Row { - return &Row{ +func newRow(item int64, estimate int64, ub int64, lb int64) Row { + return Row{ item: item, est: estimate, ub: ub, @@ -44,8 +44,8 @@ func newRow(item int64, estimate int64, ub int64, lb int64) *Row { } } -func newRowItem[C comparable](item C, estimate int64, ub int64, lb int64) *RowItem[C] { - return &RowItem[C]{ +func newRowItem[C comparable](item C, estimate int64, ub int64, lb int64) RowItem[C] { + return RowItem[C]{ item: item, est: estimate, ub: ub, diff --git a/frequencies/row_benchmark_test.go b/frequencies/row_benchmark_test.go new file mode 100644 index 0000000..de7b280 --- /dev/null +++ b/frequencies/row_benchmark_test.go @@ -0,0 +1,199 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package frequencies + +import ( + "math/bits" + "slices" + "strconv" + "testing" + + "github.com/apache/datasketches-go/common" +) + +// Sinks, to keep the compiler from eliding the benchmarked work. +var ( + benchRowPointerSink []*Row + benchRowValueSink []Row + benchRowItemPointerSink []*RowItem[string] + benchRowItemValueSink []RowItem[string] +) + +func benchMapSizeFor(n int) int { + need := (n*4 + 2) / 3 + if need < 1<<_LG_MIN_MAP_SIZE { + return 1 << _LG_MIN_MAP_SIZE + } + return 1 << bits.Len(uint(need-1)) +} + +func newBenchLongsSketch(b *testing.B, n int) *LongsSketch { + b.Helper() + sk, err := NewLongsSketchWithMaxMapSize(benchMapSizeFor(n)) + if err != nil { + b.Fatal(err) + } + for i := 0; i < n; i++ { + if err := sk.UpdateMany(int64(i), int64(i%97)+1); err != nil { + b.Fatal(err) + } + } + if got := sk.GetNumActiveItems(); got != n { + b.Fatalf("want %d active items, got %d (sketch purged)", n, got) + } + return sk +} + +func newBenchItemsSketch(b *testing.B, n int) *ItemsSketch[string] { + b.Helper() + sk, err := NewFrequencyItemsSketchWithMaxMapSize[string]( + benchMapSizeFor(n), common.ItemSketchStringHasher{}, common.ItemSketchStringSerDe{}) + if err != nil { + b.Fatal(err) + } + for i := 0; i < n; i++ { + if err := sk.UpdateMany("item"+strconv.Itoa(i), int64(i%97)+1); err != nil { + b.Fatal(err) + } + } + if got := sk.GetNumActiveItems(); got != n { + b.Fatalf("want %d active items, got %d (sketch purged)", n, got) + } + return sk +} + +func keep(errorType errorType, lb, ub, threshold int64) bool { + if errorType == ErrorTypeEnum.NoFalseNegatives { + return ub >= threshold + } + return lb >= threshold +} + +// sortItemsPointerLong is the pre-change LongsSketch.sortItems, kept for comparison. +func sortItemsPointerLong(s *LongsSketch, threshold int64, errorType errorType) ([]*Row, error) { + rowList := make([]*Row, 0) + iter := s.hashMap.iterator() + for iter.next() { + est, lb, ub, err := s.frequencies(iter.getKey()) + if err != nil { + return nil, err + } + if keep(errorType, lb, ub, threshold) { + rowList = append(rowList, &Row{item: iter.getKey(), est: est, ub: ub, lb: lb}) + } + } + slices.SortFunc(rowList, func(a, b *Row) int { + if a.est > b.est { + return -1 + } + if a.est < b.est { + return 1 + } + return 0 + }) + return rowList, nil +} + +// sortItemsPointerItem is the pre-change ItemsSketch.sortItems, kept for comparison. +func sortItemsPointerItem[C comparable](i *ItemsSketch[C], threshold int64, errorType errorType) ([]*RowItem[C], error) { + rowList := make([]*RowItem[C], 0) + iter := i.hashMap.iterator() + for iter.next() { + est, lb, ub, err := i.frequencies(iter.getKey()) + if err != nil { + return nil, err + } + if keep(errorType, lb, ub, threshold) { + rowList = append(rowList, &RowItem[C]{item: iter.getKey(), est: est, ub: ub, lb: lb}) + } + } + slices.SortFunc(rowList, func(a, b *RowItem[C]) int { + if a.est > b.est { + return -1 + } + if a.est < b.est { + return 1 + } + return 0 + }) + return rowList, nil +} + +var benchRowSizes = []int{10, 100, 1000, 10000} + +func benchLongs(b *testing.B, run func(*LongsSketch, int64) error) { + for _, size := range benchRowSizes { + b.Run("size="+strconv.Itoa(size), func(b *testing.B) { + sk := newBenchLongsSketch(b, size) + threshold := sk.GetMaximumError() + b.ReportAllocs() + for b.Loop() { + if err := run(sk, threshold); err != nil { + b.Fatal(err) + } + } + }) + } +} + +func benchItems(b *testing.B, run func(*ItemsSketch[string], int64) error) { + for _, size := range benchRowSizes { + b.Run("size="+strconv.Itoa(size), func(b *testing.B) { + sk := newBenchItemsSketch(b, size) + threshold := sk.GetMaximumError() + b.ReportAllocs() + for b.Loop() { + if err := run(sk, threshold); err != nil { + b.Fatal(err) + } + } + }) + } +} + +func BenchmarkSortItems_Row_Pointer(b *testing.B) { + benchLongs(b, func(sk *LongsSketch, th int64) error { + rows, err := sortItemsPointerLong(sk, th, ErrorTypeEnum.NoFalseNegatives) + benchRowPointerSink = rows + return err + }) +} + +func BenchmarkSortItems_Row_Value(b *testing.B) { + benchLongs(b, func(sk *LongsSketch, th int64) error { + rows, err := sk.sortItems(th, ErrorTypeEnum.NoFalseNegatives) + benchRowValueSink = rows + return err + }) +} + +func BenchmarkSortItems_RowItem_Pointer(b *testing.B) { + benchItems(b, func(sk *ItemsSketch[string], th int64) error { + rows, err := sortItemsPointerItem(sk, th, ErrorTypeEnum.NoFalseNegatives) + benchRowItemPointerSink = rows + return err + }) +} + +func BenchmarkSortItems_RowItem_Value(b *testing.B) { + benchItems(b, func(sk *ItemsSketch[string], th int64) error { + rows, err := sk.sortItems(th, ErrorTypeEnum.NoFalseNegatives) + benchRowItemValueSink = rows + return err + }) +}