15 ms·
John Carmack on Inlined Code (2014)
- panic 10y agoSome good previous discussion here: https://news.ycombinator.com/item?id=8374345 https://news.ycombinator.com/item?id=8374345
- bshimmin 10y agoImagine how fantastic it must be working with someone like Carmack. Sure, the first few code reviews would be fairly traumatic - as you quickly realise just how much faster and generally better he is than you - but I think after a little while you could just relax and try to absorb as much as possible. I love how everything in these emails is delivered as a calm series of reflections, chronicling with great honesty his own changing opinions over time - nothing is a diktat. I also found it rather heartening that he makes the same copy/paste mistakes that the rest of us do - how many times have you duplicated a line and put "x" or "width" on both lines..? Seemingly Carmack can actually tell you how many times he's done that!
- smegel 10y ago> the first few code reviews would be fairly traumatic Hopefully because he is saying "did you really think vain attempts at premature optimization were going to impress me?".
- blackbeard334 10y agoNerd rage: https://youtu.be/JjDsP5n2kSM?t=13m41s https://youtu.be/JjDsP5n2kSM?t=13m41s
- samlittlewood 10y agoA good read about Michael Abrash's experience of this: http://blogs.valvesoftware.com/abrash/valve-how-i-got-here-what-its-like-and-what-im-doing-2/ http://blogs.valvesoftware.com/abrash/valve-how-i-got-here-w... (And interesting history about Valve)
- bluecmd 10y agoHonestly, this is why working for someone like Google is so great. All the smart people you can learn from.
- sitkack 10y agoCarmack is smart, but that isn't what makes him interesting. He constantly re-evaluates his beliefs in the face of new facts, the vast majority of people do not do this, regardless of their intelligence.
- mark-r 10y agoMy theory is that this is what separates the good from the great. You can't improve unless you're continually considering what you might be doing wrong, or what you could have done better.
- yannickt 10y agoBrian Hook wrote about Carmack's productivity here: http://bookofhook.blogspot.ca/2013/03/smart-guy-productivity-pitfalls.html http://bookofhook.blogspot.ca/2013/03/smart-guy-productivity... It's an interesting read. Previous HN discussion: https://news.ycombinator.com/item?id=5383650 https://news.ycombinator.com/item?id=5383650
- p0nce 10y agoAbout style A vs B vs C: Robert C. Martin encourages style B because it reads topdown and replaces comments with names.
- andy_ppp 10y agoYes, once you understand functional programming you never want to go back to non-explicit state changes to all of your variables contents without you knowing or your explicit consent.
- xxs 10y agoa snarky remark: 'citation need' Functional programming ain't a panacea, either.
- andy_ppp 10y agoHave you read the article - John Carmack suggest FP and writing pure functions is better.
- accatyyc 10y agoNot really. He suggests that if a function is called from multiple places, one should try to make it pure to avoid subtle bugs. If called from a single place it may be better to inline it. Citation: "I don’t think that purely functional programming writ large is a pragmatic development plan, because it makes for very obscure code and spectacular inefficiencies, but if a function only references a piece or two of global state, it is probably wise to consider passing it in as a variable."
- Malice 10y agoYour citation is from his 2007 thoughts. His 2014 thoughts: No matter what language you work in, programming in a functional style provides benefits. You should do it whenever it is convenient, and you should think hard about the decision when it isn't convenient.
- jbooth 10y agoWhich is still a very different statement from the way functional zealots would put it. Carmack's talking about pure functions at the architecture/design level. Within those functions, there's still lots of temporary mutable state, I'd be willing to bet. He's writing graphics code, he's probably not passing functions to functions in order to sum an array, he'll just do the fast, iterative thing.
- schlipity 10y ago>I now strongly encourage explicit loops for everything, and hope the compiler unrolls it properly. I get why this is a thing. Sometimes an unrolled loop is faster. But if this is really an issue, why isn't there a [UnRoll] modifier or a preprocessor or something that handles that for you? Something like this: for (int i = 0; i < x; i++;) { dothing(x[i]); } versus: unroll for (int i = 0; i < x; i++;) { dothing(x[i]); } Only the compiler / preprocessor would unroll the second one. You have the best of both worlds with a reduced chance of subtle errors.
- shin_lao 10y agoYou mean something like... __attribute__((optimize("unroll-loops"))) ? :-)
- efaref 10y agoThe compiler has heuristics for unrolling, and ought to do it automatically when appropriate. Sometimes unrolling hurts performance, as you make the code larger and therefore reduce i-cache performance.
- chli 10y agoThis kind of pre-processor / compiler specific keyword is available in most embedded C/C++ compiler. See http://www.keil.com/support/man/docs/armcc/armcc_chr1359124992247.htm http://www.keil.com/support/man/docs/armcc/armcc_chr13591249... For example.
- smcl 10y agoMany compilers let you supply pragmas that give hints as to how many iterations a loop will usually be hit, whether this will multiples of x etc. Compiler can then use this info to decide whether to apply various optimisations. I think it's not part of C because it's kinda an underlying detail - explicitly informing the compiler how it should optimise something shouldn't really be part of the language really, pragmas are good for this
- xxs 10y agobtw, there shouldn't be a semicolon past i++ (i.e. the code won't compile) any compiler worth its salt should be able to unroll w/o explicit demand from the developer.
- bluetomcat 10y agoAnother perspective in defense of long functions is that they enable you to spot common expressions/statements within the body, for example: void long_func(void) { ... if (player.alive && player.health == 100) { .... } ... if (some_other_condition && player.alive && player.health == 100) { } } Conventional wisdom says that you should write a function `is_player_untouched` and substitute the composite expressions with function calls, but the code in question can be refactored in a much more straightforward way: void long_func(void) { ... const bool player_untouched = is_player_untouched(); if (player_untouched) { .... } ... if (some_other_condition && player_untouched) { } } Had the function body been split into more functions for "clarity", you would be doing duplicate calls to `is_player_untouched()` which go unnoticed because they would be buried deep in the call graph.
- rubber_duck 10y agoThis leads to brittle code - it's easy to determine if is_player_untouched state changes between those two conditions when writing the code but when someone else edits that code it's also easy to introduce something that will break it and the monolithic/big function makes it hard to keep track of assumptions like this and it leads to a lot of code changes in those big functions as well. Even worse this kinds of bugs usually end up being hard to test if you're not covered with unit tests, and games rarely are, and big functions go against testing practices.
- bluetomcat 10y agoIt sure leads to brittle code if assumptions and contracts in the code are not crystal clear, but Carmack's point in this particular article was about writing "consistently performing" code which doesn't degrade under specific conditions. In my refactored example, you wouldn't be eventually calling `is_player_untouched()` once more if `some_other_condition` is true.
- icebraining 10y agoHopefully, that should be something the compiler could do on its own; assuming the language has appropriate semantics, it could identify the common expression, prove that it's value can't have changed in between, and then do the substitution on its own. Is there any language where this is implemented, or is the effort too great for the gains?
- typedef_struct 10y agoThis is a pet peeve of mine. If you made a block of code into a separate function, I'm assuming it's called from multiple places. Or maybe it used to be. Or will be soon. But still.
- sickbeard 10y agoIt's hard to unit test large functions.
- deleted 10y ago[deleted]
- jon-wood 10y agoThe thing that struck me was Carmack's relentless pursuit of perfection. I can't think of many people who'd describe a single frame of input latency as a cold sweat moment!
- dfan 10y agoWhen you are making videogames, a (video) frame of latency is a big deal. When I worked on Guitar Hero and Rock Band, we worried about sub-frame latency (timing is more important when you're hitting a drum than when you're firing a gun).
- speeder 10y agoHow you handled CRT vs non-CRT screens? I still use CRT screens, not because of latency, but because of better contrast and colour reproduction, and the capability to use whatever resolution I want. I noticed that in new games, and using newer video-cards, there is some kinda weird lag there, like if they were geared on purpose for slow LCDs (there seemly even some variables that you can control on AMD cards, using Windows Registry, or tweaking the Linux driver, related to screen input lag, they are on the "PowerPlay" part of the drivers for some reason though, I couldn't figure yet what they do exactly). EDIT: Also, I stopped playing music-games almost entirely, I found many of them completely unplayable on my setup, I just can't find the correct settings to make the timing work. The least aggravating one is "Necrodancer" that seemly is really good in calibrating.
- dfan 10y agoThere are so many AV setups, we had to leave calibration to the user (we tried with auto-calibration but it could not always be perfect). The fundamental problem is that there are two independent delays that both depend on your individual system: the delay from the time that the console produces a video frame to the time that the user sees it, and the delay from the time that the console produces a sound to the time that the user hears it. In a beatmatching game, you really need the user's perceptions to be in sync, which means delaying either the video or the audio. Of course, the more you delay one or the other, the more the repercussions you run into. In a regular video game, it's not a big deal if you fire a gun and hear the shot 50ms later, but in a beatmatching game, that delay is really noticeable.
- hiou 10y agoI think this is a great example of something that is different for an exceptional developer as opposed to an average one. A developer like Carmack and likely the teams he works with are able to keep a much larger system in their head at one time than an average developer. And this is typically why they can write larger functions like that and get away with it. A less talented developer will be much more likely to introduce bugs near the top of that function over time as they struggle to maintain the entire function in there head. Sometimes choosing the correct tool has more to do with the craftsman than the craft.
- ajuc 10y agoAs an average developer I find it much easier to keep the system in head when it's not cut into 10-line-long pieces in random order. Hiding the complexity doesn't make it irrelevant suddenly. That's how you get code that does the same thing 5 times in 5 different branches of highly nested call tree "just to be sure".
- yxhuvud 10y agoFunctions with the correct level of abstraction doesn't hide the complexity - it categorizes it and put names to the different categories.
- dzdt 10y agoI have had the pleasure to work with lots of other people's code of varying styles and quality. Nothing is harder to read and understand than code which is deeply nested calls from one little helper (or wrapper) function to another. Nothing is easier to read and understand than code which just flows straight through from top to bottom of a big function. There are other tradeoffs of code reuse and speed and worst-case-speed and probability of introducing bugs. If you haven't read the article, do, its worth it. I love that Carmack tries to measure which styles introduce more bugs! Who else does that? Seriously, I would love to see more of that.
- andrewingram 10y agoI'd have thought at least a few people measure bug ratios of different coding styles? It's a big motivator in terms of deciding up best practices. And whenever I fix a bug, I try to identify the root cause. If it was a coding pattern to blame (as opposed to bad process or design) I look for ways to eliminate that pattern.
- hndl 10y agoWe do have Cyclomatic Complexity (https://en.wikipedia.org/wiki/Cyclomatic_complexity https://en.wikipedia.org/wiki/Cyclomatic_complexity). I see this often enough to understand how important it is.
- rakpol 10y agoIsn't cyclomatic complexity just about the number of paths through code? It seems like (statically resolved) nested calls, as long as they don't loop or branch themselves, shouldn't increase cyclomatic complexity.
- sb8244 10y agoThere may be subtle branches like error handling that can impact it for otherwise non branching code
- rakpol 10y ago
- TickleSteve 10y agoCorrect me if I'm wrong (I only skimmed it) but this is less about not liking inlining than having deterministic/time-bounded performance. These are two separate/orthogonal issues, I doubt he would turn his nose up at the processor doing less work iff it was also deterministic and had predictable worst-case timing.
- bluetomcat 10y agoI would sum it up as "reducing variability in code paths", i.e. not causing degraded performance when certain conditions change.
- TickleSteve 10y agoyes, but the intention behind that is to have control and knowledge over the maximum time a function will take. What he is effectively saying is to treat all code in the same manner as you would for a hard-real-time system. I certainly agree with this for performance critical code (performance being overall duration or latency), but this is not a one-size-fits-all solution. There are a lot of cases where this is not appropriate.
- ajuc 10y agoIt depends if we optimize for thtoughput or latency.
- TickleSteve 10y ago(I mentioned overall duration or latency which covers your point). but those two cases are the same.... they're both performance-critical code. Not all code is.
- ajuc 10y agoI would argue in this context "optimize running time" is different while "optimize latency" and "don't care about performance" are similar. I mean - if you don't care about total running time it makes sense to remove special cases that don't change the worst case because of readibility/bugs, no matter if you care about latency.
- ctrlrsf 10y agoLong functions might read easier but you lose some testing precision. I think recent focus on testing has lead to shorter functions with as little responsibility as possible. When short functions fail a test you have smaller surface area to look for the cause.
- leojfc 10y agoDoesn’t this imply the need for a new language feature? So that well-defined sections of inline code can be pulled out, initial conditions set in a testing environment, and then executed independently. I guess this could trip up if the compiler optimisations available when considering all the code at once means that the out-of-context code actually does something different in testing...
- chimprich 10y agoPull out well-defined sections of inline code that can be executed independently for testing? Sure, that's breaking it down into functions.
- MereInterest 10y agoI think there is such a feature. In most languages, these are called "functions".
- virtualized 10y agoGame developers don't test, they debug. That's why he draws wrong conclusions like "inline everything manually".
- lj3 10y agoInterestingly, Casey Muratori accidentally demonstrates during one of his Handmade Hero sessions that the compiler won't always be able to optimize certain bits of code that are put in a function as opposed to being inline. In the video, he inlines a very simple function and his game gets twice as fast for no apparent reason. It's instructive to watch him dive into the generated assembly to figure out why. https://www.youtube.com/watch?v=B2BFbs0DJzw https://www.youtube.com/watch?v=B2BFbs0DJzw
- sitkack 10y agoTIL, another reason to perform 'defactor inline-method' (with directed feedback of course).
- Longwelwind 10y agoI'm not sure it should be considered as a reason for anything. The most important point of the article, imo, is: If a function is only called from a single place, consider inlining it. You should consider inlining your function, not always do it. Recently, I made a mod for a game and I had to draw an UI by code, and there, it made sens to use one-time function because it made the code easier to read (super-expressive functions like DrawLeftPane() or DrawHeader(), and next to no ties between functions). Most of the time, code readability should be prioritized over performance.
- sitkack 10y agoTotally agree, but in perf critical inner loops, it might be interesting to speculatively inline different functions and measure perf. Overly factored code inside an inner loop has been shown in the video to cause compiler confusion.
- sqeaky 10y agoPeople write perf intensive inner loops so infrequently that this really ought to be discounted as an argument. I work in two performance sensitive projects, both C++, and this has yet to be a reason to inline code. Algorithm choice is optimization of choice first and so far finally.
- apeace 10y agoI think there's a big point that's being missed here. Carmack is conflating inlining code with writing functional code. These are different things. I'd agree that if the majority of your code is mutating state, it makes sense to mash all that together in one place. You want to keep an eye on the dirty stuff. But on the other hand, inlining pure functions that don't use or mutate any global state doesn't make sense to me. Why is making it "not possible to call the function from other places" a benefit? How about calling that code from a unit test!
- phkahler 10y ago>> Why is making it "not possible to call the function from other places" a benefit? When it's a pure function that's not a problem. When it changes state then you lose track of ordering and such. That's his point, state changes need to be kept in the one big function so you can keep track of them easily. And of course, almost all interesting software has mutable state. Otherwise you're just doing a computation and looking for a single output.
- apeace 10y ago>> When it's a pure function that's not a problem. That was my point. He's conflating two different things. I understand why inlining mutation has benefits. Just not inlining functional code. >> And of course, almost all interesting software has mutable state. Of course, but I think most programmers overestimate how prevalent state needs to be throughout a program. I once wrote an RSS aggregator as an eight-stage pipeline. It checked about 40k feeds, each every 60 seconds. Every stage had a 'main' file where the vast majority of state was kept. The rest was functional libraries. I suppose that would be a demonstration of what Carmack is proposing, with the difference being that my pure functions (the majority of the code) had clear names and were unit-tested. It worked so well that almost every large program I've written since has been designed the same way!
- MyNameIsFred 10y agoCarmack addressed this, at least as I read it. One of his concluding guidelines was that it purely functional methods are a virtue and should be made purely functional wherever possible (he phrases it in terms of "const"). He did also day, however, that in his domain non-trivial pure-functional methods rarely apply.
- dustingetz 10y ago50% of HN comments have misread this! The first few paragraphs mentioning FP were written in 2014 and are retracting the opinion of the long email about inlining, which is from 2007
- abritinthebay 10y agoNot... quite. He's not retracting so much as saying he's much more positive about FP now in its ability to solve this issues his 2007 email talks about. It's not a retraction so much as an expansion of solutions to include (with caveats) FP. Also it clarifies some drawbacks to the approach on mobile/limited resource platforms.
- anotherhacker 10y agoDon't we write code for other programmers first--then for the system? It seems counter-intuitive but in the long run this mentality best serves the business.
- sophacles 10y agoThe argument in the article is actually a "for programmers" argument. Mainly that sometimes it's easier to reason about inline code than deeply chained functions, because you don't necessarily know all the implications of calling that function. Or you don't know the potential starting states when writing the function.
- softawre 10y agoDepends if you're working on some LOB application with average programmers or if you're writing a game that pushes the limits of modern technology.
- Practicality 10y agoI wonder how much of this change is because he can no longer keep track of so much state in his head (or just doesn't want to). I only say this because I've gone through a similar transition of valuing my mental computation time in the last 20 years of coding :). The efficiency of inlining is compelling when you code the whole thing at once, in one session. Once you decide to break the work up over multiple sessions, it's too much to keep in your head over multiple days (or weeks).
- hellofunk 10y agoOne of his definitions of a pure function is one that only has by value parameters and doesn't change state. Am I correct in thinking that in C++, the advent of C++11 lambdas allows you to be explicit about this and prevent the compiler from allowing you to accidentally use variables from outside the scope of the function's parameters, by writing lambadas (named, if necessary, like a normal function) with no-capture lists ("[]") which would force you to work in a more pure style. In C++, what other method might help you enforce purity?
- pbsd 10y agoYou can still freely access global variables inside lambdas, with or without captures.
- hellofunk 10y agoThanks for clarification. I thought the capture list was required for this. I suppose the capture list is for capturing variables that are local to the scope of the lambda's definition.
- hellofunk 10y agoI have been testing this, and I'm not convinced. A lambda with no capture at all (just []) will not compile if it accesses any variable in the scope of the lambda definition (including globals). I must add [=] or similar to get access to them. What were you referring to? The compiler error is clear: "variable 'lam' cannot be implicitly captured in a lambda with no capture-default specified"
- logfromblammo 10y agoIf you occasionally inline all the functions and unroll all the loops, you can occasionally find optimizations that even the compiler won't be able to make. For example, in quaternion-based rotation math, there exists a "sandwich product" where you take the (non-commutative) product of the transform and the input, followed by the product of that result and the conjugate of the transform. It turns out that several of the embedded multiplication terms cancel out in that double operation, and if you avoid calculating the canceled terms in the first place, you can do a "sandwich product" in about 60% the total floating-point operations as two consecutive product operations. In the application that used spatial transforms and rotations, the optimized quaternion functions were faster than the 4x4 matrix implementation, whereas the non-optimized quaternion functions were slightly slower. That change alone (adding an optimized sandwich product function) cut maybe 30 minutes off of our longest bulk data processing times. You would never be able to figure that out from this. out = ( rotation * in ) * ( ~rotation ); You have to inline all the operations to find the terms that cancel (or collapse into a scalar multiplication).
- nickm12 10y agoI'm surprised at the enthusiasm for really long functions here. I find my experience is just the opposite—I find it much more difficult to read code written that way than when the different sections are broken up into smaller functions. It is of course essential that the smaller functions be well-named and manage side-effects carefully. That is, they should either be pure functions, or the side effects should be "what the function does", so that readers of the main function don't generally need to read the function's code to understand its side effects.
- mark-r 10y agoYes, a long monolithic block is hard to read. But he's suggesting using comments and braces to visually separate it into blocks, so the end result is a happy halfway point between monolithic and broken-down functions. The intro suggests that he agrees that pure functions are an even better solution.
- vfaronov 10y agoA side benefit to having separate, clearly named functions: they often play better with tools. You can jump to them by name from a completely different part of the code (with some editor support). You see them in the headers of your Git hunks so you don't lose context. In languages that are heavy on type inference, like Haskell or Python+MyPy, they make for a convenient boundary at which you can assert your types, to help with type errors.
- qwertyuiop924 10y agoWell, yes, but in games, you have a lot of global state. You wind up manipulating so much state that you may indeed want this kind of large function so that you can see everything.
- Kenji 10y agoand I was quite surprised at how often copy-paste-modify operations resulted in subtle bugs that weren’t immediately obvious. I noticed this quite some time ago. This is also a major source of bugs that I write. That is, until I decided to stop copy-pasting more than a word at all, and retype everything character by character when I need it again. Interestingly enough, this saves a lot of time because the bugs I would generate otherwise cost way more time than a bit of typing.
- qwertyuiop924 10y agoAlthough I am a fan of the LISP school of program design (minimal global state, build small functions and macros, make sure they work, and than build more functions and macros on top of that, until you have an abstraction that you can build your app on), Carmack raises some interesting points: If you're handling a lot of global, mutable state, you may want to abstract minimally, so that you can see where that state is mutating, which makes bugs easier to spot. Not a bad idea.
- mgregory22 10y agoMaybe the key idea is to put all the global state mutations as closely together as possible, so they can be compared and contrasted as easily as possible.
- qwertyuiop924 10y agoWell, yeah, that's a big part of it.
- corysama 10y agoReposting my comment from the last time this was posted. There was a lot of nice discussion there: https://news.ycombinator.com/item?id=8374345 https://news.ycombinator.com/item?id=8374345 === The older I get, the more my code (mostly C++ and Python) has been moving towards mostly-functional, mostly-immutable 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); // 2nd bar's lifetime does not overlap with the 1st result2 = bar.awesome(); // return bar.awesome(); } return result1.howAwesome(result2); } If it's done strictly in the style that I've shown above then refactoring the blocks into separate functions should be a matter of "cut, paste, add function boilerplate". The only tricky part is reconstructing the function parameters. That's one of the reasons I like this style. The inline blocks often do get factored out later. So, setting them up to be easy to extract is a guilt-free way of putting off extracting them until it really is clearly necessary. === In the earlier discussion sjolsen did a good job of illustrating how to implement this using lambdas https://news.ycombinator.com/item?id=8375341 https://news.ycombinator.com/item?id=8375341 Improvements on his version would be to make everything const and the lambda inputs explicit. AwesomenessT largerFunction(Foo1 foo1, Foo2 foo2) { const ResultT1 result1 = [foo1] { const Bar bar = barFromFoo1(foo1); const Baz baz = bar.makeBaz(); return baz.awesome(); } (); const ResultT2 result2 = [foo2] { const Bar bar = barFromFoo2(foo2); return bar.awesome(); } (); return result1.howAwesome(result2); } It's my understanding that compilers are already surprisingly good at optimizing out local lambdas. I recall a demo from Herb Sutter where std::for_each(someLambda) was faster than a classic for(int i;i<100000;i++) loop with a trivial body because the for_each internally unrolled the loop and the lamdba body was therefore inlined as unrolled.
- saynsedit 10y agoI think Carmack is conflating FP with good abstractions. Haskell abstractions are often good because they flow from category theory and there are usually well established mathematical laws associated with them. I'm thinking of the "monad laws" and the "monoid laws." Mathematicians tend to create abstractions if the abstraction satisfies coherent and provable properties. Programmers tend to be less rigorous about what and how they abstract. There is nothing about C++ that prevents making good abstractions. It's just the culture of the language. Industry programmers are taught to not duplicate code and to keep functions short but they are not taught the fundamentals of what makes a good abstraction.
- Animats 10y agoBoth games and low-level real time systems have one big loop executed at a fixed rate. That leads to the architecture Carmack describes. It's not particularly helpful to a server that's fielding vast numbers of requests of various types.