Skip to content

Commit

Permalink
tag.go: use slices package Sort functionality
Browse files Browse the repository at this point in the history
Add golang.org/x/exp/slices dependency.

This is allocation-free and has comparable speed to prior approach.
stdlib and slices package Sort functions
also use insertion sort for small inputs,
and as such a local insertion sort optimization
has not been needed since at least 1.18.
  • Loading branch information
extemporalgenome committed Jul 16, 2023
1 parent 0e1a0c4 commit d11a18e
Show file tree
Hide file tree
Showing 4 changed files with 73 additions and 41 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ require (
github.com/mdlayher/genetlink v0.0.0-20190313224034-60417448a851 // indirect
github.com/mdlayher/netlink v0.0.0-20190313131330-258ea9dff42c // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 // indirect
golang.org/x/net v0.7.0 // indirect
golang.org/x/sys v0.5.0 // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw=
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
Expand Down
47 changes: 15 additions & 32 deletions tag.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package stats

import (
"sort"
"sync"

"golang.org/x/exp/slices"
)

// A Tag is a pair of a string key and value set on measures to define the
Expand Down Expand Up @@ -34,39 +35,23 @@ func M(m map[string]string) []Tag {
// TagsAreSorted returns true if the given list of tags is sorted by tag name,
// false otherwise.
func TagsAreSorted(tags []Tag) bool {
if len(tags) > 1 {
min := tags[0].Name
for _, tag := range tags[1:] {
if tag.Name < min {
return false
}
min = tag.Name
}
}
return true
return slices.IsSortedFunc(tags, tagIsLess)
}

// SortTags sorts the slice of tags.
// SortTags sorts and deduplicates tags in-place,
// favoring later elements whenever a tag name duplicate occurs.
// The returned slice may be shorter than the input due to duplicates.
func SortTags(tags []Tag) []Tag {
// Insertion sort since these arrays are very small and allocation is the
// primary enemy of performance here.
if len(tags) >= 20 {
sort.Sort(tagsByName(tags))
} else {
for i := 0; i < len(tags); i++ {
for j := i; j > 0 && tags[j-1].Name > tags[j].Name; j-- {
tags[j], tags[j-1] = tags[j-1], tags[j]
}
}
}
// Stable sort ensures that we have deterministic
// "latest wins" deduplication.
// For 20 or fewer tags, this is as fast as an unstable sort.
slices.SortStableFunc(tags, tagIsLess)

return tags
}

type tagsByName []Tag
func tagIsLess(a, b Tag) bool { return a.Name < b.Name }

func (t tagsByName) Len() int { return len(t) }
func (t tagsByName) Less(i int, j int) bool { return t[i].Name < t[j].Name }
func (t tagsByName) Swap(i int, j int) { t[i], t[j] = t[j], t[i] }

func concatTags(t1 []Tag, t2 []Tag) []Tag {
n := len(t1) + len(t2)
Expand All @@ -89,7 +74,7 @@ func copyTags(tags []Tag) []Tag {
}

type tagsBuffer struct {
tags tagsByName
tags []Tag
}

func (b *tagsBuffer) reset() {
Expand All @@ -100,15 +85,13 @@ func (b *tagsBuffer) reset() {
}

func (b *tagsBuffer) sort() {
if !TagsAreSorted(b.tags) {
SortTags(b.tags)
}
SortTags(b.tags)
}

func (b *tagsBuffer) append(tags ...Tag) {
b.tags = append(b.tags, tags...)
}

var tagsPool = sync.Pool{
New: func() interface{} { return &tagsBuffer{tags: make([]Tag, 0, 8)} },
New: func() any { return &tagsBuffer{tags: make([]Tag, 0, 8)} },
}
64 changes: 55 additions & 9 deletions tag_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"reflect"
"sort"
"testing"

"golang.org/x/exp/slices"
)

func Test_copyTags(t *testing.T) {
Expand Down Expand Up @@ -156,11 +158,24 @@ func BenchmarkTagsOrder(b *testing.B) {
b.Run("TagsAreSorted", func(b *testing.B) {
benchmarkTagsOrder(b, TagsAreSorted)
})
b.Run("sort.IsSorted(tags)", func(b *testing.B) {
benchmarkTagsOrder(b, func(tags []Tag) bool { return sort.IsSorted(tagsByName(tags)) })
b.Run("slices.IsSortedFunc", func(b *testing.B) {
benchmarkTagsOrder(b, func(tags []Tag) bool {
return slices.IsSortedFunc(tags, tagIsLess)
})
})
b.Run("sort.SliceIsSorted", func(b *testing.B) {
benchmarkTagsOrder(b, func(tags []Tag) bool {
return sort.SliceIsSorted(tags, tagIsLessByIndex(tags))
})
})
}

func tagIsLessByIndex(tags []Tag) func(int, int) bool {
return func(i, j int) bool {
return tagIsLess(tags[i], tags[j])
}
}

func benchmarkTagsOrder(b *testing.B, isSorted func([]Tag) bool) {
b.Helper()
b.ReportAllocs()
Expand Down Expand Up @@ -191,12 +206,7 @@ func BenchmarkSortTags_few(b *testing.B) {
{"C", ""},
}

t1 := make([]Tag, len(t0))

for i := 0; i != b.N; i++ {
copy(t1, t0)
SortTags(t1)
}
benchmark_SortTags(b, t0)
}

func BenchmarkSortTags_many(b *testing.B) {
Expand Down Expand Up @@ -224,11 +234,47 @@ func BenchmarkSortTags_many(b *testing.B) {
{"C", ""},
}

benchmark_SortTags(b, t0)
}

func benchmark_SortTags(b *testing.B, t0 []Tag) {
b.Helper()

b.Run("SortTags", func(b *testing.B) {
fn := func(tags []Tag) { SortTags(tags) }
benchmark_SortTags_func(b, t0, fn)
})

b.Run("slices.SortFunc", func(b *testing.B) {
fn := func(tags []Tag) { slices.SortFunc(tags, tagIsLess) }
benchmark_SortTags_func(b, t0, fn)
})

b.Run("slices.SortStableFunc", func(b *testing.B) {
fn := func(tags []Tag) { slices.SortStableFunc(tags, tagIsLess) }
benchmark_SortTags_func(b, t0, fn)
})

b.Run("sort.Slice", func(b *testing.B) {
fn := func(tags []Tag) { sort.Slice(tags, tagIsLessByIndex(tags)) }
benchmark_SortTags_func(b, t0, fn)
})

b.Run("sort.SliceStable", func(b *testing.B) {
fn := func(tags []Tag) { sort.SliceStable(tags, tagIsLessByIndex(tags)) }
benchmark_SortTags_func(b, t0, fn)
})
}

func benchmark_SortTags_func(b *testing.B, t0 []Tag, fn func([]Tag)) {
b.Helper()
b.ReportAllocs()

t1 := make([]Tag, len(t0))

for i := 0; i != b.N; i++ {
copy(t1, t0)
SortTags(t1)
fn(t1)
}
}

Expand Down

0 comments on commit d11a18e

Please sign in to comment.