Live data from Hacker News

Every Byte Matters

fzakaria.com

101–110 of 159 posts

Re: Every Byte Matters

#101
post #9

> The cost of each new field is rarely considered Most developers, in Java and in most other languages, do not consider the cost of every field, but I can tell you that people who need micro-optimisations certainly do care, and in Java's standard library, a layout is very much a concern (except, as always, you want to optimise what really matters; there's no point in optimising something that is unlikely to be a hot…

And probably, those optimization could be automated by LLM's.

Re: Every Byte Matters

#102
Perhaps worth noting that the number of lines in a cache is often different than the number of rows, which can be relevant for some workloads.

The size of an ordinary cache is rows × ways × size(line), where rows = 2 ↑ num-idx-bits. For example, most Intel 64 and AMD 64 processors use log₂(size(page)) − log₂(size(line)) = 12 − 6 = 6 index bits for the L1 cache*, so an L1 cache with 8-way associativity is 64 sets × 8 lines/set × 64 bytes/line = 32 KB large, and an L1 cache with 12-way associativity is 64 × 12 × 64 = 48 KB large. I remember being surprised to learn that most processors have only 64 rows in the L1 cache!

*So that virtual indexes and physical indexes are identical (so that retrieval of the row can happen in parallel with TLB lookup).

Re: Every Byte Matters

#103

Earlier quoted context omitted.

Assuming ordering isn't a concern, can't you just have a field called "removed" and skip those when iterating? Or swap it with the last monster, and keeping an index for the last monster alive.

Then you have to read the "removed" field on every field read on every operation. SoA is only useful when you don't read multiple fields for most operations.

Two fields should be fine, actually. The way caches are organized you are very unlikely to thrash with the lookups (due to n-way associativity) while only keeping relevant data in the cache at the same time. You still have roughly the following layout (in the cache), where A is the field and V is valid:

  | A1 A2 A3 A4 | A5 A6 A7 A8 | ...
  | V1 V2 V3 V4 | V5 V6 V7 V8 | ...
The former access pattern still yields a clean cache layout where no unnecessary data is loaded (which is the most costly operation here by far) as opposed to

  | A1 V1 B1 C1 | ... | A2 V2 B2 C2  | ...
In the general case there will exist a number of fields for which SOA layout will be worse if all are accessed close to each other, but for just a validity indicator this should not be the case. I think your statement is not wrong, but also not 100% correct.

This is on par to linear search being faster than binary search for small n. As soon as caches and branch prediction chime in many rules of thumb just change. Most importantly, however, is that a distinction between small and large n basically _needs_ to happen at that point.

Re: Every Byte Matters

#104
post #99
post #78

Earlier quoted context omitted.

This is valid for sequential scanning of the data. The CPU will fill whole cache lines at once with the arrays that do get used and the algorithm touches all the field instances in the array. Now think about random access to single struct instances instead: the CPU loads a cache line worth of data for each field and uses only one element out of the whole cache line. This is much worse than a compact structure represe…

This sounds similar to relational databases vs document oriented databases, at least when I briefly looked into database like MongoDB when such things were all the rage 15-20 years ago. For the internal web site that customer support people used a document oriented database would be great because that wants to load everything about one customer and pretty much doesn't need anything else until the user is done support…

The Array-of-Struct vs Struct-vs-Array organization is actually more similar to row-major ordering vs column-major ordering, i.e. the data structure that analysis databases use to optimize for aggregate calculations. Document databases are not really comparable because they don't impose structure on the data; with document databases you just have a tree of JSON elements, which is neither AoS nor SoA.

Re: Every Byte Matters

#105
post #3

So if you need speed, you just have to swallow your OO programmer's pride and put your data in arrays.

... IF that's your main performance problem.

I already know I'm dealing with huge perf issues caused by ORM & lazy-load semantics. I/O abuse is usually going to be so, so much worse than memory/cache issues. Java is mainly used for business information systems, where I/O is king. Plain vanilla memory abuse is also a big one.

But my main problem is a mgmt convinced the magic wand of AI will make all sorts of problems dissapear, and it's going to take 5 years for them to realize nope.

It's still fun to learn about cache optimization though, esp. when someone makes it reasonably digestible like this. And maybe it also helps people to recognize that OOP is not some great over-arching zen truth of truths.

Re: Every Byte Matters

#106
Zig's MultiArrayList is a cool language feature to support objects of collections, and I wish more languages had first class support for it (without overhead of copy's).

Re: Every Byte Matters

#107

Data Oriented Design rocks. It was the subject for my CppCon 2025 keynote: https://youtube.com/watch?v=SzjJfKHygaQ

Oh, I was just watching this yesterday and got a little re-energised about getting back to more active development of my DoD JS engine! Thanks!

Re: Every Byte Matters

#108

Earlier quoted context omitted.

It's because removing a monster with 20 fields from an SoA structure means resizing 20 arrays. Removing the same monster from an AoS array involves resizing a single array, which you're going to process in a very cache friendly way.

I'm not sure why anybody would at the same time be implementing SoA AND resizing 20 arrays for a single delete, those things seem to be on either ends of the "I care about performance" spectrum.

The point is that a simple SoA implementation requires this - each field in the monster struct is an item in 20 different arrays. So, removing one monster means removing that item from those 20 arrays.

Now, as others have suggested, you can have a more complex implementation, where instead of removing the monster's fields from those arrays, you just mark them as "dead" or whatever and then skip them when consuming the relevant arrays, with some relatively small extra bookkeeping overhead. Of course, this comes with its own drawbacks, especially if the number of monsters is very dynamic and you are memory constrained.

The point is not to say that SoA is never good for performance, it obviously and certainly is, probably even in most cases. It's just not always best for performance, this was all.

Re: Every Byte Matters

#109

Earlier quoted context omitted.

It's because removing a monster with 20 fields from an SoA structure means resizing 20 arrays. Removing the same monster from an AoS array involves resizing a single array, which you're going to process in a very cache friendly way.

Assuming ordering isn't a concern, can't you just have a field called "removed" and skip those when iterating? Or swap it with the last monster, and keeping an index for the last monster alive.

Sure, but these schemes might have their own drawbacks depending on the exact use case - especially if you have a very dynamic number of monsters and constantly add and remove them (say, some kind of bullet hell style game).

Re: Every Byte Matters

#110
post #3

So if you need speed, you just have to swallow your OO programmer's pride and put your data in arrays.

If you had the right language you could use AoS syntax with SoA implementation. I heard Jai was going to have this feature?
Post reply on HN