Faster Sorting with Go Generics
eli.thegreenplace.net
eli.thegreenplace.net
I’m being somewhat pedantic, but that clearly depends on the application.
The truth will set you free.
Simply, of course, we can begin with why it should be that data structures have operations in common -- rather than, say, having each their own specific versions.
2 + 2 == 4
"Hello" + "World" == "Hello World"
[2, 2] + [3, 4] == [2, 2, 3, 4]
Should we bother reusing `+` for this? Why not, 2 intPlus 2 == 4
"Hello" strPlus "World" == "Hello World"
[2, 2] arrayPlus [3, 4] == [2, 2, 3, 4]
Well: the polymorphism `+` allows us to express a common idea, that of "appending". For each of these specific types: int, string, array we can speak in the application-domain of "appending" whilst in the programming domain of "+"ing if we introduce an interface for `+`, that of "appendable", interface Appendable[A] {
A + A -> A
A + 0 -> A
}
This interface allows us to use `+` generically in a principled way, the technical name for Appendable is `Monoid`, but i prefer Appendable (incidentally, Monad is `Sequencable` or `Nestable`).Polymorphism is just the ability to speak as generically in the programming-domain as we speak in the application domain, ie., to use the double-meanings of ordinary thinking in programming.
Integer addition is a magma, semigroup, monoid, and group. It's _also_ a commutative version of each of those structures, but we can still definitely use it with functions only expecting multiplicative structures just fine.
The issue I take with using + for a multiplicative monoid like string concatenation is that readers of code should expect it to be commutative. Using x or * is probably better, using ⓧ is mathematically nice but a pain to type, so something like ++, <>, .., or some other new operator just for concatenation is imo best.
In python, where I take the example, `+` performs that role,
[a() + a() for a in (str, bool, int, list, tuple)]
etc.Except about 95% of the arrays I sort aren't comparable with just <. It's less clear how the unmentioned sort.Slice() will improve.
name old time/op new time/op delta
SortStructs-8 18.6ms ± 2% 15.9ms ± 3% -14.43% (p=0.000 n=10+10)
14.4% is the speedup of the generic version.The article explains why this happens (towards the end).
However, the article points out this will likely change in the future, since the compiler now has enough information to do so. The Go team made a good call imo resisting to optimize this. Now, they can observe how generics are used in the global Go community and then optimize using real world code.
In fact, optimize is an understatement, because dyn dispatch vs monomorphization is a trade-off between compilation time + binary size vs speed (as C++ and Rust programmers know too well). Optimizing early based on microbenchmarks would likely have given an advantage in favor of monomorphization, but may not be suitable for average-to-large binaries in the a generic-heavy future. I assume the team wants to find a good balance for the 99%, and avoid compiler flags, hint-syntax, profile guided optimizations and so on if they can..
Sorting or ordering?
Using a custom comparator is covered later on, and shows that the ordering function (the custom comparator) is not inlined because of how Go 1.18 groups GC shapes.
However an article from a few days ago (https://planetscale.com/blog/generics-can-make-your-go-code-...) showed that it can be made to work, in some cases, by parametrising the function on the callback. This leads to the sorting function being monomorphised on the callback, and thus the callback (likely) getting inlined.
Why choose? With generics we can have all three!