13 ms·
John Carmack on Inlined Code
- lambdasquirrel 12y agoI don't have a great deal of experience tuning code, so would someone be able to explain how inlining is related to mutation of state? I'm not John Carmack or Brian O'Sullivan, and I'm not sure if I understand how purity would make things better. We do inline our code in Haskell sometimes, but usually the real gains (in my limited experience, with numerics code) are to be had by unboxing, FWIW.
- tel 12y agoInlining doesn't always work in side-effecting languages. E.g. only if `f` is pure are the following two fragments identical. let a = f () in a + a f () + f ()
- JoshTriplett 12y agoIf the compiler has the full source of f() available at compilation time, it knows whether it can convert between those two. (Though not all compilers are good at knowing the memory-usage implications of doing that kind of commoning operation.) (And, for that matter, whether it can convert that to "f()<<1".)
- mappu 12y agoDoes `shl eax 1` really outperform `add eax eax`? Although i guess that's a question for `-mtune` to decide.
- JoshTriplett 12y agoShift definitely outperforms "mul"; it may or may not outperform "add", but it'll probably outperform a series of adds to implement a larger power of 2. And if you're using the result as part of an address, you can use [eax*2 + ...] as an operand and the shift will happen as part of addressing.
- spc476 12y agoSHL EAX,1 and ADD EAX,EAX are the same, time wise. The only difference is that the A flag (auxiliary carry [1]) is undefined for SHL and defined for ADD. SHL EAX,n may be faster than n repeats of ADD EAX,EAX, but there are other ways to do quick multiplication by powers of 2 though. [1] The auxiliary carry reflects a borrow from bit 4 to bit 3, or a carry from bit 3 to bit 4. It's used for BCD [2] arithmetic, and there is is no direct way to test for it. [2] Binary Coded Decimal
- tel 12y agoSure, and thus inlining does not always work. To be clear, what I probably should have said was beta reduction, but I wanted to keep the terminology of the op. The point being that one has to be aware of state in order to execute an inline/beta reduction. In pure and total[0] code the above can only ever change the execution speed, never the meaning. [0] And of course "pure and total" just means "pure" since non-termination is an effect.
- kazinator 12y agoInlining always works, by definition, otherwise your compiler is buggy. In side effecting languages, inlining cannot make function calls disappear, unless the compiler can convince itself that it's safe to do so. If an expression calls f() twice, and f has a side effect, then the inlined version of the code has to do that side effect twice, exactly if the non-inlined function were called, just without some of the overhead of a function call. (On a different note, in a language like C, inlining a side effect actually improves safety. If we have f() { global++ } then f() + f() is safe and has the effect of incrementing global twice, whereas global++ + global++ is undefined behavior.)
- tel 12y agoSorry, you're using a slightly different definition of inlining than I was. Fortunately, it still demonstrates the same point: you have to be aware of state/side effects in order to do inlining. In a pure and total language the two fragments are always the same. No further thought needed.
- kazinator 12y agoNot really. Inlining can be done by mechanically incorporating the called function into the caller, in a way that respects lexical scopes. This will result in identical semantics. Then the resulting bloated code is optimized: subject to caching optimizations, common subexpression optimizations and whatnot. These have to obey the usual rules for not deleting or reordering side effects. The issue you're referring to boils down to the fact that in an imperative language, we cannot assume that two occurrences of a variable X can be replaced by the same value, because X may have been assigned in between. So compilers have to do flow analysis to discover in what part of a call graph does a given variable have a stable value. E.g. if we have a silly C function like int accumulate(int x, int y) { global += (x + y); } and then call it in two places: accumulate(s, t); accumulate(u, v); the inlining is almost like just macro-expansion of the above into: { int x = s; /* "parameter" passing */ int y = t; global += (x + y); } { int x = u; /* "parameter" passing */ int y = v; global += (x + y); } we don't have to care about side effects when we are doing this expansion. That's the job of later optimization. The later optimization pass can worry about things like whether global is volatile-qualified. If it is, then the best that can be done is: global += s + t; global += u + v; these can't be merged or re-ordered. And so this stays faithful to the original function calls. If global isn't volatile, then more can be done: global += s + t + u + v;
- Guvante 12y agoBut that isn't the kind of inlining he is advocating. He is talking about f _ = do_work 12 let a = do_work 12 in a + a
- pionar 12y agoIn his context, it really has to do with references. So, if I have a value A, and it's a reference type(either by pointer, or by just the nature of the language), then if I pass that object into function DoSomething, DoSomething may change A without my knowledge, and cause behaviors further on that I wasn't expecting. If I inline DoSomething, I see exactly what I'm doing to A, what I'm changing on it.
- zenogais 12y agoMy interpretation: Firstly, inlining has nothing to do with state mutation. He just happens to be talking about a codebase that does a lot of state mutation - eg. a video game. Therefore the functions he's talking about inlining are state mutating functions. It also sounds like performance is a secondary, but still important, concern in this post. What he's really getting at is what the typical modular programming style does to our awareness of details and therefore our ability to understand and optimize our programs. The commonly held conviction is that smaller functions are better for writing understandable and correct code. He's saying this isn't necessarily the case - especially not when you're trying to optimize code and understand the interrelations between the state it's mutating. Modularity can often produce deep stacks while hiding and scattering state mutations. It can also obscure interrelations you should be aware of. In the kinds of scenarios where correctness, comprehensibility, and performance are tantamount having one big long function might just be better.
- phkahler 12y ago>> It also sounds like performance is a secondary, but still important, concern in this post. Not exactly. He points out that in real time systems, worst case execution time is more important than average execution time. You have to render a video frame in 1/60 of a second or it will need to be displayed twice. In that case, getting the job done faster on average doesn't matter. This can change a bit if something like power consumption becomes relevant - then you've got conflicting requirements. Real time keeps performance a top priority, we just look at a different metric.
- geoelectric 12y agoAs a software automation engineer, I've made this argument in the past with regards to UI/systems test code bases. Heavy modularization makes a ton of sense with utility code made to create and tear down fixtures, as well as with general utility functions like navigation through a user interface to get to the initial point of testing. However, in the test, where you need to have complete understanding of the sequence of events in order to keep the test valid, it's much better to inline nearly everything that would affect simulated user or client flow even if that means duplication between tests. That's exponentially more true when more than one person/team/org/whatever would be maintaining different tests or test areas. The last thing you want to do at that point is share test sequencing code, since it's so easy to subvert the flow in other tests using it by mistake. It's a hard argument to make, because everyone gets SPOT, yo, Fowler rules, etc. They aren't wrong, but it really only applies when the interface is everything and how you fulfill the interface is irrelevant. For some types of code--frame-accurate games being one, tests being another--the order of events is paramount. IMO, even mature patterns like Selenium's Page Object Model gets this one wrong by encouraging test flow code to live in POM methods. There are absolutely times where optimizing for understandability and being paranoid about implementation changes is the way to go.
- deleted 12y ago[deleted]
- cygx 12y ago"inlining" means that instead of hand-writing blocks of manually inlined code, you write a function, which can be inlined for you. That's backwards: Carmack advocates to avoid writing such functions, manually inlining their code into the caller (with the possible exception of pure functions).
- ufo 12y agoI think the article was about "manual inlining" rather than the kind of automatic inlining that a compiler does. Its less about raw perfomance (inlined functions have less function call overhead and open opportunities for cross-function optimization) and its more about correctness and predictability of runtime. For example, he mentions that he prefers having a dumb and sequential code flow that always runs at 60 FPS than something clever and full of conditionals that runs at 120 FPS most of the time but falls to 30 FPS every once in a while. In addition the predictable runtime there are also many bugs that happen when stateful functions are called in the wrong order and this is less of an issue if you always call them once in the same order. For example, he mentions that he would rather check for `health <= 0 && !killed` at the end of your game loop instead of potentially calling KillPlayer() in 20 different places, which might end up doing slightly different things in each frame. As for pure code, the reason he has become more bullish about functional programming is that it by definition is less susceptible to all these subtle order-of-execution issues. You are free to structure your code in whatever way you like and split it into as many functions as you want and still have the peace of mind that you will never access an uninitialized variable or use a dead object.
- vonmoltke 12y agoBack when I was writing real-time signal processing code, I spent quite a bit of time refactoring code from variations of styles A and B to style C. The problem Carmack talks about is even worse when some of those subroutines were in separate source files. Over the course of my refactorings, I was able to re-arrange and combine functionality in ways that were not possible with the discrete functions. I was able to pull operations out of inner loops that repeatedly and expensively calculated the same values. I found and fixed all sorts of actual or potential bugs in doing this. The original code was extra hairy, though, because it was written for DSPs, not general-purpose microprocessors. As a side note, the original code contained large numbers of manual loop unrolling optimizations like noted in the email. I actually saw a performance increase from removing them. Same in some cases for manually inlining the function calls. From what I could tell, writing simpler, inline code made the optimizer much more efficient.
- AdmiralAsshat 12y agoWouldn't in-lining make unit testing more difficult? Admittedly I am not a very experienced programmer, but I thought the general line of thought with regards to making your program easier to test would be that each function should do as little as possible.
- clarry 12y agoYes it would, but Carmack is advocating the inlining of functions that deal with game state. These tend to be kinda hard to test in isolation anyway. But he does also advocate keeping small helper functions pure.
- lutusp 12y ago> Wouldn't in-lining make unit testing more difficult? That depends on the nature of the inline code. If it simply unrolls a local loop that has no side effects, then it's structurally identical to the original loop, but (in some cases) faster. This is demonstrated by the fact that some compilers can be coerced to inline certain repetitive actions originally written in the form of loops, as a speed optimization.
- 12y ago
- tdicola 12y agoHaha, I like this quote: "That was a cold-sweat moment for me. After all of my harping about latency and responsiveness, I almost shipped a title with a completely unnecessary frame of latency."
- sssilver 12y agoA good insight into his mindset and way of seeing things.
- deleted 12y ago[deleted]
- DrTung 12y agoThe Saab Gripen aircraft that is mentioned did crash anyway (in its first flight show over Stockholm 1993 http://en.wikipedia.org/wiki/Accidents_and_incidents_involving_the_JAS_39_Gripen http://en.wikipedia.org/wiki/Accidents_and_incidents_involvi...) in spite of that specially crafted fly-by-wire flight software...
- deleted 12y ago[deleted]
- idlewords 12y agoYour comment implies there was some fault in the software. The wikipedia link explains that the crash was due to pilot-induced oscillation (a coupling between the plane's response to inputs and the pilot's reaction time). Basically, the plane would have been better off cutting off input from the pilot and flying on its own.
- vilhelm_s 12y agoI'm not sure it's reasonable to characterize this as due to a "bug" though---the software correctly implemented the specified control laws, but those laws had some unanticipated properties, and fixing it required some new developments in control theory. The 1993 crash was due to Pilot Induced Oscillation (PIO). This is a general term for situations when the pilot makes inputs to stabilize an airplane, but the inputs instead end up exacerbating the instability. A simple example of how this could happen is if the control inputs for some reason take effect with a time delay: the airplane pitches up, so the pilot tries to push it down, after a moment the transient passes and the plane pitches down so the pilots pulls up, but the previous input amplifies the downwards movement so the pilot pulls up harder, etc. Several of the first generation of unstable fly-by-wire airplanes had problems with PIO. Unlike conventional aircrafts, where the rudder positions exactly follow the position of the stick, here the desired rudder position is calculated as the sum of two inputs, one calculated from the stick position, and one calculated by flight control software to dampen instabilities. Early versions of the software was "rate limited", i.e. at each iteration of the main loop the software calculates the desired rudder position and then moves the rudders towards that position at the fastest rate the rudder actuators allow. However, that leads to problems when there are very large transient stick inputs: because the rudders take some time to move, the largest rudder deflection occurs with some delay after the largest stick input (see figure 6 in [1]). In the 1993 crash there was a wind gust causing a pitch movement, and both the pilot and the flight control software provided a compensation. The sum of the two signals was big enough to hit the rate-limitation, so the response of the airplane was strange, the pilot gave several more large inputs, and there was PIO. Incidentally, one of the YF-22 prototypes crashed for basically the same reason, even though they run different software. The solution was to develop some new "phase compensation" methods for designing controllers [1]. [1] http://ieeexplore.ieee.org/xpls/abs_all.jsp?arnumber=558586&tag=1 http://ieeexplore.ieee.org/xpls/abs_all.jsp?arnumber=558586&...
- javert 12y agoThe following alternative style (vs. the 3 presented by Carmack) is one that I find very easy to read. algorithm() { do_small_thing_one(); do_small_thing_two(); do_small_thing_three(); ... do_small_thing_X(); } Advantages over Carmack's Style C: 1. Substitutes comments for accurate and specific function names. Why better? Because comments can get out of sync with the code. 2. You can quickly see the sub-steps of the algorithm, rather than reading a multi-page-long giant function with a ton of comments. When using this style, the inner functions are not visible outside of that source file (you can arrange this depending on your programming language). Then it's easy to make sure they are only called once within the source file, or only called appropriately. That's because I agree with Carmack that functions called from lots of unrelated places are a terrible thing. (Edited for clarity after people pointed out that it seemed like I was just advocating for Carmack's style A or B.)
- pajtai 12y agoThat is option A and B. I think the entire article is describing the down sides of that. You can't see if you a repeating the same stuff in multiple small things.
- javert 12y agoI guess this kind of got lost in my comment, but the point is that these functions are never called from anywhere else. You achieve that by not exposing them in header files.
- roghummal 12y agoIf they aren't called from anywhere else and they're only called once (in the parent), it might be better to inline them. Doing so goes against the urge to decompose as much as possible but it'd make the code easier to follow for the next guy. "What does do_small_thing_X do?" Even when the next guy is you, a year from now. Do you really remember what do_small_thing_X does?
- 12y ago
- tantalor 12y ago> do always, then inhibit or ignore Explain?
- ShaunK 12y agoRather than conditionally execute code (to avoid performing an unnecessary expensive operation) always execute the code, then discard the result if it is unneeded. The idea being that the performance gained by avoiding the unnecessary operation is not worth the complexity added.
- chillacy 12y agoAlso adds consistency, which I'm sure is important in realtime games
- Guvante 12y agoAs a minor note, conditional checks can have significant performance impacts if you don't consistently handle the conditional. As an example performing an operation every other frame can cause your CPU to thrash due to always taking the wrong path. (I of course am oversimplifying and OOO CPUs are quite complex)
- deleted 12y ago[deleted]
- deleted 12y ago[deleted]
- ranran876 12y agoI might be alone on this, but whenever I read things by John Carmack I get a vague sense that he doesn't really get object oriented programming. He always has a lot of interesting things to say, but it also kinda reads like a C guy trying to code in C++. I'm glad his thinking keeps evolving and he's not dogmatic about anything. I'd honestly love to hear his thoughts on C++11 "The function that is least likely to cause a problem is one that doesn't exist, which is the benefit of inlining it." That's the equivalent of saying "the faster you drive the safer you are b/c you're spending less time in danger" You'll just end up with larger monster functions that are harder to manage. "Method C" will always be a disaster for code organization b/c your commented off "MinorFunctions" will start to bleed into each other when the interface isn't well defined. " For instance, having one check in the player think code for health <= 0 && !killed is almost certain to spawn less bugs than having KillPlayer() called in 20 different places" I don't completely get his example, but I see what he's saying about state and bugs that arise from that. You call a method 20 times and it has an non obvious assumption about state that can crop up at a later point - and it can be hard to track down. However the flip side is that when you do track it down, you will fix several bugs you didn't even know about. The alternative of rewriting or reengineering the same solution each time is simply awful and you'll screw up way more often
- Guvante 12y ago> The alternative of rewriting or reengineering the same solution each time is simply awful and you'll screw up way more often He might not have communicated it completely correctly, but I believe he wasn't advocating for getting rid of functions to reduce redundancy. He instead was advocating getting rid of functions that simply provide documentation of the process, and instead find a way to inline those functions clearly. > However the flip side is that when you do track it down, you will fix several bugs you didn't even know about. I think he is saying a class of bugs is avoided. For instance if I do X, Y and Z where all are only ran when the player is alive and Y might kill the player, leaving the player alive avoids a bug in Z if it assumes that the player is alive.
- phkahler 12y ago>> I don't completely get his example To use Minecraft as an example, a player may die from falling from too high, drowning, getting attacked by a monster. If killPlayer() is called serparately for each of those cases, he asserts that it may cause bugs due to differing context or sequencing relative to other parts of the code. If OTOH you just decrement player health in each of those places and then check for health<=0 at only one place, you eliminate that class of bugs.
- dzuc 12y agoI'm assuming this was posted because it was brought up during Jonathan Blow's talk last night: http://www.twitch.tv/naysayer88/b/572153991 http://www.twitch.tv/naysayer88/b/572153991 (which have been interesting, (and Twitch is a great format for these))
- cygx 12y agoit is often better to go ahead and do an operation, then choose to inhibit or ignore some or all of the results, than try to conditionally perform the operation. The way we have traditionally measured performance and optimized our games encouraged a lot of conditional operations [...] This gives better demo timing numbers, but a huge amount of bugs are generated because skipping the expensive operation also usually skips some other state updating that turns out to be needed elsewhere. Now that we are firmly decided on a 60hz game, worst case performance is more important than average case performance, so highly variable performance should be looked down on even more. Two words: Battery life. In case of mobile devices, this is not sound advice.
- nkurz 12y agoIs this covered by the update in John's introduction? To make things more complicated, the .do always, then inhibit or ignore. strategy, while a very good idea for high reliability systems, is less appropriate in power and thermal constrained environments like mobile. Or do I misunderstand your objection?
- cygx 12y agoNo, you're completely right - I only skimmed the addendum. In fact, I actually only skipped that particular paragraph - Murphy's law strikes again :( It's just that two days ago, I had to deal with exactly that issue, so it was fresh on my mind while reading the article...
- ufo 12y agoLooks like Carmack was right then. Conditional paragraph execution does lead to extra bugs :)
- uzfzjvzjftzdtz 12y agoHe states the same on that very web page.
- corysama 12y agoThe older I get, the more my code (mostly C++ and Python) has been moving towards mostly-functional, mostly-single static assignment (let assignments). Lately, I've noticed a pattern emerging that I think John is referring to in the second part. The situation is that often a large function will be composed of many smaller, clearly separable steps that involve temporary, intermediate results. These are clear candidates to be broken out into smaller functions. But, a conflict arises from the fact that they would each only be invoked at exactly one location. So, moving the tiny bits of code away from their only invocation point has mixed results on the readability of the larger function. It becomes more readable because it is composed of only short, descriptive function names, but less readable because deeper understanding of the intermediate steps requires disjointly bouncing around the code looking for the internals of the smaller functions. The compromise I have often found is to reformat the intermediate steps in the form of control blocks that resemble a function definitions. The pseudocode below is not a great example because, to keep it brief, the control flow is so simple that it could have been just a chain of method calls on anonymous return values. AwesomenessT largerFunction(Foo1 foo1, Foo2 foo2) { // state the purpose of step1 ResultT1 result1; // inline ResultT1 step1(Foo1 foo) { Bar bar = barFromFoo1(foo); Baz baz = bar.makeBaz(); result1 = baz.awesome(); // return baz.awesome(); } // bar and baz no longer require consideration // state the purpose of step2 ResultT2 result2; // inline ResultT2 step2(Foo2 foo) { Bar bar = barFromFoo2(foo); // second bar's lifetime does not overlap with the 1st result2 = bar.awesome(); // return bar.awesome(); } return result1.howAwesome(result2); } I make a point to call out out that the temp objects are scope-blocked to the minimum necessary lifetimes primarily because doing so reduces the amount of mental register space required for my brain to understand the larger function. When I see that the first bar and baz go out of existence just a few lines after they come into existence, I know I can discard them from short term memory when parsing the rest of the function. I don't get confused by the second bar. And, I don't have to check the correctness of the whole function with regards to each intermediate value.
- swah 12y agoExactly, and he also suggests that in the email: "(...) and often enclosing it in a bare braced section to scope the local variables and allow editor collapsing of the section is useful". I hadn't understood this "maybe leave functions inlined" rant a couple years ago when I first heard about it - it makes a lot of sense now.
- mr_brown 12y agoI never wrote too many C or C++ on the desktop, but often ended up refactoring my embedded code from one style to an other. After a while I realized this is simply my way of understanding the code better, and making sure I haven't missed anything. The direction (inlining or breaking things to functions) almost doesn't matter. What matters is working with the code. It's not so strange If you think about it, designers understand things by sketching and taking notes, that's why you see designers run around with their moleskins.
- protonfish 12y agoIf anyone other than Carmack wrote this, I doubt it would be so well received so I'm glad he did. We all have our own programming dogma that we love and defend religiously, but we should never stop asking if our code is truthfully, objectively, clear and easy to read, prone to bugs and/or runs efficiently. "Best practices" can get you 80% of the way there, but a developer should never stop questioning the quality of their code, even if it contradicts the sacred rules.
- ssadler 12y agoWould that apply to any craft?
- dmjio 12y agoWe'll all be programming in Haskell soon enough
- VikingCoder 12y agoIs anyone else reminded of Facebook's Flux? http://www.infoq.com/news/2014/05/facebook-mvc-flux http://www.infoq.com/news/2014/05/facebook-mvc-flux
- tgb 12y agoI'm not a professional programmer and I rarely work with large code bases. So the fact that my code has drifted steadily over the years towards the large-main-function I thought was a factor of several things, first being my general amateurism. I still think that, but there are definitely other reasons too: I now use more expressive languages (Python instead of C) and more expressive idioms within those languages (list comprehensions instead of while loops) and more expressive structures/libraries (NumPy instead of lists of structures), so I can afford to put more in one spot. I also write smaller but more numerous programs. But there are very real advantages. I learned through game programming and still do some for fun and I absolutely prefer having a main loop that puts its fingers into all the components of the game than to have a main loop which delegates everything to mysterious entity.update()-style functions. The lack of architecture allows me to structure the logic of the game more clearly for exactly the reasons Carmack outlines. Everything is sequenced - what has already happened in the frame can be seen by scrolling up a bit instead of digging through a half-dozen files. But the real win here is for the beginner programmer. I strongly dislike the trend these days towards programming education being done in a "fill in the blanks" manner where the student takes an existing framework and writes a number of functions. The problem is that the student rarely has any idea what the framework is doing. I would rather not have beginners write games by make on_draw(), on_tick(), etc. functions but much rather have them write a for loop and have to call gfx_library_init() at program start and gfx_library_swap_buffers() at the end of a frame. That way they can say "The program starts here and steps through these lines and then exits here" versus having magic frameworks do the work for them. There is plenty of magic done these days behind the scenes for any beginner, but it is too much to have a completely opaque flow-control.
- angersock 12y agoSo, that's the difference between beginners and professional engineers, right? If you use a framework (or at least a common pump_messages->update_ents->render cycle), it's a hell of a lot easier for other people to work on your code for a longer period of time (and those other people include yourself). Imagine if you decide to change your gfx_library with something new, especially if you had decided to scatter it's draw calls all over the "game logic" (instead of, say, a dedicated entity draw() method). What about when you start allowing data-driven entity updates? What about <insert practical concern here>? To be fair, you take it too far (as I'd once done) and you destroy performance. So, eventually, you work out how to have nice architectures that still support the fast hacky stuff. Anyways, the beginner is allowed to make such mudballs for a while--but no longer than necessary!
- deleted 12y ago[deleted]
- Igglyboo 12y agoYea but the point is readability not performance. If I have 50 functions that are only called once it will be hard for someone who hasn't read the code to immediately realize that which will make it more difficult to modify these functions, if they're all inlined there's no issue.
- deleted 12y ago[deleted]
- rza 12y ago> "...immediately realize that which will make it more difficult to modify these functions". Please clarify what you mean here. I absolutely would prefer a method to look like: def main(): SetUp() try: DoStuff() except: HandleErrors() then a thousand-line god function. It encourages modularity, reduced state and scope (please ignore my example above which implies a lot of shared state:)), and every method should be well-named and do a single thing well. Maybe for super-sensitive realtime systems, this might cause a few necessary checks that we could ignore, but we are talking about readability here. Having to navigate between different methods is honestly a really lame excuse for sticking everything in one method. That's what IDE's, documentation, and good method names are for.
- taeric 12y agoSeeing this makes me realize the beauty of CWEB. It lets you break up a piece of the code this way, but when it presents the code that is in "DoStuff" it explicitly tells you where else it is used. That is, the concern with having a DoStuff method is that if it gains a new caller, it may have just gained new requirements. This will eventually pose a problem, especially when it is believed that one can edit functions in isolation to all of the expectations of existing call sites. Explicitly listing the call sites, though, goes a long way to understanding why "DoStuff" does what it does.
- jameshart 12y agoThere's an interesting game programmer problem here, that is somewhat alien to a coder like me who grew up on the web. Where for a web coder, statelessness is the default, and we have to work to recover and recreate state between 'frames', game coders live in the run loop - and so the assumption is that you have a repository of persistent global state to act on each frame. Having noticed that he has a problem when multiple functions are all interacting with that same shared global state, it's kind of amusing that Carmack's reaction is to reduce the number of functions, rather than remove the global state.
- chipsy 12y agoAt its core the problem game programmers run into is that game state is very globalized with a lot of dependency overlap. You can push around the data into different containers and declare dogmatic methods of access, but you always wind up with the same problems: The animation state depends on the physics. The rendering depends on the animation state. The physics of a local entity depend on its collision with the rest of the world. The results of physics depend on which things collided first. And so on and so forth. And so games live within this environment of confusion over which things happen when. At every point there are a few defensive tools - queue up actions in a buffer, poll values instead of copying them, etc. - but the overall management of these concurrent, overlapping systems remains a challenging task lacking in silver bullets.
- petersellers 12y agoSeems like a big argument against type C is that it would be more difficult to unit test the code. The nice thing about A/B (which are essentially the same to me) is that each subroutine becomes an easier target for unit testing.
- cpeterso 12y agoSteve McConnell's classic "Code Complete: A Practical Handbook of Software Construction" cites a number of studies of real systems from the 1970s and 1980s with some surprising results. Some studies showed that function size inversely correlated with bug counts; as functions increased towards 200 LOC, the bug count decreased. Similar, another showed that shorter functions had more bugs per LOC and reduced comprehension for programmers new to the system. Another study showed that function complexity was correlated with bug count, but size alone wasn't.
- hisham_hm 12y agoAfter reading this, I feel a lot better about the huge main() function I wrote for htop. I've always thought about splitting it into many functions, but somehow keeping it all flowing in sequence just made more sense.
- notastartup 12y agoIs this arguing that developers shy away from the forced OOP, and Patterns, instead relying on simple to read, step by step, functions? One of my biggest gripe about OO programming was that you had no idea what the other components were doing unless you investigated each component directly. Sometimes the dependencies and the chain would get so large and complicated, you'd spend more time figuring out how to wrap your head around the whole thing than doing things that result in direct business benefit. But every interview you go to will tell you otherwise, inflating technical debt is a great thing to keep managers keep their job and for sales team to boast about six digit LOC = Obviously state of the art.
- AnimalMuppet 12y ago> "The function that is least likely to cause a problem is one that doesn't exist, which is the benefit of inlining it." That statement (at least taken in isolation) is false. Inlining it means that you're still executing the exact same code. If it had problems as a function, it still has problems when inlined. But that isn't the problem that Carmack is trying to address. He's concerned about bugs caused by lack of programmer comprehension of the code's role in the larger function. It's a valid concern. But inlining it makes it harder to find problems in the details of what the inlined function does (or even to realize that that's where the problem is, or maybe even to realize that there's a problem at all). All styles help with some problems and make others worse. The answer isn't a style, it's good taste and experience to know when to use which style.
- skylan_q 12y agoInlining it means that you're still executing the exact same code. If it had problems as a function, it still has problems when inlined. But if it's inlined, it's no longer a function. ;) What he's saying here is that the function itself was fine and free of bugs but that there is a problem for the programmer. The programmer's understanding and expectation of what the function does isn't in that it affects state in a way the programmer didn't know or expect. What the function does becomes much more obvious and controllable when you inline the function's body.
- akkartik 12y agoAn aside on tool interactions: For the past year or so I've been using a new way to do syntax highlighting[1][2] that works really well for highlighting the dataflow in large functions (whether well or poorly written): http://i.imgur.com/EmFMTtv.png http://i.imgur.com/EmFMTtv.png [1] https://medium.com/@evnbr/coding-in-color-3a6db2743a1e https://medium.com/@evnbr/coding-in-color-3a6db2743a1e [2] http://www.reddit.com/r/programming/comments/1w76um/coding_in_color/cezpios http://www.reddit.com/r/programming/comments/1w76um/coding_i...
- mturmon 12y agoThat's a cool idea I had never seen before, thanks.
- frik 12y agoGreat idea, so simple yet no IDE provide this feature out of the box.
- regularfry 12y agoKDevelop (specifically Kate) does this.
- Too 12y agoI've seen a plugin to some ide, can't remember if it was eclipse or pycharm, that would highlight the last three variables you had selected. Similar to what most editors do with the current variable under cursor but with a short memory.
- chetanahuja 12y agoI found the functional programming (in C++) advice post linked from the referenced post a much more interesting read. http://gamasutra.com/view/news/169296/Indepth_Functional_programming_in_C.php http://gamasutra.com/view/news/169296/Indepth_Functional_pro... "Avoid globals" is a fairly common (and good) truism for programmers of all stripes. But casting it in light of the central (and easily digestible) tenet of functional programming makes it much more approachable. Smart (but sometimes insufferably pompous ) proponents of functional programming should take notes.
- curiousCoffee 12y agoDoes anyone write in style C and then refactor to style A/B? That gives you all the benefits of both styles..
- Too 12y agoNo, the problem with style A and B is that smallFunction() might have only been correct under the context of it executing inside largeFunctionA(). IMO this risk is actually increasing even more if it originates from a refactoring from style C, since you didn't design smallFunction() from bottom up considering all possible use cases, you most likely just highlighted a random block in largeFunctionA() because it was getting too big and clicked "extract method" in your IDE. Imagine two months later someone writing largeFunctionB() is browsing around the code and finds smallFunction(), thinking it will do the job he requires but actually it has a hidden bug that was never triggered under the context of it executing in largeFunctionA or under the limited input range that largeFunctionA was using. See in particular this paragraph from the article: Besides awareness of the actual code being executed, inlining functions also has the benefit of not making it possible to call the function from other places. That sounds ridiculous, but there is a point to it. As a codebase grows over years of use, there will be lots of opportunities to take a shortcut and just call a function that does only the work you think needs to be done. There might be a FullUpdate() function that calls PartialUpdateA(), and PartialUpdateB(), but in some particular case you may realize (or think) that you only need to do PartialUpdateB(), and you are being efficient by avoiding the other work. Lots and lots of bugs stem from this. Most bugs are a result of the execution state not being exactly what you think it is.
- curiousCoffee 12y agoOh yeah I see what you mean. Do you think all smallFunction()s should be generic/reusable? Seems like it's the fault of the second developer for using the smallFunction() without understanding what it does.
- ilaksh 12y ago> If something is going to be done once per frame, there is some value to having it happen in the outermost part of the frame loop, rather than buried deep inside some chain of functions that may wind up getting skipped for some reason > I do believe that there is real value in pursuing functional programming, but it would be irresponsible to exhort everyone to abandon their C++ compilers and start coding in Lisp, Haskell, or, to be blunt, any other fringe language. "Here, let me dismiss functional programming, and by the way OCaml and other 'non-pure' functional languages don't exist, and functional programming languages aren't useful for anything 'real' so you should do your functional programming in C, and also you may want to dump everything in one long-ass function because I don't like deep stacks". He's just rationalizing C traditions.
- jaunkst 12y agoI believe in both functional and encapsulated patterns. It all boils down to scope of the task at hand. There is a certain kind of beauty in programming in a pattern than can compliant to a particular interface and a pattern that's efficient and scoped to the result required. Inline is a great way to encapsulate in a functional way.
- throwaway9134 12y agoA few questions: Are there any good examples of code written in this style (e.g. by Carmack or Blow)? When I tried this style, I would frequently end up with 800+ line functions: is this what the code is supposed to look like in the end, or should I be refactoring earlier? When I do end up refactoring, it's often hard to switch to a functional style. There are complex dependencies between the different "minor" functions, and the easiest route forward seems to be to replace the function with a class: minor functions become methods, and the function-level local variables become instance variables, etc. Also, this is a small issue, but how do you deal with minor functions that "return" variables? I typically wrap the minor functions in brackets, and I declare the "returned" values as local variables right before opening bracket, but it looks strange.
- darylteo 12y agoSeems like everyone misreading the intent of the post. He is NOT advocating inlining code in this post. He is suggesting that FP is better at solving the same problems in a more sensible way. The email was written in 2007. In there, he advocates the inlining of single-use functions into god functions as it reduces the risk of these functions being opted into other routines, especially when they all deal with shared mutable data. Single-use functions are explicitly singled out in his email; he mentions that he does not encourage duplicating code to avoid functions. | "In almost all cases, code duplication is a greater evil than whatever second order problems arise from functions being called in different circumstances, so I would rarely advocate duplicating code to avoid a function" The blurb at the front indicates the intent of his post. Since then he has favoured a functional-programming approach - don't inline your functions, but avoid making your functions rely on mutable/larger scope states. Pass in everything that is needed by the function through parameters. Avoid functions with side-effects, encourage idempotence. That way, reusing the function does not lead to unintentional side-effects. He also mentions that should you still decide to inline, " you should be made constantly aware of the full horror of what you are doing.". A lot of things change within a decade. =)
- highCs 12y ago> If a function is only called from a single place, consider inlining it. Funny because it's backward. If a code is duplicated, consider to make a function if the pieces of code are the same semantically. (Two pieces of code which are the same at a given time can diverge over time and you don't want to miss that. Only analyzing the sense of what you're doing (=semantic) gives you the answer.) Never add fancy things (like adding a function which is not a function) in your code because code is not fancy, it causes bugs. > If a function is called from multiple places, see if it is possible to arrange for the work to be done in a single place, perhaps with flags, and inline that. Well yeah, fix the semantic if it needs to else do nothing. > If there are multiple versions of a function, consider making a single function with more, possibly defaulted, parameters. Well yeah, fix the semantic if it needs to else do nothing. >Minimize control flow complexity and "area under ifs", favoring consistent execution paths and times over "optimally" avoiding unnecessary work. The right thing to do is to never optimize unless it's to slow and you've identified the first bottleneck. "Never optimize" means: write the naive code correctly (without performance aberation like adding element in an array). > To sum up: Stop fancy. Stop optimization. Stop thinking about code syntactically (=the succession of operation gives the good result). Think constantly about your code semantically.
- phlakaton 12y agoThere is an interesting parallel between Mr Carmack's "inlining" observations and one of the sessions I went to at Strange Loop this year. Jonathan Edwards was trying to beat back "callback hell" (i.e. unpredictable execution order leading to unpredictable side effects) by radically simplifying the control flow of his programs and keeping the execution model dirt-simple. Both would appear to argue that it's best to arrange heavily stateful code in a simple linear sequence, and use a top-down execution model, so that stateful effects are clear and easy to predict. That being said, I've seen procedures that followed this sort of approach that were thousands of lines long. Even if we could have cut down on the ridiculous number of conditionals in that code, most of the state at that scale asymptotically approaches an undifferentiated mass of global variables. The result is testable and maintainable only via heroic effort. There have got to be limits to this kind of approach. (For Mr Edwards the solution was to break the whole thing up into a sequence of composable views, or lenses, with the interface between each stage being well-defined.) I wonder to what extent Mr Carmack's pivot to pure functions is simply an acknowledgement that there were much better ways to refactor the code than the mess of one-timer procedures that probably seemed like a good idea the first time through...
- notastartup 12y agoHow would you explain the benefits of functional programming with an employer who absolutely refuses to believe that OOP is overvalued? Lot of job requirements will say experience with OOP and then be asked to recite from memory what Singleton patterns look like or draw a Factory pattern as a yardstick of developer efficiency.
- eru 12y agoYou can do a prototype, and try to convince like that. Or just do your own thing and don't mention any paradigm labels. Or you can look for a more enlightened employer. Mine is hiring, for example.
- tome 12y agoWhat sort of functional programming jobs do they have available and in what languages?
- eru 12y agoGoogle is pretty limited in the functional languages as a main tool for your job category. It's easy enough to sneak in functional things here and there, though. Standard Chartered, where I worked before doing FP, is looking for Haskellers every now and then. Contact me (see profile) for some more info. Citrix is still using OCaml in Cambridge, UK, as far as I know. Not as much Haskell any more as they used to.
- notastartup 12y agohow do I apply for your company? seriously, sounds awesome.
- eru 12y agoContact details are in my profile (or write to me via orb@google.com). I currently work for Google. They are pretty enlightened in general, but somewhat conservative in official language choice for big projects, but it is possible to do prototypes and smaller projects in a wide variety of languages. (Eg we have a Haskell group, and of course there are 20% projects.)