Bit Twiddling Hacks
graphics.stanford.edu
graphics.stanford.edu
[1] https://github.com/scala-js/scala-js/blob/v1.3.1/linker-priv...
[2] https://lampwww.epfl.ch/~doeraene/thesis/doeraene-thesis-201...
Compilers failing to generate POPCNT cause sometimes 10x slowdowns in key operations.
Portability is an issue though, that works on GCC/Clang but you need a different intrinsic for MSVC
C++98 gave us std::bitset<N>(x).count(). With C++20 we have std::popcount(x). But getting the compiler to produce the instruction reliably and portably is no easier than before.
popcnt(unsigned int):
xor eax, eax
popcnt eax, edi
retI was able to jot down a naïve implementation and the obvious optimisation based on a lookup tables. I only vaguely remembered the Bit Twiddling treatment of the subject but, with a bit of nudging from the interviewer, I managed to implement and explain the variant that runs in O(set bits) (“Brian Kernighan's way”). I got the job.
Now, it’s fashionable to deride this this kind of code interview as unrealistic and unhelpful. But in my first week on the job, by sheer coincidence, I had to use the function. Obviously there are existing, efficient implementations, including intrinsics. But knowing how to derive an efficient implementation certainly didn’t harm. My job has since evolved into different responsibilities but low-level algorithmic knowledge is still important. I’m not sure testing for it in job interviews is generally a good idea, and designing good job interviews is certainly a big topic. But in my particular case it happened to be a relevant, fair test of my abilities.
Nobody should ask this anyway, this has become the fizzbuzz type of question.
What's funny is that 15 years later, I actually had a use for popcnt, put it together with something that seemed expensive at the time, and wound up with a C program that exhaustively searched a problem space in .12 sec. On a single core, in a vm, on a laptop. So much for me trying GPU programming on that one. (as in , feckit, there aren't that many possibilities, we'll just count them all)
There were three broad ways candidates answered it:
1) Hack out a for-loop-style bit count. This was good, because even though it wouldn't be optimized, it demonstrated they could understand the problem and at least conceptualize a solution.
2) Give the "leetcode" best-answer (it was some bitwise math trick). I'd then ask if they'd seen the problem before, and the answer was always yes. This was a mark in their favor—but no better than (1)—and also a signal I needed to ask them a follow-up question that actually made them think.
3) Code a bit, but not quite arrive at a solution. I'd then probe them about their thought process. Not an automatic fail, sometimes our brains just don't walk down the corridors we want them to at a given time, especially during an interview. (One candidate couldn't hold the marker steady because his hands were shaking too much! Poor guy.)
4) Give up and say they had no idea. Obviously the worst case for the interviewee.
The purpose was not to check a candidate's recall and test-taking abilities, but rather to watch them reason through a novel problem, appropriately limited in scope for the interview timeframe. Case (2) served as a great short-circuit to block test-preppers lacking actual experience.
The interview was for a senior position.
> Even if you do learn it all, people tend to lose knowledge they don't use
Let me emphasise that my interview was not a knowledge test (and at any rate I don’t study for interviews). I wasn’t expected to know by heart how to implement popcount. The interviewer was trying to see me work. Successfully, I might add. — Another question I got concerned something I had no knowledge of, and I had to derive a solution myself. In fact, I failed to do so, but that didn’t prevent me from getting the job since the interviewer was satisfied with what they observed about my thought process.
Years ago I was messing around with SAT-based bounded model checking of C programs that I was crafting. Many of these tricks allow you to remove branches/loops from your program, which is great because you end up with a smaller SAT formula, which (usually) can be solved faster.
At the moment, the only ISO Standard way to say "popcount" is in C++, with e.g.
std::bitset<64>(x).count()
But even this is insufficient on MSVC, which targets pre-2002 arm64, which lacked it; and on gcc and clang on amd64, similarly, absent a -fpopcnt or -march=native or related option (which there are numerous other reasons to use).Given -march=native or =core2 or various other means, optimizers will happily rewrite the Kernighan loop into a straight-up POPCNT instruction. Anyway, all compilers provide it as a non-standard, therefore variously-spelled, intrinsic, but (except on MSVC) only actually produce it if the "-march=" or related commad-line option enables that.
Historically, it is common for instruction sets to start out lacking the instruction, and then getting it in subsequent releases because of customer demand. Also historically, a key such customer has frequently been the US NSA. Thus, initial and64, alpha, POWER, and SPARC lacked it, but soon got it. x86 ("ia32") is perhaps the principal laggard. When a feature is added, at great expense, to a subsequent ISA revision, that says a lot for its importance.
[0] All except RISC-V, to date. Some would say this makes RISC-V non-modern. It appears in the (still) unratified B extension, which (therefore) nobody implements.
[1]: https://graphics.stanford.edu/~seander/bithacks.html#CountBi...
And by small, I mean writing code to fit on a credit card, transit ticket, or SIM card. (Yes <2KB ROM budgets still exist.)
template <typename T> int sgn(T val) {
return (T(0) < val) - (val < T(0));
}
Not mine, from here
https://stackoverflow.com/questions/1903954/is-there-a-stand...https://ocw.mit.edu/courses/electrical-engineering-and-compu...
Also, curiously enough, I was on the page in the submission after a co-worker asked yesterday if there was a cleaner way to do:
return direction === 'asc' ? value : -value;
Was fun to work through how https://graphics.stanford.edu/~seander/bithacks.html#Conditi... works. (no, we didn't change our javascript code to use the bit hack)Also helpful: being able to represent integer types with binary. Since I'm not a genius and I write code to be maintained by other not-geniuses, `mask = 0b_1010_1010` is clearer than `mask = 0xAA`.
uint64_t a = ((a0 & 0x7F) | ((a1 & 0x7F)<<8) ...;
uint64_t b = ((b0 & 0x7F) | ((b1 & 0x7F)<<8) ...;
uint64_t c = (a + b) & 0x7F7F7F7F7F7F7F7F;Indeed, even 8-bit fields can be added in parallel, using the fact that ^ is like a + that does not produce a carry:
uint64_t signmask = 0x8080808080808080;
uint64_t sum_without_sign_bits = ((x & ~signmask) + (y & ~signmask));
uint64_t sum_of_sign_bits = (x ^ y) & signmask;
return sum_without_sign_bits ^ sum_of_sign_bits; int const mask = v >> sizeof(int) * CHAR_BIT - 1;
r = (v ^ mask) - mask;