6 ms·
One of the most interesting parts of this code to me was the switch statement in the main loop. Professionally, I have run into programs that have massive switc
by jsaxton86 13y ago
One of the most interesting parts of this code to me was the switch statement in the main loop. Professionally, I have run into programs that have massive switch statements with hundreds of cases in them, which I've always considered a "code smell". Your approach is interesting -- breaking everything up into logical groups, putting them in separate files, then using the preprocessor to put everything back together -- that's the first time I've seen that.
I'm still not sure how I feel about it -- you could argue that you have the same problem, but now it's spread out over many files. At the same time, I can look at the loop and have a pretty good idea of what it does and I can easily find what I'm looking for. I guess without OO, there isn't a great solution to the problem, although yours is probably a slight improvement, with the added bonus of getting me to think about the preprocessor in a new way.
Good work.
- alextingle 13y agoDoing it once is fine. The problems start when this technique is used liberally throughout a 1,000,000 line "enterprise" application.
- userbinator 13y agoIMHO there's no much simpler way to write a CPU emulator than as a huge switch inside a loop as that models quite well what a real CPU does. Anything more just obfuscates the essence of the functionality.
- jimmaswell 13y agoI suppose you could create a dictionary of opcode -> function pointers. In C# that'd be like var ops = new Dictionary<Opcode, Action<opcodeArgs>> () { {0, nop }, {1, add }, ... } or even better, maybe I'd get a list of the opcodes with their names, name all the functions after those names exactly, then use reflection to automatically create this dictionary. Then in the main loop, ops[opcode].Invoke(args); I don't know if something like that would be any use in C, though.
- grapeshot 13y agoIt's possible to do something like that in C. In C++, you can even do some template magic to create all the appropriate functions for each opcode based on something like the original instruction decode ROM. Brilliant and short code, but very difficult to write in the first place. Here's a very apropos example of this technique as applied to the 6502. http://www.youtube.com/watch?v=y71lli8MS8s http://www.youtube.com/watch?v=y71lli8MS8s
- haldean 13y agoThe 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.
- jsaxton86 13y agoAt a previous job, there was a legacy UI application that had a 10,000+ line file called "messloop.c". It consisted of a function called messloop(), which was an infinite loop with a massive switch statement inside of it. I never had to work on it, but I'm told it was a maintenance nightmare. So when I read the author's code and saw the way he handled a similar problem, I was intrigued. If you had to deal with a massive switch statement with thousands of cases, it would probably be easier to maintain IMHO. For a hobby project, the author's solution is 100% correct and I'm not critiquing it at all. In fact, the only reason I even commented was because I thought his solution was better than any other procedural solution I had seen before. With that said, in an enterprise environment, where maintainability is crucial, I'd argue that a massive switch statement is probably a bad idea. Going back to messloop.c, what happens if the user tries to change a floating point value through the UI? Well, I can tell you that there is a case statement for that somewhere in messloop.c. What is that case statement called? I'm not sure, all I know is it's a #define that I'm sure made sense to the original author. It's basically a needle-in-a-haystack problem.
- userbinator 13y agoThe problem in your case (no pun intended) seems to be the difficulty of "finding the right branch". With a CPU emulator, it's not so hard: look up the opcode and there it is. Would you rather scan through a single file or several dozen?
- csmuk 13y agoThat's what the debugger is for (if you inherited it). However, a lot of desktop applications are written that way. If you've ever dealt with Win32, you'll see nested switch statements from hell on your average project. If you have a pure OO language like Java or C# then there's no excuse but some legacy applications built in C tend to be "switchy" because the older APIs seem to favour that form of message dispatch. There is still no excuse as you can have decent abstraction in C or C++. However for what is effectively a jump table, a switch statement is exactly spot on for this project.
- pcwalton 13y ago> Professionally, I have run into programs that have massive switch statements with hundreds of cases in them, which I've always considered a "code smell". Most optimizing compilers will emit a jump table for a switch, which is a good way to get a reasonably high performance interpreter while keeping the code pretty clean. The performance is better than using virtual methods [1]. [1]: http://www.complang.tuwien.ac.at/forth/threading/ http://www.complang.tuwien.ac.at/forth/threading/
- Sharlin 13y agoIt's still a code smell. It might be a justifiable code smell in some very specific circumstances where the performance actually matters, but a code smell nevertheless.
- haberman 13y agoYou are substituting dogma for critical thinking. Sometimes a switch statement (or goto, etc.) is the best overall solution to a problem. In those cases, it is actually good code, not "justifiable code smell."
- Sharlin 13y ago"Code smell" is just a heuristic. It's what happens when you look at some code and something in your head says "this doesn't look good". We all know that in the real world maintainability is more important than minor theoretical performance gains (yes, premature optimization without actually ever measuring the gains is a common thing, as we all hopefully know). I certainly didn't mean to imply that a switch or a goto statement cannot be the best way to implement something; I simply meant that a theoretical performance argument is not enough to make good code out of bad code.
- dalke 13y agoA large switch statement is exactly the correct solution for this task. Other virtual machines do the same thing. For example, look at Python's ceval.c, which has a switch table implementation and the text "The traditional bytecode evaluation loop uses a "switch" statement, which decent compilers will optimize as a single indirect branch instruction combined with a lookup table of jump addresses." Actually, it also has a version which uses GCC's "Labels as Values" extension, see http://gcc.gnu.org/onlinedocs/gcc/Labels-as-Values.html http://gcc.gnu.org/onlinedocs/gcc/Labels-as-Values.html . The comment then points out that this non-portable solution is up to 15-20% faster than a large switch statement, because having N jump points instead of 1 improves the CPU's branch prediction. As haberman says, this is "actually good code", not "justifiable" bad code. I am another who dislikes the term "code smell." I agree with mbrock - I would rather get a comment which reflects the underlying complaint than use a proxy term like "code smell". In your reply to mbrock you wrote: > the "smell" is just a heuristic that says that without any additional arguments in its favor, a hundred-case switch usually isn't the cleanest and most maintainable way to implement something. Why not just say "a hundred-case switch statement usually isn't ...", and omit a reference to "smell"? What additional meaning does the term "code smell" lend? I dislike using the term "code smell" in general. Some people detest stinky cheeses, or a peaty whisky, while others adore the complex aromas. Often this is a learned taste which comes with age and experience. As a result, the obvious rebuke to any "this is a code smell" comment is "that's because you aren't mature enough to appreciate it." I can't think of any effective response which stays on topic, other than to bring the conversation back to the specific problems in the code. Why not just start from that point instead?
- haldean 13y agoThanks! The one downside is that the local variables that are in scope (and heavily used) in the opcode handlers are all in a different file, which is super weird, but. On the other hand, I get the performance advantage of the switch statement jump table and, like you said, it's a lot easier to find whatever I'm looking for. I think it turned out a net win.
- brianfryer 13y agoThe README is completely broken in movie devices due to everything being inside the `code` block :(
- voltagex_ 13y agoI'd say that's a GitHub bug, not a code-specific bug - maybe try contacting support (with app details and a screenshot)
- haldean 13y agoOh wow, yeah; just checked and it does look pretty crap. I agree that it's a Github bug though; if they allowed zooming on that page it would be fine.
- haldean 13y agoWhat are movie devices? The README is bare text; I like my READMEs to look as good in vim as they do on Github :)
- userbinator 13y agoWhat autocorrect thinks of "mobile devices", I guess. But people do use them an awful lot for watching movies too...
- EpicEng 13y agoEven with OO, what would you suggest? A bunch of classes, classes, inheritance, and virtual methods to replace it? Great, now my logic is spread out all over the place. I know that there are definitely cases in which an OO approach beats a bunch of switch statements scattered throughout the code (specifically, when checking the type of the input variable), but I don't think that applies here. If I saw an interpreter written in that way I would assume it was created by someone who didn't know what they were doing. As an aside, 1) I can't stand the term "code smell" as it tends to be used by hipsters with little to no experience building complex systems, and 2) I realize that you posted your honest thoughts and did a good job of analyzing the approach taken by the author. I'm not trying to prop up a straw man here.
- bromagosa 13y agoWhen most people hear OO they understand C++... NesTalk is a good, real OO aproach to a machine emulator: http://smalltalkhub.com/#!/~zeroflag/NesTalk http://smalltalkhub.com/#!/~zeroflag/NesTalk
- fit2rule 13y agoI just wanted to say that 'code smell' predates hipsters as a term, yo, and a lot of very experience engineers are right when they say 'this code smells' (i.e. I am suspicious of its suitability for consumption..) In my experience the term is something I learned in the 70's from the smelly retiring hippies whose code I had to maintain for a decade or so. ;)
- dalke 13y ago"code smell" in the use that EpicEng complained about dates from the late 1990s. To get a sense of its history, https://books.google.com/ngrams/graph?content=code+smell&year_start=1997&year_end=2008&corpus=15&smoothing=3&share=&direct_url=t1%3B%2Ccode%20smell%3B%2Cc0 https://books.google.com/ngrams/graph?content=code+smell&yea... put a start date of around 1998. It was definitely in Fowler (1999) "Refactoring: Improving the Design of Existing Code". See also http://c2.com/cgi/wiki?CodeSmell http://c2.com/cgi/wiki?CodeSmell , which claims "A code smell is a hint that something has gone wrong somewhere in your code. ... KentBeck ... seems to have coined the phrase in the "OnceAndOnlyOnce" page" and points to "Refactoring" as earliest known use. The phrase you refer to - "this code smells" - and the implication that it reflects hygiene, has different meaning and intent. While related, they are not the same thing as a "code "smell." Compare "the wine smells" vs. "wine smell" to get a sense of how there can be a difference. The modern use of the term "hipster" dates from 1999-2003, claims Wikipedia at http://en.wikipedia.org/wiki/Hipster_%28contemporary_subculture%29 http://en.wikipedia.org/wiki/Hipster_%28contemporary_subcult... , but it's easy to find things like the 1998 alt-comic "Urban Hipster #1" at http://www.indyworld.com/uh/uh01.html http://www.indyworld.com/uh/uh01.html which predate 1999 and use hipster in its modern meaning. In any case, the term 'hipster' is derived from a term in widespread use from the 40s-60s, which then fell into disfavor. To get a rough idea of the change in use pattern, see https://books.google.com/ngrams/graph?content=hipster&year_start=1800&year_end=2008&corpus=15&smoothing=3&share=&direct_url=t1%3B%2Chipster%3B%2Cc0 https://books.google.com/ngrams/graph?content=hipster&year_s... . Obviously the 1940s "hipster" term predates "code smell" as applied to computer code, because there was no computer code in the 1940s. So no, "code smell" does not predate the term "hipster."
- fit2rule 13y agoCompare your observations of this project, with what you may feel about the 6502 emulator in oriculator: https://code.google.com/p/oriculator/source/browse/trunk/6502.c#980 https://code.google.com/p/oriculator/source/browse/trunk/650... Which do you find easier to read/understand/work on? In my case, I still think the #include'd switch case's to be less readable in comparison .. although it must be admitted that oriculator uses the C macro processor in its own nefarious ways .. "READ_ZIX;" indeed .. ;) (BTW, oriculator is a very mature 6502 emulator .. and also rocks as a way to return to the glory days of the 80's machines that never got enough love: the Oric-1 and Atmos...)
- haldean 13y agooriculator is much more powerful and complex than x6502, even you ignore the non-CPU emulation bits (like video and emulated controller, which are complex and difficult). The fact that they manage to get approximately correct timing is really impressive, and involves lots more logic around opcode decoding because you need tables of how many instructions each one takes. That said, I find it much more difficult to read. The reliance on long, complex macros means that I need to flip back and forth between the macro definitions and the opcode interpreter, and long, multi-line macros like they have give me the heebie-jeebies. I'd rather see inline functions used (like x6502 does in functions.h; the functions in functions.h serve much the same purpose as the macros in oriculator). Not to take anything away from the project -- it's obviously an amazing software project and an incredible achievement. We just set out with different goals in mind: mine was more pedagogical, and theirs was more practical.
- drblast 13y agoDepending on the CPU, the compiler, and a whole host of other variables, a switch statement can be one of the better performing ways to write a virtual machine. You certainly want to avoid the overhead of many function calls. And if you're writing a program that does one of hundreds of things based on the value of a byte, the options for really clean code are limited, and you can do a lot worse than a huge switch statement. Here is an interesting article on the topic: http://www.complang.tuwien.ac.at/forth/threading/ http://www.complang.tuwien.ac.at/forth/threading/
- seanwoods 13y agoReminds me of "the highest level feature of C" http://prog21.dadgum.com/166.html http://prog21.dadgum.com/166.html "The possibilities when compiling a switch are much more varied. It can result in a trivial series of if..else statements. It can result in a binary search. Or, if the values are consecutive, a jump table. Or for a complex sequence, some combination of these techniques. If each case simply assigns a different value to the same variable, then it can be implemented as a range check and array lookup. The overall sweep of the solutions, from hundreds of sequential, mispredicted comparisons to a single memory read, is substantial."