The number of comparisons needed to sort a shuffled array: qsort vs. std:sort
lemire.me
lemire.me
One very minor quibble is that the C function contains the "counter++;" statement to increment the benchmarking counter, but the corresponding C++ function does not. Mildly confusing but probably just a sign that the code in the article is not necessarily the code that ran for the tests.
Also, very good to showcase the slightly non-intuitive use of memcpy() here. It is compiled out [1] but a very "safe" way to grab data across types. Now if only he omitted the parentheses for non-type name arguments to sizeof ... :)
I think casting from void* back to double* is the better option here. memcpy() is overkill here since that idiom is mainly for reinterpreting data with a different type without triggering undefined behavior: https://stackoverflow.com/questions/98650/what-is-the-strict.... We're not changing the type, so why confused people with an overly powerful idiom? That said, the memcpy() idiom is common and useful enough that C++ added a specific API for it: https://en.cppreference.com/w/cpp/numeric/bit_cast.
Casting and the memcpy() idiom compile down to the same code https://godbolt.org/z/xY6W9ja69*
Are you sure this also applies to function pointers?
Not sure about the platform question, I tried building it for a Cortex-M3 (32-bit "small" ARM) but that still doesn't issue the memcpy() call. Instead it just loads pairs of words from memory then moves those into the FPU for comparison. The loads are marked with "@unaligned" but not sure what that actually means and didn't spend the time to research it now.
I assume you're talking about this:
long x = <whatever>
double y = *(double*)&x;
Nope, it is undefined behavior, with few exceptions, even if sizeof double == sizeof long. See https://stackoverflow.com/questions/98650/what-is-the-strict...If you got rid of this rule a huge number of optimizations would be lost, since the compiler would have to assume that any memory, of any type, could change if anything at all is modified through a pointer. You'd be surprised how slow everything would be. You'd also be surprised how much code violates this and gets away with it. Time bombs waiting to happen. ;-)
memcpy(), and more generally accessing bytes through char*, are how you avoid this kind of undefined behavior. It comes up so often that C++20 added std::bit_cast for this same use: https://en.cppreference.com/w/cpp/numeric/bit_cast
Here's how I would have written it, without memcpy():
static int cmp_double(const void *va, const void *vb)
{
const double a = *(const double *) va, b = *(const double *) vb;
return a < b ? -1 : a > b;
}
I know you can't "jump types", but this doesn't, since qsort() is passing pointers to doubles and we're just getting them back.Edit: missed const:s in function body oops.
If you run the code by yourself you end up that C (while calling the compare() less often than C++) needs more comparisons (i.e. more '<' and '==') kinda defeating the argument of costly comparisons:
N = 2**16:
qsort comparison calls 14.735568
qsort comparisons 22.103386
std::sort comparisons 19.294233Edit: For primitives we can also consider what happens in the processor. A comparison consists of two parts. First loading the operands and comparing them (loading is the expensive part here), and then a conditional branch instruction. A three-way comparison only loads the operands once and so has the same memory pressure but it does put more stress on the branch predictor. Not quite twice though, because the second branch instruction will only be encountered about half the time, and it will usually take the same path as elements are usually not equal.
C++ compare function:
compare(double, double): # @compare(double, double)
ucomisd xmm1, xmm0
seta al
ret
C compare function: compare: # @compare
movsd xmm0, qword ptr [rdi] # xmm0 = mem[0],zero
movsd xmm1, qword ptr [rsi] # xmm1 = mem[0],zero
movapd xmm2, xmm0
cmpneqsd xmm2, xmm1
movq rcx, xmm2
and ecx, 1
ucomisd xmm1, xmm0
mov eax, -1
cmovbe eax, ecx
retWe have found that in the SIMD context, those two comparisons are expensive enough to make this no longer worthwhile. We instead special-case subarrays with one and two unique values, which can be about 4x as fast on real data vs uniform-random data.
>>> compare is a function which returns an integer less than, equal to, or greater than zero if the first argument is less than, equal to, or greater than the second. Because it is a C function, it takes in void pointers which we must convert back to actual values. One safe way to achieve such a conversion is through the memcpy function.
wait, what? why do you need to copy the memory??? can't you just cast to (double*) and deference???
edit s/pointer/pointee
int compare(const void *a, const void *b) { return *(double*)a - *(double*)b; }
https://bugfix-66.com/834f0677c85b23c0bf1047d3654ab7c27ff054...
There are of course faster sorts than std::sort, such a pdqsort, if you are really pinched. Usually you are not.