The internals of switch and chained else ifs and which one should you use - Printable Version + open.mp forum (https://forum.open.mp) -- Forum: SA-MP (https://forum.open.mp/forumdisplay.php?fid=3) --- Forum: Tutorials (https://forum.open.mp/forumdisplay.php?fid=37) --- Thread: The internals of switch and chained else ifs and which one should you use (/showthread.php?tid=322) |
The internals of switch and chained else ifs and which one should you use - kristo - 2019-04-17 The internals of switch and chained else ifs and which one should you use This tutorial takes a look into how if and switch statements work on the assembly level and helps determine, where to use a switch and where to prefer chained else ifs. I decided to write this tutorial, because one of the topics I am about to address is a semi-common issue and currently there is no good place to refer to when it comes to it. In addition to that, the research I did for this tutorial helped me understand the AMX structure quite a bit more than I used to before. The internals I used the -a flag to generate the assembly of two simple snippets of code that perform the same task, one using switch and the other using chained else ifs. I also took the time to rewrite them in a more human-readable form (using variable names instead of raw addresses, named labels instead of numbered ones prefixed with l., etc.) and to document them. It is worth noting that while the rewritten assembly is valid, it does not currently compile due to a compiler bug related to forward jumps. Chained else ifs PHP Code: new val; For the sake of simplicity I used the form 2 <= val <= 4 instead of val >= 2 && val <= 4 for the range check. If written the other way, the code would use 7 jump instructions instead of 4 and keeping track of the instructions would be more difficult. It would also be a tiny bit slower since it would have to load the value of val to the primary register one more time. When using chained else ifs, the assembly contains the AMX instructions for each individual check. If the check fails, it jumps to the next check, otherwise it jumps to the end of the chain. Here is the assembly equivalent to this code: PHP Code: new val; If the value of val is 1, the code uses four instructions: PHP Code: LOAD.S.pri? val If the value of val is 2, 3 or 4, the code uses 13 instructions: PHP Code: LOAD.S.pri? val If the value is not matched by any of the previous checks, the code uses 12 instructions: PHP Code: LOAD.S.pri? val The longer your chain is and the more failed checks there are, the more instructions the code uses. Each failed single value check adds 3 instructions and each failed range check adds 9 instructions. switch PHP Code: new val; When using switch, the compiler generates a case table that contains the value and jump address for every single case, including the default case. Here is the assembly equivalent to this code: PHP Code: new val; Whatever the value of val is 1, the code always uses just three instructions: PHP Code: LOAD.S.pri? val The SWITCH instruction jumps to the case table, performs a linear search on the cases and jumps to the correct one. If the search does not find a matching case, it jumps to the default case instead. It is similar to the behaviour of chained else ifs, but since it is done in native code instead of using AMX instructions, the performance is a lot better. When NOT to use switch? Large ranges As seen before, case tables can only use single values as cases, which means that if you use a range of 10000 elements in your switch, the compiler will have to write 10000 entries to the case table and the runtime will have to search through 10000 entries. Using the following benchmarking script I determined that the runtime performance of a case table becomes worse than the one of a single a <= x <= b check at around 30 entries. PHP Code: #define MAX 30 However, runtime performance is not the biggest issue. As seen from this forum topic, the amount of time it takes for the compiler to generate large case tables can be so long that it seems for the user that the compiler has ran into an infinite loop. Even ranges small enough to only have a small impact on the compilation time (for example, 1000 elements) can eventually make the compilation process painful if the delays stack up. Floating point values When using switch with floating point values, the case table must also contain every single value within the range. The step between two cases grows as the values grow, but it starts at the smallest possible floating point value in the IEEE754 specification, which is 1.4E-45. This means that each entry differs from the previous and next one by just 0.0000000000000000000000000000000000000000000014. Some examples of ranges with the respective amounts of case table elements: Code: |? ? ? ? ? ? ? ? ? ? ? ?Range? ? ? ? ? ? ? ? ? ? ? ?| Amount of entries | This test should be enough to show that using switch with floating point values relevant in the context of SA-MP (coordinates, health, etc.) is completely unreasonable. When to use switch? I would say that everywhere where there are chains of comparisons against constant values. I have seen people saying that switch is faster no matter what and using a switch with two cases is much faster than an if-else. However, their runtime performance is about the same and where an if and an else make more sense, they should be used instead. Tips and tricks Using switch with floatcmp to compare floating point values Comparison operators for variables with the Float tag are overloaded to use the floatcmp native instead of comparing them with AMX instructions. When comparing floating point values to eachother and having different outcomes depending of if one of the values is larger than the other or they are equal, you can save a floatcmp call by comparing them using switch instead. floatcmp returns 0 if the values are equal, 1 if the first value is larger and -1 if the second value is larger, so the following snippets of code are equivalent, but the second one performs better: PHP Code: if (a > b) { // floatcmp called implicitly PHP Code: switch (floatcmp(a, b)) { Using switch to compare strings Comparing strings with strcmp has been notorious being both slow and tedious for a long time. Although most of that bad reputation came when ZCMD became the goto command processor and strcmp is still very useful elsewhere, there is a grain of truth in every joke. When comparing a string to a long chain of other strings, comparing their hashes would be more efficient and also convenient due to the ability to use switch, because the hashes are in the form of integers. The PAWN language does not support such thing natively, but y_stringhash from YSI does just the job. According to Y_Less? tests, comparing strings using hashes becomes more efficient than using strcmp at around 10 comparisons. More information about y_stringhash and its usage can be found from this topic. PHP Code: if (!strcmp(number, "one")) { PHP Code: switch (YHash(number)) { Credits
|