4 ms·
The problem with that method is that it's inefficient; with the switch statement, there's no hashing or significant computation involved in finding the location
by haldean 13y ago
The problem with that method is that it's inefficient; with the switch statement, there's no hashing or significant computation involved in finding the location to jump to. With an object-based method, you have (at least) a memory access to get the method object, a lookup in a vtable to find the location of the Invoke method and a function call (which means pushing a new stack frame) before you start processing the instruction. With the switch statement, none of that happens; instead, it compiles to (essentially) two jump instructions; one into a lookup table, then one from the lookup table to the opcode handler. Getting out of the handler and back into the loop is also a single constant jump (no stack frame popping).
- jimmaswell 13y agoThese are mostly O(1) things and shouldn't matter. The dictionary lookup can become O(1) if you turn it into an array of function pointers indexed by ints, the opcodes.
- breckinloggins 13y agoBut remember that O(n) is really c*n. The problem is that, when emulating a CPU, the "c" can dominate in these cases.
- haldean 13y agoYup, that's definitely true, but opcode parsing and dispatch needs to be a really tight loop. Keep in mind that function calls still involve pushing/popping stack frames, which while super, super cheap, are still not free. There are no loops other than the main loop in here, and every instruction takes constant time, which means that constant factors in opcode parsing and dispatch are going to really dominate.
- haberman 13y agoIn these kinds of situations, O(1) means nothing; any (reasonable) approach will be O(1), but constant factors will dominate. For example, there are measurable differences in branch prediction based on whether each opcode's implementation ends with its own dispatch code or branches back to a common dispatch routine. The sequence of instructions is basically the same in both cases, but there is an icache/branch prediction tradeoff to be made. Subtle stuff (see http://repo.or.cz/w/luajit-2.0.git/blob/0ded8e82a88fadb40b4dd7e6632311781f1b2f91:/src/vm_x86.dasc#l283 http://repo.or.cz/w/luajit-2.0.git/blob/0ded8e82a88fadb40b4d...)
- weland 13y ago> These are mostly O(1) things and shouldn't matter. I often hear this from programmers coming from a higher-level background. O(1) only means constant time. It doesn't mean quickly. This function: int foo(int x) { sleep(3600); return x; } is O(1), just as this: int bar(int x) { return x; } Foo takes an hour to commplete, but it's just as O(1) as bar :-).
- csmuk 13y agoNo dictionaries are O(1). Even if they were they are pitifully slow compared to a switch. Consider chaining and the fact that you have to compute hashes. switch is O(1) and fast as the operation is usually 2-3 deterministic time instructions: 1. Look up jump address from lookup table made by compiler. 2. Jump to it. More people need to have written assembly to actually get this into their heads.
- userbinator 13y agoIn the case that the value you're switching on is a byte, and all 256 values are present in the case (very common for an 8-bit CPU emulator), a switch is a single instruction. You can't get faster/simpler than that.
- haldean 13y agoNot to nitpick, but doesn't it need to be two instructions? You first do a relative jump by the value you're switching on, which takes you to another jump that jumps you to the actual code for that case. I can't think of a way to do it in just one instruction.
- Keyframe 13y agoWhat are the common alternatives to avoid function calls when you want to wrap them? For example you want to wrap an adding function in order to have several cases for it (different architectures for example) but you also want to avoid wrapping it in a function call either via function pointer or switch to invoke different functions.
- haldean 13y agoMacros can satisfy that in simple cases (like the NEXT_BYTE macro in x6502), and inline functions (like set_flags, get_flag, mem_abs, etc.) can handle more complex cases. Both of these result in the code in the contents of the "function" being directly inserted into the calling block. (For inline functions, that's only true if you turn on optimizations, but I think even O1 on any C99 compiler turns on inlining). In general, if I need something to avoid function calls, I'll use macros as a shorthand for a complex expression (like NEXT_BYTE) and inline functions for anything that involves one or more full statements (like set_flags). Obviously I don't follow that too dogmatically, because mem_abs is an inline function.