Lomuto's Comeback
dlang.org
dlang.org
https://github.com/SaschaWitt/ips4o
https://arxiv.org/abs/1705.02257
As an example, to sort 10 million random longs on my computer it takes std::sort 766 ms (roughly in line with Andrei's numbers) and ips4o::sort takes 274 ms.
[edit:formatting]
Another benefit of this algorithm is that it is parallizes extremely well.
On my office Xeon E5-2690 machine when using multiple threads the runtime decreases like this
840 ms for std::sort
372 ms for IPS4o sequentially
201 ms for 2 threads
104 ms for 4 threads
53 ms for 8 threads
33 ms for 16 threads
How does it behave with almost-sorted data?
There's also some logic to handle the case where there are a lot of equal elements which also results in faster performance than the completely random case.
That is very strange. I can not reproduce your results in C++, using your code.
On my machine (Threadripper 2950x, 64 bit Windows 10, GCC 10.1.0 from MSYS2 MinGW64) my algorithm performs best and your branchless version ends up slower than your branchy one:
$ g++ -std=c++17 -O3 -DNDEBUG -DLOMUTO lomuto.cpp && a 10000000
min_milliseconds=828.1250
median_milliseconds=890.6250
$ g++ -std=c++17 -O3 -DNDEBUG -DLOMUTO_BRANCHY lomuto.cpp && a 10000000
min_milliseconds=671.8750
median_milliseconds=671.8750
$ g++ -std=c++17 -O3 -DNDEBUG -DPDQSORT lomuto.cpp && a 10000000
min_milliseconds=343.7500
median_milliseconds=343.7500
On an Intel university machine (Xeon E5-2667 v2 @ 3.3GHz, 64 bit Ubuntu 16.04 LTS, GCC 5.4.0) I get: $ g++ -std=c++17 -O3 -DNDEBUG -DLOMUTO lomuto.cpp && ./a.out 10000000
min_milliseconds=514.8051
median_milliseconds=536.1984
$ g++ -std=c++17 -O3 -DNDEBUG -DLOMUTO_BRANCHY lomuto.cpp && ./a.out 10000000
min_milliseconds=688.2471
median_milliseconds=698.9603
$ g++ -std=c++17 -O3 -DNDEBUG -DPDQSORT lomuto.cpp && ./a.out 10000000
min_milliseconds=340.1935
median_milliseconds=344.7432
Maybe AMD or Windows doesn't like your branchless code or perhaps GCC is generating inferior code for AMD/Windows, as at least on Intel/Linux your Lomuto branchless code can beat the branchy code. But pdqsort (which uses branchless Hoare partitioning) consistently beats both.I didn't change the benchmark, I only added pdqsort to yours, my full changes are the simple inclusion of 1 header and 3 lines of code: https://github.com/orlp/lomuto/commit/29381176f49e3588f6882c...
In order for any interested readers to reproduce, simply clone https://github.com/orlp/lomuto and run the above commands.
Lots of people replaced their sorting algorithms with Tim-Sort because runs of alrealdy-sorted data are empirically common (at least in some workloads). I've seen it myself, in use-cases where sort keys change slightly from iteration to iteration in some larger algorithm, and sort items need to be dynamically kept in order. It no doubt also happens when grabbing data from multiple places, each of which is theoretically ordered arbitrarily but is in practice ordered predictably.
So yeah, the author picked the use-cases where this algorithm is going to show in its best light. But that's not bad either, right? Data is often not well-ordered -- if you're counting occurrences of words, and want to get an ordered list (and never mind that you wouldn't use a comparison-based sort for that) you wouldn't expect much order. Likewise sorting any keys/values from a decent hash table.
One worry that I would have applies specifically to C++ and D: sometimes values are big, and copies are expensive. But I guess users can always choose to sort pointers like other languages do, if the standard library uses a copy-heavy, branch-light algorithm.
Then again, maybe there are algorithms letting you compute percentiles without sorting?
I think you may be able to do so probabilistically, which is often good enough? Additionally, one observation about percentiles is that you don't need to sort all of the data; you can just hold on to the top N results in something like a Heap.
There is a 28-step 16-input network there. It admits a direct translation to AVX-512 instructions.
What is usually considered minimal differs from what is best for AVX512. AVX512 is happy to do 16 comparisons at each stage; its efficiency is limited only by the number of stages. Thus, a network that did more comparisons in fewer stages would be faster.
I mean, to the degree that you need to sort the data at all, it contains informational entropy.
Many "presents as sorted" data structures trade space for time by keeping track of the internal sortedness of chunks of the data, between sortation passes. Heck, any insert-optimized data structure (B+ trees; LevelDB's sorted-string-tables; N-ary tries generally) will get most of its performance gains by being able to guarantee, at node split time, that the children of any particular node are sorted relative to one-another.
So, in many cases, by the very fact that you don't already have metadata attached to your data asserting that it's sorted, it's highly likely to have no branch-predictability.
(Couple that to the fact that load-balancing in OLTP distributed systems favors writes to evenly-keyspace-distributed partitioning keys, ala Dynamo, and you'll frequently find that your inputs to an index-like data structure can be guaranteed random.)
If such a pre-check is in play, then the sorting algorithm itself will basically never have the sort of “runs” of sorted values that make some algorithms cheaper. Those “runs” were already plucked out and turned into nodes!
Yeah, I figured heap sort should be the standard because it has a worst case O(n*log(n)) complexity. But it does tend to do more swaps (a smarter implementation can avoid many writes), and the memory access is very cache-unfriendly. In practice it's just not used.
This is only a smart part of the benchmarks I've run because I wanted to drive one point home within a limited space. I've run many tests on various data types and shapes, and Lomuto does better than Hoare on most. (E.g. its improvement on double is even larger than on integrals.)
I also said that sorting is often more complicated than just comparing integers by their values, which you didn't really address except to mention doubles, which missed the point I was making (I was trying to say sorting often needs e.g. satellite information).
In any case though, if you've done other benchmarks, it'd be nice if you could post them on GitHub or something. Maybe I'm missing something.
Alexandrescu's branch-free code in this article is a thing of beauty, in the same sort of rugged way that Stepanov's code is.
https://www.amazon.com/Age-Entanglement-Quantum-Physics-Rebo...
Which is not to imply that I am right and you wrong or vice versa! Simply to observe that people are different.
As I liked the article but was annoyed by that beginning, it’s great for me that the first comment I saw was yours. So thanks for making it.
0. https://www.amazon.com/Cambridge-Quintet-Scientific-Speculat...
Should CPUs include more masking instructions for regular, non-vectorized code as well?
*edit: wording
The problem in question is due to predictive execution in which the CPU has to flush the pipeline on misprediction. This is dominant in desktop PC CPUs. I have no knowledge on what is prevalent in e.g. server or mobile CPUs.
Both are (according to https://en.wikipedia.org/wiki/Speculative_execution) forms of speculative execution.
I'm not sure if eager evaluation is related to the topic at hand and was speaking specifically about speculative execution. Eager evaluation is just what pretty much every language except Haskell (or Haskell-like) does.
The point is that there are at least two ways CPUs can speculatively execute a conditional: Either predict which branch is taken and flush the pipeline if the prediction was wrong, or execute both arms of the conditional and discard the one that was not actually taken. The top-level SIMD comment is about the latter, but most optimization around speculative execution is for the former.
Yes, CPUs do speculative execution. The "flush pipeline on mispredict" kind ("predictive execution"). That is not the same as the "execute both and discard the untaken one" kind ("eager execution"). Your first reply suggests you consider them equivalent when they are not. I just wanted to clear that up.
Also, you can get something very similar even for scalar code as long as your machine has a conditional move operation, which they all have:
/* true path */
a = ...
b = ...
true_result = ...
/* false path */
x = ...
y = ...
false_result = ...
/* final result */
result = (condition ? true_result : false_result)
This works if the two "branches" have no side effects (memory writes, function calls) and cannot raise exceptions. Again, it's very difficult to estimate for compilers (and humans) if it will pay off to run both computations rather than branch.The trade-offs for vectorization are different from scalar code since vectorizing a loop by a factor N gives a huge win, and even if you waste some time doing some redundant computations, you still have a reasonable chance of being faster than scalar.
Gcc will not under any circumstances produce two cmov instructions in a basic block, so that is your only alternative without dropping to asm. Clang is happy to produce two adjacent cmov instructions. Usually your ALUs are not otherwise so engaged as to make the number of operations involved costly.
int select(int x, int y, bool condition) {
...
}https://news.ycombinator.com/item?id=23237663
The bitwise thing is correctly -c&x|(c-1)&y.
inline bool swap_if(
bool c, long& a, long& b)
{
long ta = a, tb = b;
a = c ? tb : ta;
b = c ? ta : tb;
return c;
}
and then use it in a partition like right += swap_if(
*left < pivot,
*left, *right);
compiled with Clang (which generates `cmov` instructions), the Hoare partition is still faster, and more than twice as fast as `std::sort`.Using `swap_if` in Lumuto is also faster, but not as much faster. I interpret the difference (vs. array ops) as resulting from reduced L1 bus traffic.
A fully general swap_if,
template <typename T>
bool swap_if(
bool c, T& a, T& b)
{
T v[2] = { a, b };
b = v[c], a = v[1-c];
return c;
}
could and IMHO should be peephole-optimized to use a pair of `cmov` instructions, but is not in Clang, Gcc, Icc, or MSVC. But even without such an optimization, it makes Quicksort much faster.(Gcc, incidentally, is very, very sensitive to details of the second swap_if. Change the order of assigning a and b, or use `!c` in place of `1-c`, and it gets much slower, on Intel, for very non-(to me-)obvious reasons. Gcc also will never, ever produce two `cmov` instructions in a basic block. I have filed a bug.)
If `swap_if` were in the Standard Library, it would probably be implemented optimally on all compilers, and almost half of the Standard algorithms could use it to get, often, ~2x performance.
1. Write your branchy code
2. Massage the branches until their code is identical (testing frequently)
3. Trim a useless branch, and eat a piece of chocolate
I think it's also generally less expressive, though. Writing Lomuto's partition as efficiently would likely be impossible, but writing it branch-free might not be -- you would compose branch-free primitives like
left = filter(array, array < array[0])
right = filter(array, array > array[0])
where `filter` is branch-free and takes an array of Boolean values as its second argument.Shrugs, aside from some standard set of functions/primitives and idioms I'm not sure it would help much, though perhaps a mindset-shift is a more likely solution than something more technical.
1. https://eigen.tuxfamily.org/dox/group__TutorialArrayClass.ht...
http://pvk.ca/Blog/2012/08/13/engineering-a-list-merge-sort/
The question is, is there such a dependency in this algorithm? I didn't check.
Edit: I can't seem to locate that 4-way swapping algorithm, I swear it was posted to HN in the last year... somebody halp!
"Array Layouts for Comparison-Based Searching" https://arxiv.org/pdf/1509.05053.pdf
(I guess at the very least it prevents vectorization, but other than that ... shouldn't be too much, but probably still the new limiting factor?)
The operations to count, nowadays, are not swaps or comparisons, which are very cheap, but instead pipeline stalls, which are very, very expensive.
It is easy to better-than-double the speed of vanilla quicksort with a three-line change in the partition step.
This whole field is a waste of time, and anachronism from when the master copy was paper/pre-computer and computers were just doing the analytics.
I wrote this comparing the high-level performance of a “set” (AVL or red-black tree) against just that:
Faster lookups, insertions, and in-order traversal than a red-black or AVL tree [0]
[0]: https://neosmart.net/blog/2019/sorted-list-vs-binary-search-...
(Spoiler: it depends on the nature of your data, but sorted vector can be dramatically faster.)
My point is I care about the map or set interface. I don't use temporary sets or quicksort to sort....because I don't sort---I don't want a list/array of things in order, basically ever.
But some of us work on problems where performance of the sort does matter.
Map are just far more the bread and butter of programming than sorting, and not just if you are using the one of the languages satirized by https://elbenshira.com/blog/the-universal-data-structure/