John Carmack on Inlined Code (2014)
number-none.com
number-none.com
There is a reason regular front-to-back books are more popular then choose-your-own-adventure books. You know where you are, and you dont spend all day flipping back and forth.
I also find that when you start splitting up your functions down to 5 lines, it becomes almost impossible to differentiate what the different functions do clearly, and that makes good naming almost impossible.
EDIT: Also, I dont understand why people complain about functions being longer then 5 lines making code hard to navigate, and then turn around and write header only files with 10000 lines of code....
I don't think that has anything to do with it. It's a roleplaying adventure where you need to keep track of things on a piece of paper and use dies for encounters. People that want to read don't want to participate, just consume. Active vs passive.
Regardsless, splitting 4-5 lines of code out into a function and giving that a proper name can enhance readability significantly. And modern IDE tools make it simple to read that without disrupting the flow. Visual Studio and IDEA for instance has a peak definition that makes it trivial.
If that function does not do what it say, that is a different problem.
If the function doesn't need to be called many times, it doesn't need to exist.
I prefer clear naming over comments, because comments doesn't "travel" everywhere the named function/type/variable does.
> Enclosing pages of code inside a conditional or loop statement does have readability and awareness drawbacks
If I'm reading a novel, it would be jarring to read "and then he took 45 steps of average length to reach the kitchen" instead of "he went to the kitchen".
Of course, for certain tasks long functions are still reasonable, but they tend to be the exception rather than the rule, and it takes quite some practice to realize.
Best practical advice: do more design and planning on paper.
Easier to test usually, too.
That's a bad example. A better example - and yes, I'm totally biased - is the main() method for the rebuild I'm working on - it's basically 'read config', 'set up logger', 'set up service x', 'set up service y', 'validate runtime dependencies like files and scripts', 'create & start http server', 'wait for shutdown signal', 'gracefully shut down http server on shutdown signal', etc. All in one place, no need to extract methods in my opinion. That said, it really is not the critical path of the application.
But then some people will say that you're testing the internals of the caller and that you shouldn't do that. If we follow that logic to the absurd, there should only be one test for the entire application, one for main(), or whatever your entry point is.
I've never seen an answer to this problem coded as an intelligible, easy-to-follow guideline. Empirically and intuitively I think my "hotspot" approach kinda works in maximizing reliability.
Don't break stuff up for the sake of it, but DRY.
But I don't think I'd like writing long functions as a system or philosophy; I think there is a threshold where a function becomes fat enough that it hurts readability.
Code is an art form.
As you said, organizing code is an art form. There are no hard and fast rules, only your own judgement and experience.
You end up with the same amount of code, but now you have to jump around. In most cases it also forces you to turn local variables into instance variables, globals, or pass state in parameters in order to carry state between the new functions, which further decreases readability.
You might as well add an "index" at the top of the large function and get the same readability benefits from doing it.
What should be done instead is trying to find code that can be abstracted without hurting readability. Most of the time in large functions there are mixed abstraction levels, such as business logic mixed with I/O, generic error handling that could be somewhere else, or object transformation that exists because of different libraries working differently.
When you automatically refactor into smaller functions you carry those problems into the small functions and you get code that is probably harder to follow, generates worse stack traces and only looks good in a shallow inspection.
If your function does an unexpected side effect, thats a problem with the code. Not with the idea of writing functions that do a specific thing.
I can't help but think people who complain about this, are just bad at creating functions at the correct level of abstraction.
It's also why i'm a functional fan, because it makes it more difficult for functions to do unexpected things.
They are complaining due to difficulty of reading other peoples' code, not their own.
> You can just skip over it
The point is that if I blindly break a larger functions by functionality, I still have to understand the deeper functionality. So, I can't just skip when I'm debugging or deeply reviewing/inspecting the code, or doing a rewrite or trying to understand it.
> I can't help but think people who complain about this, are just bad at creating functions at the correct level of abstraction.
My point was exactly that you should create functions at the correct level of abstraction, rather than just splitting large functions purely by functionality, like you would with a cooking recipe.
> If your function does an unexpected side effect, thats a problem with the code
But this is exactly what I'm trying to avoid. If you need side effects, keep them visible, by keeping them in the main function, instead of just blindly putting them into smaller functions.
That's exactly what a well abstracted function should let you do.
It only comes difficult with global state, and unexpected side effects.
You should break up functions by intent.
I understand the argument to be that by breaking everything out into functions it makes it too easy to hide what is actually going on and you can miss a lot of fine detail. If your code looks like
x=doA();
y=doB(x);
z=doC(y);
then it becomes very hard to spot for example that both functions doA and doC recomputed the same intermediate value that you could just compute once and reuse or that doB validates its input, but in this case you know it's unnecessary to do so, since you know that doA can only return data that is valid for doB. If everything was inline then these things would jump out at you a lot quicker.Of course in many cases this doesn't matter and recomputing that intermediate value twice costs so little in the grand scheme of things that the extra readability is worth it.
Especially if you don't need reuse (or if you only need it in a very limited fashion) nor unitary testing, you can handle things using different techniques than splitting into functions. Like simply having a succession of blocks in a bigger function, with inline notes about invariants between each blocks, etc.
If done in a disciplined way I don't see much avantage about that approach than about just using functions anyway, esp with a good IDE or code editor. Of course nothing is absolute but if it does not fit on a screen maybe at least consider splitting it (and improving the doc, etc.; or at least just improve the doc). Blindly requiring e.g. 5 lines max strictly everywhere is insane though, never do that. There can be excesses on both sides.
As a more personal rule of thumb, I avoid splitting things too much esp. if the result takes more lines than inline versions, except if not splitting results in a high number of duplications (small in that case, but still duplications). A moderate increase of LOC when splitting is OK but when trying to split like crazy you can fall to N fold increases and at some point this is sometimes counter productive. On the other hand some very moderate amount of code duplication is ok, at least if the duplicated parts are not too far away and can be found easily, and not in an unreasonable quantity, modulated by the size of duplications. For extremely tiny duplications even in huge quantity, replacing by a function call does not necessarily makes sens, if e.g. that's (i + base) % mod repeated 30 times in the same file maybe it would give no advantage to replace that with rotbufindex(i, base, mod), the call is not more easy to reuse than the inlined formula.
MajorFunction() {
// fast inverse square root
float x2 = number * 0.5F;
float y = number;
// several more lines of code
// find decimal point
char *c = input;
while (*c && *c != '.')
++c;
}
A goal of style A or B is for the names of the minor functions to make the shape more obvious, ideally self documenting: MajorFunction() {
y = Q_rsqrt(number);
char *c = strchr(input, '.');
}
This is the kind of advice you'll find in, say, Code Complete. Although I generally accept the premise, there is a readability cost to indirection. The bigger and more complex MinorFunction() is, the more likely I'm going to have to jump into it and remind myself what it does.There are two concepts that underlie DRY: coupling and cohesion. There are good expositions on this in old writings on structured programming and design (e.g., Yourdon and Constantine). If MinorFunction() is cohesive, and MajorFunction() is appropriately coupled to MinorFunction(), then style A/B is likely to be superior to style C. One of Carmack's points is that "very little of our real code" ends up that way.
In analysis using the structured program theorem, you only have three tools, really: Sequence, selection, iteration. If you copy-paste, you lean on the sequence more; if you add a loop or a cursor structure, you're iterating; and if you add more names for things, you're selecting on the name. All three create dependency risks, but not in equal measure.
What a really long function mostly indicates is that the inherent dependencies are mostly sequential. This maps with the domains where they appear most naturally: game loops are notoriously single-threaded, embedded code often needs to operate to hard real time constraints.
I think the appeal in splitting out more names has something to do with linguistic comfort zones: Rather than examine long sequences, assume the functions used by the one you are looking at are trustworthy. Then you are "improving" the code each time you factor it out because you can read more names, and because each function is small there is little concern about sequencing errors. It's intuitive, but shows its flaws as soon as you use another lens like the structured program analysis; adding the new function makes it harder to examine the sequence across the function boundaries, causing the "flea-jump" code you describe.
What I've found works is to let the large functions accumulate and mature, then derive a new dependency - a function, class, or other abstraction - that will simplify maintenance. Not every problem is solved with a new function, sometimes it really takes a compiler to get the desired improvement.
Working mostly on Java these days, Carmack's focus on efficiently doing the right thing sounds like fairy-tales of the promised land ;)
Just yesterday, I spent much longer than I believe is reasonable to fix a bug in a Java class whose sole purpose is to do an HTTP(S) call and determine if the URL is online (200 code) or declared offline (40x code).
The first thing that really slowed me down was that instead of having a clear location for the crash, a Java .war inside tomcat tends to vomit 40-50 lines of stack trace onto the console. The reported Exception was "java.io.IOException: Server returned HTTP response code: 400 for URL: ..." in HttpURLConnection.getResponseCode(). But debugging the issue, I noticed that getResponseCode exits just fine without any exception.
A bit of Googling around revealed that the root cause was this: https://stackoverflow.com/a/54837353/525608 Inside getResponseCode(), an exception is thrown, caught, and suppressed. And then later, it is retrieved out of HttpURLConnection.rememberedException and thrown around by a different HttpURLConnection function. But of course the Exception still had the old = wrong stack trace in it. So those 40+ lines of stack trace that Java garbled up? Completely useless.
I kid you not, Java source code with Exceptions is like your own private hell, especially because the control flow tends to be somewhere between "nested inside 30 functions" and "completely random". The latter usually happens when libraries do bytecode patching so that what is actually executed doesn't even match the source code anymore, and that's also the point where stack traces become almost completely useless.
I'd take a 2000+ lines monolithic function every day over this unholy mess that ["Enterprise Java" == stacking random libraries on top of each other] has become.
"Convenient proxy factory bean superclass for proxy factory beans that create only singletons."
Basically, the error is that when some system uses virtual function calls via an interface, what that's really saying is that: "This can be changed at runtime."
So if there's some function "void foo( IBar bar )", then what that's really saying is that "bar" can change at runtime. Literally a different implementation from call-to-call.
Is that actually required?
In 99.9% of such function calls, no: the implementation of IBar will always be the same concrete class.
To see a language that has gone to the opposite end of the spectrum, take a look at Rust. It use a lot of parametric typing, which is very flexible, but by default tends to use compile-time static types instead of run-time dynamic types.
Java never went down this path because it added parametric types very late in its design.
C# had parametric types in v2.0, which is when it started getting popular.
Rust had it from the beginning.
https://golangnews.org/2020/11/experimenting-with-go-type-pa...
Do you have any idea what they did right that Java lacked?
An interface in Go can be defined by the client code and types just need to implement the functions. In Java Interfaces need to be defined before any code can use it. And exception handling makes code hard to reason about because it introduces hidden control flow.
I think Generics don’t give you that much leverage. Without them you need to duplicate the code for each type or class, but duplicated code is much easier to work with. Yes, it’s not really dry, but copy paste and search/replace is something which increases developer productivity, fixing bugs in code with too many generics decreases it.
And one thing to keep in mind: historically a lot of interfaces in Java were necessary because Java had no lambdas. In Go functions are first class citizens and you don’t need an Interface just to dynamically call different functions.
That's an important role, especially in performance-sensitive domains, but outside of that, it never seems to work very well, which makes it a easily misused feature in every language I've used that offers it. Go's original design basically took the form of offering this for the built-in structures but not letting you roll your own, ushering you towards interfaces if you really needed it, and because Go makes interfaces convenient, it mostly works.
BTW, as an example of generics abuse, crypto++ comes to mind. It's such a huge pile of templates referencing each other and exceptions being used to control the command flow, that I was once hired as a freelancer just to link the whole thing into a static library, because the company using it was unwilling to deal with the source code, but needed it for FIPS compliance.
It's an incredible engineering feat that they managed to let you mix and match cryptographic functions and containers and storage formats as you see fit, but having a C++ stack trace with 5 abstract templates in it makes it very painful to debug if stuff goes wrong.
And maybe that's just my personal style, but to me defer is much more important than generics.
I can't even remember how often I've had to debug Java bugs where someone forgot to close a ResultSet for an SQL query... And then you leak memory so badly that restarting tomcat with a daily cron is still not enough :(
With defer, you can expect people to write the cleanup code immediately after opening something, which makes it much easier to confirm that you're not leaking resources.
I just wish there was long crappy linear functions with inline configs so that I could search this mess and see line by line what it did without having to keep so much in my head.
Maybe it was easier to write but it sure as hell is harder to read ...
In my opinion, that provides one huge advantage that Java currently lacks: you can use tools like ReSharper to statically analyze control flow, find uninitialized variables or unused branches, etc.
Something must be quite bad with the design if he ends up having to remember exceptions...
I feel for the poor guy who had to figure out the bug that was caused by this -- the waste of his time was completely predictable by this idiotic design.
100%, not only did this person make a pretty strange design, he has completely misunderstood how exceptions are to be used. even when he remembers exceptions, he fails to make them the root cause of the new exceptions he throws. truly a remarkable design, in all the wrong ways.
Absolutely, a bad design that says more about the designer of that piece of code, than the language (Java). Error handling can and should be discussed, but I hope people don't use this as an example to illustrate how bad Java checked exceptions are. This is just some nonsense that can be constructed when you misunderstand underlying concepts, which is possible in any language.
Here's a bug report from 2001 where the rememberedException had accidentally changed its behaviour: https://bugs.java.com/bugdatabase/view_bug.do?bug_id=4523989
* Java Server Faces
* Tag libraries
* Portlets
How did any of this become official Java technology.
The stateful design, which fills up templates with a bunch of hidden html-input fields, in order to open web-pages in the same "state" as you left them and all sorts of other crap.
You also need to look into Portlets, which "splits" up a web-pages into seemingly stand-alone components. These are then combined into horribly complicated Servlets as you deploy, where http paramters are namespaced with some random _portletId=lsøjfksjo1920 things in order to figure out which parameters goes to which "part of the web-page". Again, all of this is so bad, you'd think it was a joke; but it was Java's offering for web-development, and a huge number of people CHOOSE that technology in the early 2000's.
Everything is broken :-)
It really should be taught in schools as how not to design anything. I actually once worked on some extension (at least I think it was) of Java Server Faces which sent serialized Java objects as hidden html-input elements (base64-encoded binary blobs) back and forth with each request(!!). At least I hope that was not part of the core JSF technology. You really should run away from most of the early Java web-tech, it truly was horrid.
It's also strange that a language, which was so "connected" to the early days of the web (even had their own html element <applet>) ended up with such strange things for web-development shortly after.
Your comment is a pretty useless blanket statement. At least invite some discussion by coming with a few arguments..
Have you investigated Common Lisp, its use of BLOCK/RETURN-FROM and TAGBODY/GO, and how it is possible to close over these lexical constructs to achieve non-local jumps to predefined points on the stack ? All of Common Lisp's error handling system is based on this primitive mechanism (and therefore written in Lisp itself).
(Disclosure: wrote a book on the topic.)
Exceptions are not Structured Programming: they are worse than goto, because at least goto is scope-local. If, maybe, the language enforced that all throwable exceptions are declared by the prototype, then it’d be alright. But I don’t know of any that do. So, given a function call, will it error out? Can you know without inspecting the source, if you even have it?
Thanks, you said it better than me. This is the essence.
fmt.Print can throw. And HTTP handlers throwing exceptions is silently hidden. If you don't write code assuming anything can throw, then your code is broken.
And don't judge exception by how they are in Java. That's just a clusterfuck. No other language I'm aware of gets exceptions so wrong.
I agree. Well, "fatal" needs to be defined. If an HTTP handler throws an exception, is that fatal for the whole webserver?
Java seems crazy about this. Exceptions seems like it's being treated as just another return value. And that leads to a mess.
But C++? What parts of the C++ standard library have unreasonable exceptions used for errors? (there may be some, I just can't think of any)
And note that you have to throw (no pun intended) away large parts of the language if you remove exceptions. E.g. you can't have constructors without exceptions. How else would you signify "those arguments you gave to the constructor are no bueno".
Go doesn't have constructors, so it's consistent with what it says.
Also see my comment here, about how common or not, discouraged or not, the mere existence of exceptions in a language changes how you must write code to not have it be buggy: https://news.ycombinator.com/item?id=25275580
Yes, in C++ 'goto' is a code smell. It's not in C (greatly used for error handling), but C++ has RAII so `goto` should be rare outside of "clever" code (where "clever" is rarely good).
The mere fact that C++ doesn't have 'finally', and Java does, tells you a lot about how exceptions and RAII differs. If you write a macro for "finally" in C++ then you're doing it wrong.
Pretty much all of my `catch` clauses are in main() (or the root of an event handler), to pretty print the error and/or log to central service, or in a top level event like HTTP handler. `catch` should be about as common in C++ as `rescue` is in Go.
In Java it's fucking everywhere.
Well, your handlers should not generally panic! It's called panic in Go so panicking should be hopefully rare for the peace of mind.
Uhm… no… no they should not.
I feel like you're missing the whole point, here. An interface that is extremely hard to use correctly without turning small bugs into major outages is not a good tool.
And one way to make sure this doesn't happen is to write exception-safe code, because Go has exceptions.
You could also argue that your C++ code shouldn't throw, and I agree. It should very rarely throw. But when it does it should be safe.
If Go had simply not had exceptions then this would have been easier.
> It's called panic in Go so panicking should be hopefully rare for the peace of mind
If you write a web service that hits a bug that panics about once per million requests, and you run 1000 qps, that means your Lock();dothing;Unlock() will deadlock the whole webserver once every 15 minutes.
If you write exception safe code, then it does not.
I don't know of any language that does mudane error handling with exceptions that is not a mess. C++, for instance, is extremely difficult to write exception safe code in.
I don't know about Rust, but Go most certainly did.
> C++, for instance, is extremely difficult to write exception safe code in.
It's WAY easier to write exception safe code in C++ than in Go, because C++ has RAII and scoped defers.
See my example for what a mess Go makes of this in this comment: https://news.ycombinator.com/item?id=25276360
I find C++ exception safe code to be pretty much trivial. Once you get used to "no naked resources" RAII just makes everything exception safe automatically.
However, i'd argue for allowing no unchecked exceptions at all that can be thrown at runtime and instead forcing developers to handle every fail state, that can be encountered in the code that they call.
If a method that you call can fail in 60 different ways, you should at least handle the 20 of them that you understand in their own ways and the rest in a blanket statement. All of that should be checked at compile-time, of course.
Java, .NET and most other ecosystems (both languages and frameworks) don't really seem to want to bother with that, though.
Basically if I define a checked exception function then I am expected to check all exceptions that will be thrown?
Maybe the solution to that is
1. Only allow checked exceptions, and force error handling.
2. Only have 2-3 exception types, not user-extendable: Retryable, Unrecoverable, and Error (for non-user code errors like OOM). OTOH, how to distinguish between different exceptions thrown by the same method, and how to add additional error information (like status_code) would become a problem. Javascript doesn't seem to care tho?
A Result type à la Rust solves most of that: you might still have to wrap lower layers, but at least the noise & syntax is much more sane (never thought I'd say that about Rust!).
Unwind stack, calling cleanup functions in frames that need them, until caught higher up the stacks. Yah, that's exceptions.
The mere fact that they are less used doesn't make them "not exceptions".
And importantly it doesn't matter that they are less common. Merely having exceptions means that everyone has to write exception-safe code.
HTTP handlers silently swallow exceptions, so you can't rely on your program dying if there's a panic.
fmt.Printf can panic as far as you know, too.
Essentially you need RAII, except that because Go doesn't have RAII every single resource needs:
r, err := getResource() if err != nil { … } defer r.Close()
But because "Go doesn't have exceptions" people often don't bother with the defer, and then they get bugs. I see it happening frequently.
You need to write exception-safe code. But also you're not allowed to use exceptions. So it's the worst of both worlds.
I write and review a lot of Go code, and I like the language. But I don't like the dishonesty.
I wasn't aware that this is a real problem in Go land, thanks for the explanation. I wouldn't say the language is "great", was just using it as an example of a modern language where there is a consensus that "they got it right" and not using exceptions, even though error handling is still tedious right now.
I think in Rust you have to consciously fuck up the panic handler to cause similar issues, but I'm not sure.
mu.Lock(); fmt.Print(someoneElsesObject);mu.Unlock();return
You need to get into the habit of writing:
mu.Lock(); defer mu.Unlock(); fmt.Print(someoneElsesObject);return
And this gets extra complicated by the fact that in Go defer runs at end of function, not end of scope. This makes every single for-loop that needs to lock anything hard to read and annoying to write.
for _, a := range stuff { if err:=func()error { mu.Lock();defer mu.Unlock(); return a.stuff()}(); err != nil {return err}}
You can also use defer if you want, but I've never seen it in real code, it's more error-prone and not as flexible. You can build it on top of RAII.
I'm saying "mu.Unlock()" except when deferred, is essentially always a bug. At the very least it's a bug waiting to happen. You need to prove that everything between Lock and Unlock is exception-safe. And that's rarely possible.
As I've said elsewhere you cannot rely on panic triggering program exit, since e.g. HTTP handlers swallow panics.
The fact that you seem to be saying you never see an Unlock deferred proves my point that saying "Go doesn't have exceptions" hurts Go programming. And Go code is in fact full of bugs because there's all this exception-unsafe code.
C++ is naturally exception safe, because RAII. Where it's not exception safe it's because RAII was not used.
Even something as simple as:
mu.Lock()
stats[metricName]++
mu.Unlock()
will throw if there's a path where stats[] map was not inited, and if called in an HTTP handler will leave the lock in place, leading to probably a deadlock of the server. Not great.
let someoneElsesObject = mu.lock();
println!("{}", someoneElsesObject);
return;
when someoneElsesObject goes out of scope, the mutex is unlocked. This happens no matter how it goes out of scope. You cannot forget to do it, because the only way to get access to someoneElsesObject in the first place is locking the mutex, because mutexes wrap the data that they are protecting.https://crates.io/crates/defer exists, but it would be weird to try and use it here, because it can't really be combined directly with this. I guess in theory you could put drop(someoneElsesObject) in the defer block, but like... that already happens for free.
Yeah looks more RAII, and looks like it would not have the problem Go has.
Thanks.
func(){
Code for lock and deferred unlock here
}()
Hence my example where the lambda has to return an error, and the loop has to check for the error. It's A LOT of boilerplate.
Edit: Actually now I don't know what you mean. You clearly replied to my comment that gave a clear example with a return from within the lambda, so what did you think that I didn't know? You took my example and removed extremely commonly needed functionality. So… huh?
I mean they still added exceptions to Golang, but are so ashamed of it they named them something else.
These people should have to code C++ for a while before being allowed to rant about exceptions.
(real C++, not Google C++ which bans exceptions for historical reasons)
But yeah, what the fuck are you doing, Java? I've yet to see a stack trace that was actually helpful.
> Suddenly the Golang authors ranting about exceptions seems somewhat rational.
Error handling in Golang is nothing to write home about, either.
Note I say "misused" -- but it happens. After all, if Java exceptions weren't misused, there wouldn't be any problem either.
Should you instead handle every error close to their source, exceptions would be an overly verbose way of doing it.
But maybe it's just my specific kind of programming prejudice shining through here.
No. You either pass them up or handle them. Silently swallowing them is a recipe for disaster, because nobody gets any info about what went wrong. In very limited cases -- e.g. "I don't care about this harmless error" -- it's correct to swallow them up, but Java devs tend to overuse it and disaster ensues.
> Should you instead handle every error close to their source [...]
You shouldn't. Handle those errors that it makes sense to handle, pass them up otherwise. Just don't swallow them.
I find that this is very rarely the case. Micro-operations failing means that a MUCH higher level operation has failed.
If there's an error writing to the output file descriptor from deep within the decompression library, do you know what failed? The HTTP request. What can the decompression library, the IDS inspector, the logging system, the tracing system, or anything else do? They can just pass along the error.
Hence so much "if err != nil { return err}" in Go. The vast VAST majority of errors are not handled, they are reported.
Verbose?
benchmark b("blah"); write(http_compress(logsize(backend_decompress(out_fd, in_fd)))
It's extremely rare that something can recover from an error. And the things that can be recovered from are not exceptions in C++ (though they seem to be in Java, brrr).
Without a way to exit a function early, you either end up with deep nesting (which becomes hard to read/follow after 2-3 levels), or a lot of small function calls to continue/break. In many cases, you can use a `with <-` but if the statement only returns a variable, you need to wrap it into a function (are have a hard-to-read guard clause, which will only work in some cases) to support matching:
Say `User.load` is out of your control and it returns nil | user. I'd love to do:
user = User.load(id)
if user == nil do
return :not_found
end
But I have to either introduce nesting, or decrease the readability with either a guard or a function.Guard:
# if we just let `nil` flow through to the `else` we won't be able to tell this `nil` from another
with user when user != :not_found <- User.load(id) || :not_found
Function: with {:ok, user} <- get_user(id)
...
defp get_user(id) do
case User.load(id) do
nil -> :not_found
user -> {:ok, user}
end
endSo instead of
inline int f(int x) { return x * x; }
int main() { f(3); }
you'd want to do int f(int x) { return x * x; }
int main() { inline f(3); }
I have seen this implemented in Zig for loops (inline for, inline while). Is there support for this in other languages?File compilation is allowed to automatically inline function calls that are in the same compilation unit as their target.
This follows from the reasoning that material which is compiled together will be loaded together, and those functions will be redefined together.
The details are in 3.2.2.3 Semantic Constraints, bullets 3-5.
In any case where this inlining would be a problem, you must use declaim globally or declare locally to request notinline.
Likewise, you can use an inline declaration to encourage inlining.
Allegro CL does inlining on its own, IIRC.
> majority of users would find this behavior problematic as default
Block compilation or whole program compilation are used for delivery of applications.
Mostly, sure just like LTO in C/C++. But if I were doing any scientific programming with common lisp I'd try to use block compilation in a few select places during interactive development as well. I'd imagine that for some code this would get rid of a lot of boxing and unboxing as well as type checks, in addition to getting rid of indirection. I'd not be surprised if you could get an integer factor speed up if you got many small functions which mostly operate on doubles or double arrays. I should probably give it a try just to satisfy my curiosity.
Nowadays most optimisers ignore them anyway and use heuristics to decide if and when they actually care, and also inline when not asked to.
If you really want the original behaviour you need to use non standard modifiers like forceinline.
Relatedly, in languages like C that have global mutable state, it's the callee who knows better whether and when it's safe to inline code, not the caller. Ditto in languages (like C) that can have complex linking semantics, multiple function definitions, etc.
Instead is a directive to the compiler to ignore the One Definition Rule for the function and it is an artefact of the header inclusion model of compilation.
I would probably do it by overriding (define ...) to make it also store the source of the function somewhere and make (inline ...) just insert it as an anonymous function:
(define (square x) (* x x))
(inline square 5)
The last call would be expanded to ((lambda (x) (* x x) 5)
Which the guile inliner always in-lines."To sum up:
If a function is only called from a single place, consider inlining it.
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.
If there are multiple versions of a function, consider making a single function with more, possibly defaulted, parameters.
If the work is close to purely functional, with few references to global state, try to make it completely functional.
Try to use const on both parameters and functions when the function really must be used in multiple places.
Minimize control flow complexity and "area under ifs", favoring consistent execution paths and times over "optimally" avoiding unnecessary work.
Discussion?
John Carmack"
What I like about learning from Carmak's approach to programming is that he's always seemed to work in domains where he had to squeeze every ounce of performance out of what he's doing. I think that kind of programming forces you to focus on what's actually true about computers, and about programming concepts. As programmers, when we talk about design patterns and best practices, a lot of what we talk about actually comes down to opinion, philosophy and aesthetics.
When you are really forced to get the right bits to the right place as quickly as possible, it focuses the problem of software design in a very specific way. It's interesting to observe that for Carmak, that appears to be in the direction of removing as much abstraction as possible.
That being said, John Carmack makes the very important point that this decision (whether to inline or not) should be made case-by-case. Someone in these comments used the flow of a novel as an example and I think it's actually the ideal way to describe the balance you want to strike with code as well. Using "The Tortoise and the Hare" as an example:
Inlined too much: "The race began. The Tortoise took a step. The Hare took a step. The Hare took another step. The Hare took another step. The Hare took another step..."
Inlined too little: "The race began. The Tortoise won!"
It's interesting because some authors actually make mistakes in this realm - overdoing it on details, or leaving the reader confused without enough context. Thinking about the flow of code like the flow of a novel is probably a good idea - both should delicately balance complexity with readability. The best examples of both often describe complex and nuanced concepts while remaining surprisingly straightforward.
If you think this example is contrived and it's always obvious where to abstract and where to inline, it's likely you're over-abstracting. The question of "where to cut" varies greatly from one bit of code to the next and often needs some thought to get right. John's list under "To sum up" here is a great set of guidelines to answer that question.
Ongoing maintenance issues are not really top priority, the game is not going to be a living software project for that long.
I (naively) thought that C was pretty much "done". And any articles written recently were just for gilding the lily.
C didn't have proper atomics or a memory model until 2011 if I'm not mistaken, and that is important.
Here's a list with code samples: https://github.com/AnthonyCalandra/modern-cpp-features
As much as I enjoy hating on C++, I have to admit most of the new features are great.
> 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.
2019 https://news.ycombinator.com/item?id=18959636
2016 https://news.ycombinator.com/item?id=12120752
Discussed at the time: https://news.ycombinator.com/item?id=8374345
To sum up:
If a function is only called from a single place, consider inlining it.
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.
If there are multiple versions of a function, consider making a single function with more, possibly defaulted, parameters.
If the work is close to purely functional, with few references to global state, try to make it completely functional.
Try to use const on both parameters and functions when the function really must be used in multiple places.
Minimize control flow complexity and "area under ifs", favoring consistent execution paths and times over "optimally" avoiding unnecessary work.
As I see it, whether you inline or split into functions or subroutines, you can have functions be pure, in the sense that they don't mutate state. In my experience with functional programming, splitting code into smaller functions is not a problem but rather encouraged too, for the sake of readability (so no different than other paradigms).
There must be something that escapes me, possibly at the systems level. Can someone please explain?
Trying to read other peoples code that jumps around between many functions with side effects, often being nested 3-4 levels deep, hurts my soul.
I guess with constexpr, at least in C++, this is a reality now.
I expected time would have solved the issue, but it's 2020 and it's worse every year. Same with GUIs.
I'm assuming this happened because the game, starting with an empty input buffer, queued input for the subsequent frame, rather than use the input as-is and process it right then and there?
https://en.cppreference.com/w/cpp/language/inline
If this specifier is used, then when your inlined function is compiled there will be no function call, just the inlined code.
This is just a hint for the compiler, though. The compiler can ignore this hint, and usually does.