What’s really so bad about bubble sort?
nicknash.me
nicknash.me
Quicksort infamously abuses branch prediction, yet is still the standard against which all other sorting algorithms are compared. Some work is being done to improve Quicksort's branch prediction performance [1], but that's mostly focusing on choosing better pivots.
A perfect sorting algorithm, something like a very large sorting network, will necessarily make any processor's branch predictor give up and go home -- and that'll still be faster to execute than a naive algorithm tuned for branch prediction.
[1]: http://www.cs.auckland.ac.nz/~mcw/Teaching/refs/sorting/quic...
For sorting, if you're minimizing the number of comparisons, the result of each compare should be random and independent of all others. So the only way to have "good" branch prediction when sorting an array of random numbers is to do lots of extra, superfluous compares, e.g. using an O(n^2) algorithm instead of an O(n log n) one.
The base case for std::sort in glibc is insertion sort. It's used for less than 6 elements. Nice and linear memory access patterns.
Martin
Also, even artificially skewing the pivot in quicksort as suggested in the paper you reference doesn't work except on /very/ long pipeline machines, because it worsens cache performance & increases instruction count. Also, even then, it's expensive to randomly sample to generate a skewed pivot (a cost they negelect in that paper, assuming it is free)
Couple years ago I had a fairly simple program which collected some data into a linked list and displayed it at the end. It took about 2 seconds to load and process the data from disk. I realized it might be neat to optionally sort the output. Since I didn't feel like doing serious work, I just added a bubble sort to my linked list which took a comparison function and then bubble-sorted by pointer swapping.
Guess what time it added? less than a 10th of a second. Completely negligible. It saved me a few hours (as writing bubble sort for a linked list takes maybe 5 minutes and has no chance of complicated bugs) and I doubt anyone will ever notice any slowdown.
So what's the lesson? Little performance tweaks hardly matter in the average program nowadays because everything's disk or network bound (IO bound). For the average program it's not worth writing more complex code that will be quicker (once you fix the hard-to-see bugs) until you actually know the slowness is an issue.
It makes sense to use the most optimal version out of the gate in these cases.
It's important to understand the characteristics of various sorting algorithms so you can choose the best one
I disagree - most languages offer a sort function which is nealry optimal in the average case. You very rarely choose unless you are doing something where the sort performance is extremely important.However, uunderstanding various sort algorithms and their intricacies is a great way to demonstrate fundamental algorithm design as well as some data structures. This is why it's important - so when you do write code, you come with a huge amount of insight into how to go about it.
This was (is still?) in the gnu flex program source:
/* We sort the states in sns so we
* can compare it to oldsns quickly.
* We use bubble because there probably
* aren't very many states.
*/
bubble (sns, numstates);
Whoever wrote this should know better. Their mistake of not using the language's built-in sorting functionality would at least be forgivable if they had used insertion sort, but maybe we should blame academic institutions for exposing programmers to bubble sort in the first place. Want to show newbie programmers the difference between an O(N^2) and O(N*logN) algorithm? Fine. Insertion sort is O(N^2) too, just like bubble sort, though there is no practical case where bubble sort out-performs insertion sort, and you certainly wouldn't use bubble sort to speed up quicksort. for(i=0 to n):
for(j=0 to n-1):
if(a[j] > a[j+1]) swap(a[j],a[j+1])
-or- for(i=0 to n*n):
j = i%(n-1)
if(a[j] > a[j+1]) swap(a[j],a[j+1])
;-)Insertion: https://github.com/pbiggar/sorting-branches-caching/blob/mas...
Bubble: https://github.com/pbiggar/sorting-branches-caching/blob/mas...
Insertion sort is significantly faster (esp in branch prediction, they have similar cache locality, insertion sort has few comparisons and fewer moves and fewer overall instructions), is almost the same to implement, and is actually the fastest possible way to sort arrays of size 4 (and probably true up to about 30). So it's actually useful to know insertion sort, whereas bubblesort is actually useless.
There are circumstances in which bubble sort really is that bad, but on small data sets that are already disk/network/peripheral IO bound, it's just fine.
And it's false to say everything's disk/network bound these days. There will always be important data structures and other code that needs to scale, where the right algorithm makes the difference.
Even the world's slowest disk will be faster than a CPU executing a sufficiently silly algorithm.
The implementation is straight-forward: instead of appending new elements to the end of the list, insert them in the correct place.
My personal go-to algorithm for linked lists is an unstable merge-sort variant with explicit stack[1] - while not as simple as insertion sort, performance is far superior.
[1] https://bitbucket.org/cggaertner/listsort/src/master/listsor...
http://stackoverflow.com/questions/11227809/why-is-processin...
Which has been posted many times before - however, applying it to bubble sort, which we all know well, is really interesting.
Thanks for sharing!
Of course the sort is named after Tim Peters, its author, rather than the said character. Sorry, Mr Peters!
http://cs.stmarys.ca/~muir/csci4452_11/ClassNotes/Astrachan_...
In commercial programming it's pretty rare to run across a situation where writing your own sorting routine makes sense.
Example: Games often bubble sort world objects by depth. There is a small performance gain to be had by drawing closer objects first, so that you can depth-cull expensive pixels behind them. Game engines can run several iterations of bubble sort, but stop before sorting the set completely, since the z-buffer will ensure correctness. Over the span of several frames, the incremental bubble sort will achieve a total sort, but the incremental approach bounds cost more tightly. Since the depth of objects only changes relatively when you look around, it's usually a pretty good approximation of "perfect" behavior.
This of course assumes that the swap won't mess up the processing algorithm.
It also is an optimization, so best left for later stages of development.
Anyway, thanks for showing this example - it gives me another trick to try for a couple projects I'm working on. :)
Far more efficient sorting algorithms do not behave very well if the comparison functions give wrong answers and this can be especially difficult to detect.
The bubble sort then helps with the test suite - you can compare the results from the brute force bubble sort against the optimised sort that your framework/language provides.
You could also directly verify (probabilistically) the properties of a total ordering [1]. If your comparison function is "comp" and returns -1, 0, or 1, the tests might go like this:
trials = 1000
while trials > 0:
a = rand_element()
b = rand_element()
c = rand_element()
aa = comp(a, a)
ab = comp(a, b)
ac = comp(a, c)
ba = comp(b, a)
bb = comp(b, b)
bc = comp(b, c)
ca = comp(c, a)
cb = comp(c, b)
cc = comp(c, c)
#reflexive
assert(aa == 0)
assert(bb == 0)
assert(cc == 0)
#antisymmetric
assert(ab == -ba)
assert(ac == -ca)
assert(bc == -cb)
#transitive
assert((ab != bc) or (ab == ac))
assert((ac != cb) or (ac == ab))
assert((ba != ac) or (ba == bc))
assert((bc != ca) or (bc == ba))
assert((ca != ab) or (ca == cb))
assert((cb != ba) or (cb == ca))
assert((ab != 0) or (ac == bc))
assert((ac != 0) or (ab == cb))
assert((ba != 0) or (bc == ac))
assert((bc != 0) or (ba == ca))
assert((ca != 0) or (cb == ab))
assert((cb != 0) or (ca == ba))
trials -= 1
[1] http://en.wikipedia.org/wiki/Total_order assert bubble_sort(data) == sort(data)
Since this always runs during development it will automatically pick up internal and external changes.What you laid out is explicitly testing everything against everything else and is 100% comprehensive. Bubble sort is less than 100% comprehensive but still gives good coverage, while optimised sorts just mean you get lucky in the face of buggy comparisons.
I had a small data set, and, more importantly, I needed a customer who might be a less-than-saavy programmer (VBA) to understand what was going on.
Bubble sort is, if nothing else, simple to understand.
I'd suggest a recursive algorithm would code up far more transparently. E.g. Sort first half of the list; sort 2nd half of the list; merge the two half-lists. That's obvious at the top level; the details of merge are all that's left to explain. It also demonstrates dissecting a problem into smaller problems, so is a double lesson.
2) The article completely ignores the possibility of conditional-move instructions, which would cut the mis-predicted branches in this case to zero (or one if we add the "are we done?" check and always predict "no").
There are (artificial) mergesorts that execute O(nlogn) branches but also mispredict O(n) times. See for example "branch mispredictions don't affect mergesort:" http://www.cphstl.dk/Paper/Quicksort/sea12.pdf
In the case of 2. I agree, although compiler support still isn't great -- I think. For almost all cases (integers, floats, strings) radix sorting would be even better -- fewer instructions, no comparison branches.
This stackoverflow post covers the "when to use" parts of various algorithms, I'm just wondering more about the practical vs theoretical runtimes of these sorts. http://stackoverflow.com/questions/1933759/when-is-each-sort...
At the same time, quicksort has the very nice property that if it accidentally picks a somewhat unbalanced pivot it naturally biases its branches making them easier to predict. This essentially cheapens the comparison branches it executes. I don't really know if that helps in practice though.
*Unless you jump through hoops/change the model of computation, one such way being conditional move instructions. There are more impractial ways too. One good way is to use radix sort (it's generally faster for other reasons) and it never compares keys.
Thanks!
You're correct that every permutation of {1, ..., n} is assumed equally likely as an input.
This way of thinking about it might help:
All that "bubble_sweep" does to the input array is to "right-shift" the left-to-right maxima, and "left-shift" the elements between them. The left-shifted elements aren't re-ordered, so they still have the usual probability of being a left-to-right-maxima.
In a bit more detail: Let L be the sequence of indices, from left-to-right, of left-to-right maxima in the array. Now in s^{th} sweep, let M be the sequence L concatenated with n - s. Assume M has r elements in total now.
Then the operation of "bubble_sweep" is just:
a[M_r] <- a[M_{r - 1}] a[M_{r - 1}, ..., M_{r - 2}] <- a[M_{r - 1} + 1, ..., M_r - 1] . . . a[M_2] <- a[M_1] a[M_1, ..., M_2 - 2] <- a[M_1 + 1, .., M_2 - 1]
I think you could actually implement a (very, very) inefficient "bubble_sweep" that worked along these lines. I think that makes it a bit clearer that, apart from at left-to-right maxima from a previous iteration, we just have subsequences of the original (uniform random) permutation, so the 1 - 1/(l+1) probability holds.
I hope that's clear(er) and I didn't make too many mistakes. The sun is shining and I must go ride my bike now!
Deleted comment