October 6th, 2024

John Carmack on Inlined Code

John Carmack highlights the benefits of inlining functions for improved code clarity and reliability, warning against unexpected state changes and advocating for performance-focused coding in game development.

Read original articleLink Icon
CuriosityFrustrationAppreciation
John Carmack on Inlined Code

John Carmack's commentary on inlined code, originally shared in a 2007 email, emphasizes the importance of coding style and its impact on software reliability and performance. He advocates for inlining functions to enhance awareness of code execution and reduce unexpected state mutations, which can lead to bugs. Carmack reflects on his experiences with game development, particularly with the Doom 3 BFG edition, where he encountered latency issues that he had previously warned against. He discusses the benefits of a coding style that minimizes function calls, particularly in performance-sensitive environments like gaming, where operations should be executed in a clear and predictable manner. He contrasts different coding styles, suggesting that inlining can lead to cleaner and more reliable code. Carmack also notes the risks associated with global state changes and the potential for bugs arising from function calls that may not behave as expected. He concludes that while purely functional programming has its merits, a balanced approach that considers the practicalities of game development is essential.

- John Carmack emphasizes the advantages of inlining functions for better code clarity and reliability.

- He warns against the risks of unexpected state changes and bugs from function calls in complex systems.

- Carmack reflects on his experiences with latency issues in game development, advocating for a coding style that prioritizes performance.

- He discusses the importance of awareness in code execution, particularly in real-time applications like gaming.

- The commentary suggests that while functional programming has benefits, a pragmatic approach is necessary for effective software development.

AI: What people are saying
The discussion around John Carmack's views on inlining functions reveals several key themes among the comments.
  • Many commenters emphasize the balance between clarity and performance in coding, with some advocating for inlining to enhance readability.
  • There is a debate on the merits of functional programming versus traditional inlining, with some suggesting that pure functional programming offers greater benefits.
  • Several participants express concerns about the scope of variables in inlined functions, highlighting potential issues with debugging and maintainability.
  • Commenters reflect on the historical context of Carmack's statements, noting how coding practices have evolved over time.
  • Some users share personal experiences and preferences regarding function granularity, indicating a divide in opinions on the ideal approach to coding structure.
Link Icon 43 comments
By @mihaic - 7 months
When I first heard the maxim that an intelligent person should be able to hold two opposing thoughts at the same time, I was naive to think it meant weighing them for pros and cons. Over time I realized that it means balancing contradictory actions, and the main purpose of experience is knowing when to apply each.

Concretely related to the topic, I've often found myself inlining short pieces of one-time code that made functions more explicit, while at other times I'll spend days just breaking up thousand line functions into simpler blocks just to be able to follow what's going on. In both cases I was creating inconsistencies that younger developers nitpick -- I know I did.

My goal in most cases now is to optimize code for the limits of the human mind (my own in low-effort mode) and like to be able to treat rules as guidelines. The trouble is how can you scale this to millions of developers, and what are those limits of the human mind when more and more AI-generated code will be used?

By @ninetyninenine - 7 months
His overall solution highlighted in the intro is that he's moved on from inlining and now does pure functional programming. Inlining is only relevant for him during IO or state changes which he does as minimally as possible and segregates this from his core logic.

Pure functional programming is the bigger insight here that most programmers will just never understand why there's a benefit there. In fact most programmers don't even completely understand what FP is. To most people FP is just a bunch of functional patterns like map, reduce, filter, etc. They never grasp the true nature of "purity" in functional programming.

You see this lack of insight in this thread. Most responders literally ignore the fact that Carmack called his email completely outdated and that he mostly does pure FP now.

By @VyseofArcadia - 7 months
> 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.

In this era of 3-5 frame latency being the norm (at least on e.g. the Nintendo Switch), I really appreciate a game developer having anxiety over a single frame.

By @gorgoiler - 7 months
> Inlining functions also has the benefit of not making it possible to call the function from other places.

I’ve really gone to town with this in Python.

  def parse_news_email(…):
    def parse_link(…):
      …

    def parse_subjet(…):
      …

    …
If you are careful, you can rely on the outer function’s variables being available inside the inner functions as well. Something like a logger or a db connection can be passed in once and then used without having to pass it as an argument all the time:

  # sad
  def f1(x, db, logger): …
  def f2(x, db, logger): …
  def f3(x, db, logger): …
  def g(xs, db, logger):
    for x0 in xs:
      x1 = f1(x0, db, logger)
      x2 = f2(x1, db, logger)
      x3 = f3(x2, db, logger)
      yikes x3


  # happy
  def g(xs, db, logger):
    def f1(x): …
    def f2(x): …
    def f3(x): …
    for x in xs:
      yield f3(f2(f1(x)))
Carmack commented his inline functions as if they were actual functions. Making actual functions enforces this :)

Classes and “constants” can also quite happily live inside a function but those are a bit more jarring to see, and classes usually need to be visible so they can be referred to by the type annotations.

By @BenoitEssiambre - 7 months
Here are some information theoretic arguments why inlining code is often beneficial:

https://benoitessiambre.com/entropy.html

In short, it reduces scope of logic.

The more logic you have broken out to wider scopes, the more things will try to reuse it before it is designed and hardened for broader use cases. When this logic later needs to be updated or refactored, more things will be tied to it and the effects will be more unpredictable and chaotic.

Prematurely breaking out code is not unlike using a lot of global variables instead of variables with tighter scopes. It's more difficult to track the effects of change.

There's more to it. Read the link above for the spicy details.

By @dang - 7 months
Related:

John Carmack on Inlined Code - https://news.ycombinator.com/item?id=39008678 - Jan 2024 (2 comments)

John Carmack on Inlined Code (2014) - https://news.ycombinator.com/item?id=33679163 - Nov 2022 (1 comment)

John Carmack on Inlined Code (2014) - https://news.ycombinator.com/item?id=25263488 - Dec 2020 (169 comments)

John Carmack on Inlined Code (2014) - https://news.ycombinator.com/item?id=18959636 - Jan 2019 (105 comments)

John Carmack on Inlined Code (2014) - https://news.ycombinator.com/item?id=14333115 - May 2017 (2 comments)

John Carmack on Inlined Code (2014) - https://news.ycombinator.com/item?id=12120752 - July 2016 (199 comments)

John Carmack on Inlined Code - https://news.ycombinator.com/item?id=8374345 - Sept 2014 (260 comments)

By @dehrmann - 7 months
Always read older stuff from Carmack remembering the context. He made a name for himself getting 3D games to run on slow hardware. The standard advice of write for clarity first, make sure algorithms have reasonable runtimes, and look at profiler data if it's slow is all you need 99% of the time.
By @low_tech_love - 7 months
Interesting: this is a 2014 post from Jonathan Blow reproducing a 2014 comment by John Carmack reproducing a 2007 e-mail by the same Carmack reproducing a 2006 conversation (I assume also via e-mail) he had with a Henry Spencer reproducing something else the same Spencer read a while ago and was trying to remember (possibly inaccurately?).

I wonder what is the actual original source (from Saab, maybe?), and if this indeed holds true?

By @donatj - 7 months
I have a coworker that LOVES to make these one or two line single use functions that absolutely drives me nuts.

Just from a sheer readability perspective being able to read a routine from top to bottom and understand what everything is doing is invaluable.

I have thought about it many times, I wish there was an IDE where you could expand function calls inline.

By @adamrezich - 7 months
I find that when initially exploring a problem space, it's useful to consider functions as “verbs” to help me think through the solution, and that feels useful in helping me figure out a solution to my problem—I've isolated some_operation() into its own function, and it's easy to see at a glance whether or not some_operation() does the specific thing its name claims to do (and if so, how well).

But then after things have solidified somewhat, it's good practice to go back through your code and determine whether those “verbs” ended up being used more than once. Quite often, something that I thought would be repeated enough to justify being its own function, is actually only invoked in one specific place—so I go back and inline these functions as needed.

The less my code looks like a byzantine tangle of function invocations, and the more my code reads like a straightforward list of statements to execute in order, the better it makes me feel, because I know that I'm not unnecessarily hiding complexity, and I can get a better, more concrete feel for what my program's execution looks like.

By @Cthulhu_ - 7 months
I feel like this style is also encouraged in Go and / or the clean/onion architecture / DDD, to a point, where the core business logic can and should be a string of "do this, then do that, then do that" code. In my own experience I've only had a few opportunities to do so (most of my work is front-end which is a different thing entirely), the one was application initialisation (Create the logger, then connect to the database, then if needed initialize / migrate it, then if needed load test data. Then create the core domain services that uses the database connection. Then create the HTTP handlers that interface with the domain services. Then start the HTTP server. Then listen for an end process command and shut down gracefully), the other was pure business logic (read the database, transform, write to file, but "database" and "file" were abstract concepts that could be swapped out easily). You don't really get that in front-end programming though, it's all event driven etc.
By @torginus - 7 months
"Typically I am there to rail against the people that talk about using threads and an RTOS for such things, when a simple polled loop that looks like a primitive video game is much more clear and effective. "

Yess, I finally feel vindicated. I've been having this argument with embedded people since forever. I was of the opinion that if million line big boy PC apps can make do with just one thread, having fifteen threads and synchronizing between them using mutexes and condition variables on a microcontroller with 64kb RAM is just bonkers.

For some reason, the statement that a while(true) loop + ISRs + DMA can do everything an RTOS like FreeRTOS can do, can rile up embedded folks to no end.

By @otikik - 7 months
> I have gotten much more bullish about pure functional programming, even in C/C++ where reasonable: (link)

The link is no longer valid, I believe this is the article in question:

https://www.gamedeveloper.com/programming/in-depth-functiona...

By @djha-skin - 7 months
This largely concurs with clean architecture[1], especially considering his foreword containing hindsight.

Clean architecture can be summarized thusly:

1. Bubble up mutation and I/O code.

2. Push business logic down.

This is how it's stated in [1]:

> The concentric circles represent different areas of software. In general, the further in you go, the higher level the software becomes. The outer circles are mechanisms. The inner circles are policies.

Inlining as a practice is in service of #1, while factoring logic into pure functions addresses #2, noted in the foreword:

> The real enemy addressed by inlining is unexpected dependency and mutation of state, which functional programming solves more directly and completely. However, if you are going to make a lot of state changes, having them all happen inline does have advantages; you should be made constantly aware of the full horror of what you are doing. When it gets to be too much to take, figure out how to factor blocks out into pure functions (and don.t let them slide back into impurity!).

1: https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-a...

By @physicsguy - 7 months
I think when developing something from scratch, it's actually not a terrible strategy to do this and pick out boundaries when they become clearer. Creating interfaces that make sense is an art, not a science.
By @nuancebydefault - 7 months
> The function that is least likely to cause a problem is one that doesn’t exist, which is the benefit of inlining it.

I think that summarizes the case pro inlining.

By @exodust - 7 months
For some reason this quote by Carmack stands out for me:

> "it 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."

I'm not the audience for this topic, I do javascript from a designer-dev perspective. But I get in the weeds sometimes, maxing out my abilities and bogged down by conditional logic. I like his quote it feels liberating... "just send it all for processing and cherry-pick the results". Lightbulb moment.

By @wruza - 7 months
I wish languages had the following:

  let x = block {
     …
     return 5
  } // x == 5
And the way to mark copypaste, e.g.

  common foo {
    asdf(qwerty(i+j));
    printf(“%p”, write));
    bar();
  }
  …(repeats verbatim 20 times)…
  …
  common foo {
    asdf(qwerty(i+k));
    printf(“%d”, (int)write); // cast to int
    bar();
  }
  …
And then you could `mycc diff-common foo` and see:

  <file>:<line>: common
  <file>:<line>: common
  …
  <file>:<line>:
    @@…@@
    -asdf(qwerty(i+j));
    +asdf(qwerty(i+k));
    @@…@@
    -printf(“%p”, write));
    +printf(“%d”, (int)write); // cast to int
With this you can track named common blocks (allows using surrounding context like i,j,k). Without them being functions and subject for functional entanglement $subj discusses. Most common code gets found out and divergences get bold. IDE support for immediate highlighting, snippeting and auto-common-ing similar code would be very nice.

Multi-patching common parts with easily reviewing the results would also be great. Because the bugs from calling a common function arise from the fact that you modify it and it suddenly works differently for some context. Well, you can comment a common block as fragile and then ignore it while patching:

  common foo {
    // @const: modified and fragile!
    …
  }
You still see differences but it doesn’t add in a multi-patch dialog.

Not expecting it to appear anywhere though, features like that are never considered. Maybe someone interested can feature it in circles? (without my name associated)

By @kazinator - 7 months
In my opinion, there is value in functions that have only one caller: it's called functional decomposition. The right granularity of functional decomposition can make the logic easier to understand.

To prevent unintended uses of a helper function in C, you can make it static. Then at least nothing from outside of that translation unit can call it.

By @rcv - 7 months
> The fly-by-wire flight software for the Saab Gripen (a lightweight fighter) went a step further...

I would love to hear some war stories about the development of flight software. A lot of it is surely classified, but I'm fascinated by how those systems are put together.

By @IshKebab - 7 months
I think the major problem with this is scope. Now a variable declared at the top of your function is in scope for the entire function.

Limiting scope is one of the best tools we have to prevent bugs. It's one reason why we don't just use globals for everything.

By @endlessmike89 - 7 months
Link to the Wayback Machine cache/mirror, in case you're also experiencing a "Bad Gateway/Connection refused" error

https://web.archive.org/web/20241009062005/http://number-non...

By @roeles - 7 months
> No bug has ever been found in the “released for flight” versions of that code.

I thought that at least his crash was a result of bad constants in flight software: https://www.youtube.com/watch?v=SWZLmVqNaQc

The first comment appears to agree with me.

By @lencastre - 7 months
I’m not even pretending I understood Carmack’s email/mailing list post but if more intelligent/experienced programmers than me care to help me out, what exactly is meant by this he wrote in 2007:

_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._

Thanks,

By @easeout - 7 months
Come to think of it, execute-and-inhibit style as described here is exactly what's going on when in continuous deployment you run your same pipeline many times a day with small changes, and gate new development behind feature flags. We're familiar with the confidence derived from frequently repeating the whole job.
By @sylware - 7 months
I have been super picky about what JC says since he moved the ID engine from plain and simple C99 to c++.
By @shortrounddev2 - 7 months
Can someone explain what inlined means here? It was my assumption that the compiler will automatically inline functions and you don't need to do it explicitly. Unless it means something else in this context
By @rossant - 7 months
(2014)
By @fabiensanglard - 7 months
How does a program work when its disallow "backward branches". Same thing with "subroutine calls" how do you structure a program without them?
By @randomtoast - 7 months
My browser says "The connection to number-none.com is not secure". Guess it is only a matter of time until HTTPS becomes mandatory.
By @ydnaclementine - 7 months
> do always, then inhibit or ignore strategy

can anyone expound on this? I'm not sure what he's exactly referring to here

By @Ono-Sendai - 7 months
There is actually a major problem with long functions - they take a long time to compile, due to superlinear complexity in computation time as a function of function length. In other words breaking up a large function into smaller function can greatly reduce compile times.
By @gdiamos - 7 months
How much of this is specific to control loops that execute at 60hz?
By @jjallen - 7 months
One benefit that I can think of for inlined code is the ability to "step" through each time step/tick/whatever and debug the state at each step of the way.

And one drawback I can think of is that when there are more than something like ten variables finding a particular variable's value in an IDE debugger gets pretty difficult. It would be at this point that I would use "watches", at least in the case of Jetbrains's IDEs.

But then yeah you can also just log each step in a custom way verifying the key values are correct which is what I am doing as we speak.

By @rickreynoldssf - 7 months
The clean code people are losing their collective minds reading that. lol
By @oglop - 7 months
Oh good, a FP post. I love watching people argue over nothing.

Here’s the actual rule, do what works and ships. Don’t posture. Don’t lament. Don’t idealize. Just solve the fucking problem with the tool and method that fits and move on.

And do not try to use this comment threat to understand FP. Too many cooks, and most of the are condescending douchebags. Go look at Wikipedia or talk with an AI about it. Don’t ask this place, it’s all just lectures and nitpicks.

By @mellosouls - 7 months
(2014)

Ten years ago - a long time in coding.

By @Myrmornis - 7 months
This is the real religious war among programmers -- it's a genuinely consequential question: someone who favors abstraction and modularity is going to absolutely hate working in a codebase with pervasively inlined code.

It's clear that Carmack's article is addressing a particular sort of C++ codebase that might be familiar to game developers, but isn't familiar to a lot of us here who work on web applications and backend distributed systems. His "functions" aren't really what we think of as functions: they're clearly mutating huge amounts of global state. They sound more like highly undisciplined methods on large namespaces. You can see that from the following quotes:

> 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.

> if a function only references a piece or two of global state, it is probably wise to consider passing it in as a variable.

In the world of many people here, i.e. away from Carmack's C++ game dev codebases of the 2000s with huge amounts of global mutable state, the standard common sense still applies: we invented structured programming with functions for profoundly important reasons: modularity and abstraction. Those reasons haven't gone away; use functions.

- In a large codebase you do not need or want to read the full tree of implementation in one go. Use functions: they have return types; you know what they do. A substantial piece of implementation should be written as a sequence of calls to subfunctions with very carefully chosen names that serve as documentation in themselves.

- Make your functions as pure as possible subject to performance considerations etc.

- This brings a huge advantage to helper functions over inlining: it is now easy to see which variables in the top-level function are being mutated.

- The implementation is much harder to understand in a single function with 10 mutable variables, than in two functions with 5 mutable variables. I think ultimately that's just a fact of combinatorics; not something we can hold opinions about.

- But sure, if the 10 mutable variables cannot be decomposed into two independent modules then don't create spurious functions.

- A separate function is testable; a block inside a function is not. It wasn't really clear that the sort of test suites that many of us here work with were part of Carmack's codebases at all!

- It is absolutely fine to use a function if it improves modularity / readability even if it only called once.

By @atulvi - 7 months
who read this in John Carmack's voice?