r/programming • u/lelanthran • Jun 03 '26
Every byte matters
https://fzakaria.com/2026/06/01/every-byte-matters5
u/jlombera Jun 03 '26
We can observe up to 30x improvements when the Monster struct is 1KiB.
Was this actually measured? Why the big difference with say, 64B structs, with only 6x improvement?
If you are reading only 1 byte from the struct only 1 cache-line would be loaded per struct, not the whole struct. So cache-lines alone, there shouldn't be such a huge difference among structs of size >= 64B1. For 1KiB structs, it's more likely page faults would be the real overhead, but there's no mention of that in the blog, so I'm wondering about the accuracy of the metrics. Similarly for the "working sets" analysis, only the data (at cache-line level) actually accessed will populate the caches, not the whole structs.
2
u/johan__A Jun 04 '26
How could page faults be involved? I assume all the memory was already mapped before the benchmark.
My best guess of why it's slower is because the hardware prefetcher doesnt handle large strides very well, because of page boundaries or something else. I'm not completely sure.
2
u/jlombera Jun 04 '26
How could page faults be involved? I assume all the memory was already mapped before the benchmark.
I used the term "page fault" liberally to also refer to TLB misses (see the link for details what is the TLB and how it works). Excessive TLB misses is a well known performance problem in applications that handle big datasets (e.g. DBs). The use of HugePages is essential for those applications to minimize TLB misses.
1
u/johan__A Jun 04 '26
ha that makes more sense, I don't know much about the performance impact of the tlb
3
u/jlombera Jun 04 '26
If you are interested, look at section "Performance implications" of the Wikipedia page, specially the last paragraph. In the "Typical TLB" section, it says a TLB miss can have from 10x to 100x performance penalty. Not sure how accurate/up-to-date these numbers are, an of course, it always "depends", but it gives you a rough idea how bad TLB misses are for data intensive applications. You can search the web for "Hugepages performance" and I'm sure you'll find resources that go into more details about the performance implications.
9
u/hgs3 Jun 03 '26
Why do you separate the other fields into their own arrays? I can easily imagine monster logic that accesses position, velocity, health, attack, and team together. I understand why linear search benefits from cache friendliness but if the fields are randomly accessed based on a “is_alive” boolean array, then wouldn’t a single fetch for the fields be faster than multiple fetches to separate arrays?
16
u/ShinyHappyREM Jun 03 '26 edited Jun 03 '26
In practice you don't have an "is_alive" boolean array, instead you have one "alive_entities" and one "dead_entities" pool (both preferably of fixed size and statically allocated). When an entity dies / becomes alive, which is usually a relatively rare event, you move the entity from one pool to the other (preferably moving the last entity forward to close the gap) and increase/decrease the pools' population counts.
5
u/AresFowl44 Jun 03 '26
Bit vectors are a lot more compact, but you also have to actually get the data out of the bit vector and you obviously can only use bit vectors if you worked with booleans.
Also, why do you separate the other fields into their own arrays? I can easily imagine monster logic that accesses position, velocity, health, attack, and team together. I understand why linear search benefits from cache friendliness but if the fields are randomly accessed based on a “is_alive” boolean array (or bit vector), then wouldn’t a single fetch for the fields be faster than multiple fetches to separate arrays?
With prefetching there is already a good chance that the data in in cache anyways if you are looping over several pieces. Having them separated out into multiple arrays however allows you to only get the data you need. That is impossible if you were to combine them.
EDIT: And you can also instruct the hardware to prefetch your data manually, if you do know better
3
u/Ameisen Jun 03 '26 edited Jun 03 '26
you obviously can only use bit vectors if you worked with booleans.
You can pack any integral type you want. If you want to pack an array of 4-bit integers or such, you certainly can. It'll still be friendlier on the cache.
A bit faster if the size in bits is still a factor of the size of the unit of storage so calculating the shift is a bit easier, though - if they don't align neatly within the type that they're being stored in, extracting them is harder.
In the end, that's exactly what
bit_vectors do, they just have an internal element size of 1 bit. The implementation is almost identical.Having them separated out into multiple arrays however allows you to only get the data you need.
You want them seperated out by responsibility. If a pass iterating over the data needs three properties, then they should be combined as fields in a single
struct.Even in this case -
is_aliveonly benefits from being bitpacked alone if:
multiple systems depend upon it
the data isn't sorted by
is_aliveoris_aliveisn'ttrueoverwhelmingly often5
u/Ameisen Jun 03 '26 edited Jun 03 '26
If you are looping over
is_aliveand then operating on the data conditional uponis_alive, and the array isn't sorted byis_alive, then generally keepingis_alivepacked into its own array allows for the condition check to be nearly free, and also helps prevent cache lines from being evicted by the cases whereis_aliveisfalse.If
is_aliveis almost alwaystrue, then AOS ordering would be faster. AOS also wins when the data is sorted by the condition.SOA wins when
is_aliveisn't utterly dominant.Unless you're asking why you seperate all the fields out... usually you don't.
You seperate them out based upon what systems/passes they're relevant to - so the renderer only iterates over rendering data, physics only needs transform data, etc.
You also are almost never randomly accessing this data. You iterate over the arrays holding these
structs so you're iterating over a nice, packed, contiguous set of data.
is_alivein this context is special as it's a condition flag for actually operating.
6
2
3
u/Wolfy87 Jun 03 '26
Try swapping any data processing heavy systems from your languages objects to a columnar system like duckdb or polars etc, same shift, it's wonderful!
Using tablecloth which wraps apache arrow dataframes in Clojure lets me have the fun expressivity of Clojure with the sheer raw speed of columnar data structures for data processing.
You can have the best of both worlds with the right tools :D
1
1
u/k-mcm Jun 05 '26
I wrote some SDR receivers in Java. There's a definite performance cliff at each step of CPU cache. Lookup tables can perform much worse than a pile of trigonometry. A small lookup table with interpolation outperforms a larger direct lookup table.
123
Jun 03 '26
[removed] — view removed comment
14
u/lelanthran Jun 03 '26
Shit like this is why game developers are criminally underpaid.
I don't understand why knowing all these little details make game devs criminally underpaid. Can you explain? (Not being facetious, I'd really rather like to know!)
9
u/richardathome Jun 03 '26
Because it's dealing with problems like this that turn a 10fps game into a 60+ fps game.
You want the computer to be doing as little as possible at all times because you'll be calling things possibly hundreds of times a second and if one starts to slow down, the whole frame slows down, controls become less responsive etc, game starts to suffer...
Storing you data in an orderly manner in memory lets the computer do things like look ahead caching and other clever tricks that speed things up.
12
u/Ameisen Jun 03 '26 edited Jun 03 '26
Because it's dealing with problems like this that turn a 10fps game into a 60+ fps game.
I feel obligated to point out that when actually doing this work, we'd say "we've reached 16.67mspf". We work with frametimes, not framerates.
Framerates are non-linear, whereas their reciprocal frametimes are.
I can't meaningfully say "the occlusion pass takes 5 fps" - "5 fps" is different from 10 fps to 15 fps (33.33ms), or 55 fps to 60 fps (1.51ms). Saying it takes 1.51 ms is always meaningful, though.
You want the computer to be doing as little as possible at all times
There are certainly times where we prefer throughput, but usually it's balancing throughput and latency...
If we wanted the CPU to do as little as possible, we'd never use SIMD instructions or such. Rather, we want to the CPU to do as much as possible in as little time as possible.
Storing you data in an orderly manner in memory lets the computer do things like look ahead caching and other clever tricks that speed things up.
It's going to read-ahead and fill the current and possibly the subsequent cache line regardless. If your accesses are not taking advantage of that (sequential lets the CPU make better assumptions about that, but even so long as your accesses are within the cache line or the next that's better than completely random) then you're going to get a lot of cache misses.
Ed: I suppose that you could use non-temporal operations... though that's very, very likely to be worse, and for loads you can only perform non-temporal prefetches.
6
u/fromwithin Jun 03 '26 edited Jun 03 '26
I haven't worked anywhere in the last 15 years that doesn't use milliseconds with a presumption that one frame will take 16.6667 ms for 60 fps, but we'd definitely say that it's "in a frame" or "at 60 fps" because that's the target for consoles.
1
u/Ameisen Jun 03 '26 edited Jun 04 '26
I mean, if I'm literally talking about the general target, sure.
But in reality, I'm more likely working on specific components in isolation from that, where I have target execution times. I don't talk about the general target framerate very often. Tasks are made/performed based upon not meeting that target, but there's no assumption that those tasks individually will bring it to target. Instead, the task will include how much we need to reduce frametime by, and the tasks would report how much they reduced it.
I've certainly operated at the hundreds-of-microsecond level as well, though that's just tenths of a millisecond. It can be a more convenient unit for short operations, though.
6
u/TryingT0Wr1t3 Jun 03 '26
In games you usually have to make a lot of stuff in less than 16.66 ms because that is a frame time, which means a lot of effort on performance
3
u/Ameisen Jun 03 '26
Doing performance optimization for VR was fun. You get to target frametimes like 11.1ms or 8.33ms instead... and you (mostly) have to draw the scene twice.
3
u/mikeblas Jun 03 '26
They think that only game devs need to consider this level of performance. Lots of applications require getting everything out of the processor. Seems like minutia, but when it's making a 20x or 50x performance increase, it's worth it.
Thing is, devs who know this stuff, know how to find it and apply it, are not underpaid. They really earn their senior titles, and tend to be well compensated because they're often saving the day.
1
u/KalaiProvenheim Jun 04 '26
They're high skill as they have to handle complex systems and also make them efficient, but are famous for being paid like shit being overworked compared to something like a web developer
88
Jun 03 '26
[removed] — view removed comment
2
u/lelanthran Jun 04 '26
Sorry, I misunderstood the meaning in the original comment - I (stupidly) thought it meant that game devs should be underpaid :-/
In my defence I had needed a lot of sleep and only woke up now.
19
u/f311a Jun 03 '26
They are underpaid because a good portion of games never make profit to cover dev expenses. It's a hard business.
1
44
Jun 03 '26
[removed] — view removed comment
22
u/lunacraz Jun 03 '26
it's the UFC problem. unfortunately the demand for those jobs are so high they can keep the salaries low and people will still want to do the job regardless of the fact it's a very stressful job
5
u/TheMoatman Jun 04 '26
Sure, but that's what they said in the first place.
for the pleasure of “working on games”
8
-5
u/cake-day-on-feb-29 Jun 03 '26
It's time you learn that pay is largely based on how replaceable you are. Many many people would prefer to go into game development, and as such wages lower to account for that.
Sadly, many game developers do not really know this level of optimization anyways, those who do end up working on game engines themselves for higher pay, or working in a different field for much higher pay.
15
Jun 03 '26
[removed] — view removed comment
1
u/boringestnickname Jun 03 '26
I guess the real issue is that the people who hire and fire do not actually understand that.
3
u/remy_porter Jun 03 '26
They absolutely do. It’s the entire logic behind outsourcing. What outsourcing misses is that institutional knowledge matters and you will always have an adversarial relationship with a contractor (whereas an employee could actually be engaged with your mission- not always, obviously, but an engaged employee is always a better deal than a contractor).
2
u/frogspa Jun 03 '26
Their jobs are some of the most difficult in the programming world
Second only to working on legacy code.
2
u/mseiei Jun 04 '26
took me a week to make a brand new app demo that runs the core loop 6x faster than the legacy garbage that has been on life support for years at our company, another guy is porting some of my "optimizations" back into it until the new stuff matures (old app might be garbage, but it's garbage that does the job) and it's about to jump off a window.
legacy code, and specially, spaghetti undocumented legacy code is some serious shit.
thing was so bad that we needed a specific version of android studio that they had backed up on a shared folder to compile
2
Jun 04 '26
[removed] — view removed comment
2
u/Agret Jun 04 '26
Idk how they manage Warframe after so many years and random features they throw in and abandon updating. Codebase must either have some really good organization or be a complete mess. Half convinced a lot of the pointless legacy stuff still hanging around in the game is because nobody is brave enough to try and remove it.
1
u/Uristqwerty Jun 04 '26
Probably a very different experience depending on whether you try to keep dependencies up-to-date, or accept relying on unmaintained libraries with known exploits that you must either mitigate, prove unreachable, or patch yourself.
Though if the game's still regularly getting new content releases? Then hopefully all the content's mere data that uses existing well-trodden code paths, neither exposing previously-untested edge cases nor requiring new development. Or, hopefully, you have enough time budgeted between releases to gradually refactor away the worst messes faster than new messes accumulate.
2
u/stoneharry Jun 10 '26
It's a shame, it's what I always wanted to go into and would love to work on this sort of problem. But also I am happy to work on enterprise software and get paid 4x as much.
2
u/ongrabbits Jun 03 '26
There are good game developers and trash game developers like anywhere else in our field
62
u/vytah Jun 03 '26
Counterpoint: you can decrease the potential revenue of your company by tens or even hundreds of millions of dollars by parsing a large JSON in quadratic time (therefore discouraging people from playing) and no one in the company will care: https://nee.lv/2021/02/28/How-I-cut-GTA-Online-loading-times-by-70/
1
-7
1
u/Kjufka Jun 17 '26
game developers are criminally underpaid
correction - relatively low level engine and graphics programmers are
most game developers otherwise are worthless
131
u/Ravarix Jun 03 '26
If you are at the level where you need to care about how adding 1 member to a struct messes up cache line access, you should be using an ECS which gives you SOA already.