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 articleJohn 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.
Related
My programming beliefs as of July 2024
Evan Hahn emphasizes tailored programming approaches, distinguishing "simple" from "easy," promoting testability through modularity, and advocating for ethical coding practices prioritizing societal impact and nuanced thinking in software development.
Beyond Clean Code
The article explores software optimization and "clean code," emphasizing readability versus performance. It critiques the belief that clean code equals bad code, highlighting the balance needed in software development.
Explicit is better than implicit
The article highlights that explicit coding enhances readability and maintainability, reduces confusion, and improves collaboration by clearly defining variables and access controls, ultimately leading to better code quality.
Optimizing Guile Scheme
David Thompson discusses optimizing Guile Scheme, highlighting strategies like minimizing memory allocation, using monomorphic functions, and employing profiling tools to enhance performance in game development and demanding applications.
What 10k Hours of Coding Taught Me: Don't Ship Fast
The article emphasizes the importance of developer experience and software architecture, advocating for simplicity in coding, prioritizing refactoring, and maintaining code quality through structured practices and passion for the craft.
- 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.
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?
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.
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.
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.
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.
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)
I wonder what is the actual original source (from Saab, maybe?), and if this indeed holds true?
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.
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.
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.
The link is no longer valid, I believe this is the article in question:
https://www.gamedeveloper.com/programming/in-depth-functiona...
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...
I think that summarizes the case pro inlining.
> "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.
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)
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.
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.
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.
https://web.archive.org/web/20241009062005/http://number-non...
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.
_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,
can anyone expound on this? I'm not sure what he's exactly referring to here
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.
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.
Ten years ago - a long time in coding.
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.
Related
My programming beliefs as of July 2024
Evan Hahn emphasizes tailored programming approaches, distinguishing "simple" from "easy," promoting testability through modularity, and advocating for ethical coding practices prioritizing societal impact and nuanced thinking in software development.
Beyond Clean Code
The article explores software optimization and "clean code," emphasizing readability versus performance. It critiques the belief that clean code equals bad code, highlighting the balance needed in software development.
Explicit is better than implicit
The article highlights that explicit coding enhances readability and maintainability, reduces confusion, and improves collaboration by clearly defining variables and access controls, ultimately leading to better code quality.
Optimizing Guile Scheme
David Thompson discusses optimizing Guile Scheme, highlighting strategies like minimizing memory allocation, using monomorphic functions, and employing profiling tools to enhance performance in game development and demanding applications.
What 10k Hours of Coding Taught Me: Don't Ship Fast
The article emphasizes the importance of developer experience and software architecture, advocating for simplicity in coding, prioritizing refactoring, and maintaining code quality through structured practices and passion for the craft.