A radix sorting library for Go (golang)

zermelo Build Status go.dev reference license Go Report Card Sourcegraph

A radix sorting library for Go. Trade memory for speed!

import "github.com/shawnsmithdev/zermelo"

func foo(large []uint64)
    zermelo.Sort(large)
}

About

Zermelo is a sorting library featuring implementations of radix sort. I am especially influenced here by these two articles that describe various optimizations and how to work around the typical limitations of radix sort.

You will generally only want to use zermelo if you won't mind the extra memory used for buffers and your application frequently sorts slices of supported types with at least 256 elements (128 for 32-bit types, somewhat more for floats on ARM). The larger the slices you are sorting, the more benefit you will gain by using zermelo instead of the standard library's in-place comparison sort.

Etymology

Zermelo is named after Ernst Zermelo, who developed the proof for the well-ordering theorem.

Supported Types

  • []float32
  • []float64
  • []int
  • []int32
  • []int64
  • []uint
  • []uint32
  • []uint64

Subpackages

Zermelo provides individual subpackages for each of the supported types. Subpackages have a SortBYOB() method where you can Bring Your Own Buffer (BYOB), for minimizing allocations. Providing a buffer that is smaller than the slice you are sorting will cause a runtime panic.

import "github.com/shawnsmithdev/zermelo/zuint64"

func foo(bar SomeRemoteData)
    data := make([]uint64, REALLY_BIG)
    buffer := make([]uint64, REALLY_BIG)

    while bar.hasMore() {
        bar.Read(data)
        zuint64.SortBYOB(data, buffer)
        doSomething(data)
    }
}

Sorter

A Sorter will reuse buffers created during Sort() calls. This is not thread safe. Buffers are grown as needed at a 25% exponential growth rate. This means if you sort a slice of size n, subsequent calls with slices up to n * 1.25 in length will not cause another buffer allocation. This does not apply to the first allocation, which will make a buffer of the same size as the requested slice. This way, if the slices being sorted do not grow in size, there is no unused buffer space.

import "github.com/shawnsmithdev/zermelo"

func foo(bar [][]uint64) {
    sorter := zermelo.New()
    for _, x := range(bar) {
        sorter.Sort(x)
    }
}

Benchmarks

You can run the benchmark on your own hardware.

go test -v -bench . -benchmem

The benchmark tests two types of slices:

  • []uint64
  • []float64

For each of the tested types, it runs the benchmark on a slice of that type with four sizes:

  • T (tiny) 64
  • S (small) 256
  • M (medium) 1024
  • L (large) 1048576

For each slice type and size, three sorters are benchmarked:

  • GoSort - The standard library sort: sort.Slice() or sort.Float64s
  • ZSort - zermelo.Sort(), does not reuse buffers
  • ZSorter - zermelo.New().Sort(), does reuse buffers

One pass for each sorter is also made against a large, presorted []uint64.

Owner
Shawn Smith
I write go stuff sometimes.
Shawn Smith
Comments
  • v2 - Generics

    v2 - Generics

    If generics, when they are released, turn out to perform well, I'm going to rewrite this library to use them and get the line count way down.

    This issue tracks the research into performance and, if it looks good, actual implementation.

    Don't expect anything here until generics are actually released.

  • unsigned: optimizations to improve performance

    unsigned: optimizations to improve performance

    The changes are 'trivial' in that they don't change any algorithms or structure of the code.

    name               old time/op  new time/op  delta
    ZSortUint64T-4     2.71µs ± 7%  2.70µs ±15%     ~     (p=0.684 n=10+10)
    ZSorterUint64T-4   2.77µs ± 9%  2.69µs ± 5%     ~     (p=0.243 n=10+9)
    ZSortUint64S-4     15.2µs ± 8%  13.2µs ± 5%  -12.98%  (p=0.000 n=10+10)
    ZSorterUint64S-4   11.6µs ± 1%   8.8µs ± 0%  -24.39%  (p=0.000 n=10+8)
    ZSortUint64M-4     48.9µs ± 7%  45.1µs ± 6%   -7.75%  (p=0.000 n=10+8)
    ZSorterUint64M-4   31.6µs ± 0%  28.8µs ± 0%   -8.96%  (p=0.000 n=9+8)
    ZSortUint64L-4     68.2ms ± 0%  62.1ms ± 1%   -8.96%  (p=0.000 n=9+10)
    ZSorterUint64L-4   68.4ms ± 0%  61.9ms ± 0%   -9.45%  (p=0.000 n=10+9)
    ZSortSortedL-4     2.47ms ± 1%  2.41ms ± 1%   -2.51%  (p=0.000 n=10+10)
    ZSorterSortedL-4   1.72ms ± 1%  1.66ms ± 1%   -3.32%  (p=0.000 n=10+9)
    
  • Use sort.Slice instead of sort.Interface?

    Use sort.Slice instead of sort.Interface?

    I've done some benchmark on both. It seems that sort.Slice has 20% performance improvement comparing to sort.Interface.

    However it means stop supporting go 1.7 and earlier versions.

  • The NaN problem

    The NaN problem

    One of the defining characteristics of zermelo is that it supports constraints.Float types, likely a rarity for radix sort libraries given the bit-flipping and type-casting shenanigans you have to do to make it happen. Which means we need to deal with NaN values.

    NaNs are lots of fun. By their own admission, they are not numbers. You can't sort them because you can't even compare them, much less consider their digits by radix. You can only have a policy on what to do with them when present.

    zermelo currently does a whole linear scan through the slice before any other action, looking for NaNs and setting them up front, ahead of all other elements. This only happens in the constraints.Float code. It then does its flip-sort-flip magic on the remainder of the slice. This behavior was chosen as it is also how sort.Float64s() handles NaNs.

    But comes now slices.Sort(), the new generic comparison sort in golang.org/x/exp. I fully expect constraints and slices and map to show up in the stdlib, probably in go1.19 if nothing goes wrong. We shall see, but that is my assumption. I do know it is much faster than the sort package, likely due to less function pointer dereferencing and more inlining, and probably also just a better comparison sort implementation.

    I'm not quite sure if there is a defined behavior at all for NaNs in slices.Sort, but it seems unlikely given is a generic comparison sort on constraints.Ordered. What I am sure of is that it isn't the same as sort.

    Which is all a long way of saying we need an official policy, documented and tested, about what is done with NaNs.

  • v1.5 - Improvements: Missing docs, more tests, benchmarks

    v1.5 - Improvements: Missing docs, more tests, benchmarks

    • coverage is only ~91%: not great, not terrible
    • Most of the new generic functions and interfaces are missing documentation
    • Benchmarks are technically there, but are different than 1.0 and need re-documentation
    • File layout should be reorganized so that generic API and old API are in separate files so that v2 is easy to move to, and after it will be easy to keep 1.x synced indefinitely.
    • Anything else I missed in my haste to make generics happen...

    I may also attempt to get timing in tests to work better on win64. Apparently time.Now() is coarse in win64 and you have to do some kind of goofy system-specific call to get nanos. It's a pita so I may not bother...

  • v2 - Create v2 with only generic interface

    v2 - Create v2 with only generic interface

    Now that generics are here, I should eventually make a v2 of this package that cleans up the API and removes the subpackages.

    I plan to not use the "folder" method, which is to say I'm just going to update the go.mod to v2 and change import strings. If you are using an obsolete go this will break you; you have been warned.

  • Sorted slice detection is incorrect

    Sorted slice detection is incorrect

    The sorted variable will never become true:

    https://github.com/shawnsmithdev/zermelo/blob/71f3d2cee9e588c954bdd6c1ad73de45444ed1a3/zuint32/zuint32.go#L48-L68

  • zint and zuint don't need to use reflection

    zint and zuint don't need to use reflection

    Readme says

    The subpackages zuint and zint must still use reflection as the bit width (32/64) is only available at runtime.

    However this is not correct. Bit width is available at build time and can be defined as constant: https://play.golang.org/p/IfKUhv0agJ

  • Version 2 - Initial Commit

    Version 2 - Initial Commit

    This commit drops all the non-generic, reflection-based code, including all previous subpackages.

    It also renames several functions, and moves the float code into a new subpackage.

    This represents a large change to the API and thus is a new major version.

    This closes #9 .

  • Reduce copied code while keeping performance?

    Reduce copied code while keeping performance?

    The below is a prototype implementation of radix sort for []uint64. It's about 3x slower than the existing implementation. I need to do some profiling on this. If it is even possible to reduce this performance hit to a tolerable amount, it might be worth refactoring the whole library in this style.

    package internal
    
    type radixee struct {
    	len int
    	mask func(int) func(int) byte
    }
    
    type radixer struct {
    	x radixee // to sort
    	y radixee // buffer
    
    	digits int
    	copier func(int, int, bool)
    }
    
    func SortRadixer(r radixer) {
    	if r.x.len > r.y.len {
    		panic("buffer too small")
    	}
    	if r.x.len < 2 {
    		return
    	}
    
    	flip := false
    	var key byte
    	var offset [256]int
    	for digit := 0; digit < r.digits; digit++ {
    		var counts [256]int
    		mask := r.x.mask(digit)
    		for i := 0; i < r.x.len; i++ {
    			key = mask(i)
    			counts[key]++
    		}
    		offset[0] = 0
    		for i := 1; i < len(offset); i++ {
    			offset[i] = offset[i-1] + counts[i-1]
    		}
    		for i := 0; i < r.x.len; i++ {
    			key = mask(i)
    			r.copier(offset[key], i, flip)
    			offset[key]++
    		}
    		r.x, r.y = r.y, r.x
    		flip = !flip
    	}
    }
    
    
    func uint64Copier(x, y []uint64) func(int, int, bool) {
    	return func(dst, src int, flip bool) {
    		if flip {
    			x[dst] = y[src]
    		} else {
    			y[dst] = x[src]
    		}
    	}
    }
    
    func uint64Masker(x []uint64) func(int) func(int) byte {
    	return func(offset int) func(int) byte {
    		bitOffset := uint(offset * 8)
    		keyMask := uint64(0xFF << bitOffset)
    		return func(i int) byte {
    			return byte((x[i] & keyMask) >> bitOffset)
    		}
    	}
    }
    
    func SortUint64V2(x, buffer []uint64) {
    	y := buffer[:len(x)]
    	SortRadixer(radixer{
    		x:      radixee{len: len(x), mask: uint64Masker(x)},
    		y:      radixee{len: len(y), mask: uint64Masker(y)},
    		digits: 8,
    		copier: uint64Copier(x, y),
    	})
    }
    
  • Add support for generics

    Add support for generics

    This commit represents the biggest change to zermelo in years as it add support for go 1.18 generics. This means most of the old subpackages are now obsolete, along with the old Sorter and Sort.

    The tests have been thrown out and rewritten, and for now there's no longer any benchmarks. This will likely be cleaned up over time.

    The intention here is to keep backwards compatability with the old non-generic interface, but there may have been mistakes made here and there. This branch (likely to be called 1.5.x) will strive to keep that API, but there will be a v2 eventually that will drop all that and thereby greatly simplify the public API.

    For now this should be considered relatively rough code, compared to the stability of zermelo up to now, but I've done what I can to test it extensively. Any bug reports are greatly appreciated.

  • Strings

    Strings

    The goal here is to attempt to add SortStrings[S ~string]([]S) and StringSorter[~string]

    • Should be faster than sort.Strings and slices.Sort for large slices
    • Should result in lexicographic sorting (MSD radix sort), such that sort.StringsAreSorted is always true after sorting
    • Possibly allows for v2 Sort function that handles the entire constraints.Ordered type space, assuming I can get reflection and generics to play nice together.

    I tried this once when I first wrote zermelo and it was too slow, but may as well give it another shot.

  • Evolve to in-place sorting

    Evolve to in-place sorting

    Now that histogram and watermark(s) are one (#4), I see the opportunity to further evolve this into (state of the art) in-place sorting.

    Would you entertain a PR or do you want to keep the current algorithm?

golang sorting algorithm and data construction.

data-structures-questions 算法和程序结构是我们学习编程的基础,但是很多的时候,我们很多都是只是在应用,而没有深入的去研究这些,所以自己也在不断的思考和探索,然后分析,学习,总结自己学习的过程,希望可以和大家一起学习和交流下算法! 目录 网络协议 数据结构 算法 数据库 Go

Dec 17, 2022
Golang implementation of Radix trees

go-radix Provides the radix package that implements a radix tree. The package only provides a single Tree implementation, optimized for sparse nodes.

Dec 30, 2022
An immutable radix tree implementation in Golang

go-immutable-radix Provides the iradix package that implements an immutable radix tree. The package only provides a single Tree implementation, optimi

Dec 29, 2022
The simplest sorting algorithm that sorts in quadratic time
The simplest sorting algorithm that sorts in quadratic time

Simplest sort Showcases the simplest sorting algorithm that works in quadratic time. From here. The pseudocode for this algo can be seen below (sorts

Oct 14, 2022
May 15, 2022
Smartsort - A smart sorting algorithm for Go to sort filename containing digits that is not zero padded

smartsort A smart sorting algorithm for Go to sort filename containing digits th

Mar 26, 2022
Adaptive Radix Trees implemented in Go

An Adaptive Radix Tree Implementation in Go This library provides a Go implementation of the Adaptive Radix Tree (ART). Features: Lookup performance s

Dec 30, 2022
A Go implementation of a radix tree, that uses binary searches to speed up insert, retrieve and delete operations on dense trees

radixs A Go implementation of a radix tree, that uses binary searches to speed up insert, retrieve and delete operations on dense trees. This implemen

Feb 14, 2022
Golang string comparison and edit distance algorithms library, featuring : Levenshtein, LCS, Hamming, Damerau levenshtein (OSA and Adjacent transpositions algorithms), Jaro-Winkler, Cosine, etc...

Go-edlib : Edit distance and string comparison library Golang string comparison and edit distance algorithms library featuring : Levenshtein, LCS, Ham

Dec 20, 2022
Golang library for reading and writing Microsoft Excel™ (XLSX) files.
Golang library for reading and writing Microsoft Excel™ (XLSX) files.

Excelize Introduction Excelize is a library written in pure Go providing a set of functions that allow you to write to and read from XLSX / XLSM / XLT

Jan 9, 2023
Golang library for querying and parsing OFX

OFXGo OFXGo is a library for querying OFX servers and/or parsing the responses. It also provides an example command-line client to demonstrate the use

Nov 25, 2022
Go (golang) library for reading and writing XLSX files.

XLSX Introduction xlsx is a library to simplify reading and writing the XML format used by recent version of Microsoft Excel in Go programs. Tutorial

Jan 5, 2023
Library for hashing any Golang interface

recursive-deep-hash Library for hashing any Golang interface Making huge struct comparison fast & easy How to use package main import ( "fmt" "git

Mar 3, 2022
A small flexible merge library in go
A small flexible merge library in go

conjungo A merge utility designed for flexibility and customizability. The library has a single simple point of entry that works out of the box for mo

Dec 27, 2022
Go native library for fast point tracking and K-Nearest queries

Geo Index Geo Index library Overview Splits the earth surface in a grid. At each cell we can store data, such as list of points, count of points, etc.

Dec 3, 2022
Data structure and algorithm library for go, designed to provide functions similar to C++ STL

GoSTL English | 简体中文 Introduction GoSTL is a data structure and algorithm library for go, designed to provide functions similar to C++ STL, but more p

Dec 26, 2022
Zero allocation Nullable structures in one library with handy conversion functions, marshallers and unmarshallers

nan - No Allocations Nevermore Package nan - Zero allocation Nullable structures in one library with handy conversion functions, marshallers and unmar

Dec 20, 2022
A Go library for an efficient implementation of a skip list: https://godoc.org/github.com/MauriceGit/skiplist
A Go library for an efficient implementation of a skip list: https://godoc.org/github.com/MauriceGit/skiplist

Fast Skiplist Implementation This Go-library implements a very fast and efficient Skiplist that can be used as direct substitute for a balanced tree o

Dec 30, 2022
Go Library [DEPRECATED]

Tideland Go Library Description The Tideland Go Library contains a larger set of useful Google Go packages for different purposes. ATTENTION: The cell

Nov 15, 2022