|
| 1 | +--- |
| 2 | +layout: post |
| 3 | +title: "Unreachable Switch Statement Default Cases" |
| 4 | +date: 2021-03-24 |
| 5 | +--- |
| 6 | + |
| 7 | +Standard C and C++ requires `switch` statements to behave harmlessly if on a value that not covered by one of their `case`'s. |
| 8 | +For example, in this case |
| 9 | +```cpp |
| 10 | +switch( i ) { |
| 11 | + case 0: |
| 12 | + j+=k; |
| 13 | + break; |
| 14 | + case 1; |
| 15 | + j*=k; |
| 16 | + break; |
| 17 | + case 2: |
| 18 | + j/=k; |
| 19 | + break; |
| 20 | +} |
| 21 | +``` |
| 22 | +If `i` wasn't 0, 1, or 2, the `switch` statement would just roll on by without anything happening to `j`. |
| 23 | + |
| 24 | +Behaving harmlessly is good, except that it's not free. |
| 25 | +In practice, this means that every time the `switch` statement executes it has to bounds-check `i`. |
| 26 | +In really tight loops, for example in a [byte-code interpreter](https://eli.thegreenplace.net/2012/07/12/computed-goto-for-efficient-dispatch-tables) where each case statement represents an instruction on a virtual CPU, this bounds-checking can add up! |
| 27 | + |
| 28 | +Enter [`__builtin_unreachable()`](https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html), a compiler intrinsic that lets you double-dog swear that a section of code will never execute. |
| 29 | +It's not standard, but GCC and Clang at least both know about it. |
| 30 | +Getting the compiler to take your word that certain execution paths will never execute is useful for all sorts of mischief, but I wondered whether it might prove useful in this particular problem of disabling `switch` bounds-checking. |
| 31 | + |
| 32 | +Basically this would look like something along the lines of, |
| 33 | +```cpp |
| 34 | +switch( i ) { |
| 35 | + case 0: |
| 36 | + j+=k; |
| 37 | + break; |
| 38 | + case 1; |
| 39 | + j*=k; |
| 40 | + break; |
| 41 | + case 2: |
| 42 | + j/=k; |
| 43 | + break; |
| 44 | + default: |
| 45 | + __builtin_unreachable(); |
| 46 | +} |
| 47 | +``` |
| 48 | + |
| 49 | + |
| 50 | +To the compiler-explorer-&-quick-bench-mobile!! |
| 51 | +:bat: |
| 52 | + |
| 53 | +Here's an assembly snippet from a small example I threw together [on Godbolt](https://godbolt.org/z/TrqKv77G6). |
| 54 | +``` |
| 55 | +unreachable_default(int): |
| 56 | + mov edi, edi |
| 57 | + mov eax, DWORD PTR CSWTCH.2[0+rdi*4] |
| 58 | + ret |
| 59 | +no_default(int): |
| 60 | + xor eax, eax |
| 61 | + cmp edi, 3 |
| 62 | + ja .L3 |
| 63 | + mov edi, edi |
| 64 | + mov eax, DWORD PTR CSWTCH.4[0+rdi*4] |
| 65 | +``` |
| 66 | + |
| 67 | +With the `unreachable_default` function, we can skip a `cmp` (compare) and `ja` (jump if) instruction. |
| 68 | +I assume that in the `no_default` function those instructions were being used for the aforementioned bounds-checking. |
| 69 | + |
| 70 | +But, does it go brrr? |
| 71 | +[Looking at this graph](https://www.youtube.com/watch?v=sIlNIVXpIns), it does indeed seem to go brrr. |
| 72 | + |
| 73 | +{:width="100%"} |
| 74 | + |
| 75 | +Compiling with Clang, adding an unreachable default case gives ~1.3x speedup [on a toy example](https://quick-bench.com/q/1cObPCIkz3g44gHWFWeTkKDgp1Q). |
| 76 | +Even though I was able to see some changes in the compiler output (so `_unreachable_default()` was doing *something*) [I wasn't able to see any speedup on GCC, though](https://quick-bench.com/q/ycTTednxWKAsGKd3JsRG9YH9bx8). |
| 77 | + |
| 78 | +Kind of a nifty trick! |
| 79 | +I've gone ahead and pasted it in to some of my projects, like [signagp-lite](https://github.com/mmore500/signalgp-lite), that do byte-code interpretation in a tight loop. |
| 80 | +I did add a (debug-mode only) assert in right before the `__builtin_unreachable()` in order to check myself when I inevitably wreck myself. |
| 81 | +:man_shrugging: |
| 82 | + |
| 83 | +## Let's Chat |
| 84 | + |
| 85 | +Comments? |
| 86 | +Questions? |
| 87 | +I'd love to hear about any related hacks you're up to! |
| 88 | + |
| 89 | +I started a twitter thread (right below) so we can chat :phone: :phone: :phone: |
| 90 | + |
| 91 | +<blockquote class="twitter-tweet"><p lang="en" dir="ltr">it's been forever, but new blog post!!! <br><br>this one on mischief with __builtin_unreachable() default switch cases<br><br>perfect for the extra percent of unsafe bytecode interpreter fun 🍹🏖️💣<a href="https://t.co/rBJCM8nnJt">https://t.co/rBJCM8nnJt</a></p>— Matthew A Moreno (@MorenoMatthewA) <a href="https://twitter.com/MorenoMatthewA/status/1374891036934295555?ref_src=twsrc%5Etfw">March 25, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> |
0 commit comments