Re:CPU Cache-Friendly Data Structures in Go: 10x Speed with ...
by solatic on Thursday October 09, 2025 @11:45AM
(#45529326)
Most modern processor architecture CPU cache line sizes are 64 bytes, but not all of them. Once you start to put performance optimizations like optimizing for cache line size, you're fundamentally optimizing for a particular processor architecture.
That's fine for most deployments, since the vast majority of deployments will go to x86_64 or arm64 these days. But Go supports PowerPC, Sparc, RISCV, S390X... I don't know enough about them, but I wouldn't be surprised if they weren't all 64-byte CPU cache lines. I can understand how a language runtime that is designed for architecture independence has difficulty with that.
Re:CPU Cache-Friendly Data Structures in Go: 10x Speed with ...
by dadkins on Thursday October 09, 2025 @12:19PM
(#45529810)
The big two, x86_64 and arm64, have 64-byte cache lines, so that's a reasonable assumption in practice. But I was surprised to discover that Apple's M-series laptops have 128-byte cache lines, and that's something a lot of people have and run, albeit not as a server.
Re:CPU Cache-Friendly Data Structures in Go: 10x Speed with ...
by danudey on Thursday October 09, 2025 @01:24PM
(#45530595)
Something like C++17's `std::hardware_destructive_interference_size` would be nice; being able to just say "Align this variable to whatever the cache line size is on the architecture I'm building for".
If you use these tricks to align everything to 64-byte boundaries you'll see those speedups on most common systems but lose them on e.g. Apple's ARM64 chips, and POWER7, 8, and 9 chips (128 byte cache line), s390x (256 byte cache line), etc. Having some way of doing the alignment dynamically based on the build target would be optimal.
Re:CPU Cache-Friendly Data Structures in Go: 10x Speed with ...
by loeg on Thursday October 09, 2025 @03:41PM
(#45532152)
Apple arm64 supposedly has 64-byte L1 cache line size and 128-byte L2? How does that work? Presumably the lines are independent in L1, but can different cores have exclusive access to adjacent lines? What's the point of narrower lines in L1?
Re:CPU Cache-Friendly Data Structures in Go: 10x Speed with ...
by danudey on Friday October 10, 2025 @11:41AM
(#45540223)
Maybe the point isn't narrower lines in L1 but wider lines in L2? Implicitly bringing in more data to the L2 cache but allowing the CPU to pick smaller chunks of it into L1 cache to work on. Something like a forced prefetch or something? Honestly no idea.
As noted by the other comments, Apple's M-series chips seem to use a 128-byte cache line. ARM doesn't mandate that their licensees must use a pre-specified cache line size: 64 bytes just happens to be the consensus-arrived standard.
Re:CPU Cache-Friendly Data Structures in Go: 10x Speed with ...
by jasonthorsness on Thursday October 09, 2025 @12:06PM
(#45529631)
For low-level small things tests can help. Go has good benchmarking built-in and you can use a tool that passes/fails based on statistically-significant regressions, benchstat (https://pkg.go.dev/golang.org/x/perf/cmd/benchstat).
Re:CPU Cache-Friendly Data Structures in Go: 10x Speed with ...
by CamouflagedKiwi on Thursday October 09, 2025 @12:31PM
(#45529955)
I think it's really beyond the power of a linter to understand when this would matter. It'd be issuing warnings on almost every struct out there saying "these two members share a cache line" which you almost never care about.
Re:CPU Cache-Friendly Data Structures in Go: 10x Speed with ...
by pphysch on Thursday October 09, 2025 @11:26AM
(#45529026)
Not really, virtually all these patterns involve tradeoffs that require understanding the data access patterns.
I don't want my compiler adding more padding than bare minimum to every struct. I don't want it transforming an AoS to SoA when I choose AoS to match data access patterns. And so on...
At best Go could add some local directives for compiling these optimizations, but these code changes are really minimal anyways. I would rather see the padding explicitly than some abstract directive.
Re:CPU Cache-Friendly Data Structures in Go: 10x Speed with ...
by danudey on Thursday October 09, 2025 @01:08PM
(#45530379)
I could imagine some kind of compiler declaration in C that would do something like specify break points - sort of like page breaks - for structs, or tell the compiler to automatically pad structs out so that components are on page boundaries, cache line boundaries, etc. Sort of "If we're not properly aligned, add whatever padding you think is best here".
I guess this is largely provided by std::hardware_destructive_interference_size in C++17, but I'm not sure if there are other language equivalents.
Re:CPU Cache-Friendly Data Structures in Go: 10x Speed with ...
by danudey on Friday October 10, 2025 @11:43AM
(#45540255)
The example I linked uses alignas, but the key is knowing what value to pass. std::hardware_destructive_interference_size tells you what the current/target hardware's correct align value is, which is the challenge.
Re:CPU Cache-Friendly Data Structures in Go: 10x Speed with ...
by jerf on Thursday October 09, 2025 @12:30PM
(#45529941)
Are you thinking of some sort of annotation the compiler could read and handle?
Because if a compiler starts automatically padding all my structures to put all of the members on their own cache line I'm going to be quite peeved. It would be easy for it to do, yes, but it would be wrong 99%+ of the time.
A far more trenchant complaint is that Go won't automatically sort struct members if necessary to shrink them and you have to apply a linter to get that at linting time if you want it.
Re:CPU Cache-Friendly Data Structures in Go: 10x Speed with ...
by danudey on Thursday October 09, 2025 @12:59PM
(#45530284)
I'm not sure if golang has the same fundamental issues in common use, but in e.g. C you don't want the compiler reordering your structs or adding arbitrary padding because that makes it incompatible with other in-memory representations - e.g. if you're using shared memory with another process that hasn't received the same optimizations, if you're loading raw data into memory/using mmap, etc.
Likewise, one of the examples is moving from an array of structs to a struct of arrays; that's a lot more complex of a code reorganization than you'd want a compiler doing.
It would be good to have a static analyzer that could suggest these changes, but, at least in many cases, you don't want them done automatically.
Re:CPU Cache-Friendly Data Structures in Go: 10x Speed with ...
by jandrewrogers on Thursday October 09, 2025 @02:40PM
(#45531459)
How would that even work? The layout of data structures are constrained by many invariants not visible to the compiler (see also: auto-vectorization). It would be more work and boilerplate to add sufficient annotations to a data structure to enable the compiler to safely modify the layout than just using the layout you want.
Re:CPU Cache-Friendly Data Structures in Go: 10x Speed with ...
by mappu on Friday October 10, 2025 @03:01AM
(#45536071)
Some languages like Odin, ISPC, and Jai all have annotations that can automatically transform AoS to SoA. A key benefit is you can easily experiment to see if this helps your application, without doing a major refactor.
Re:CPU Cache-Friendly Data Structures in Go: 10x Speed with ...
by tuetuopay on Thursday October 09, 2025 @11:06AM
(#45528752)
Overall great article, applicable to other languages too.
I'm curious about the Goroutine pinning though:
// Pin goroutine to specific CPU
func PinToCPU(cpuID int) {
runtime.LockOSThread()
// ...
tid := unix.Gettid()
unix.SchedSetaffinity(tid, &cpuSet)
}
The way I read this snippet is it pins the go runtime thread that happens to run this goroutine to a cpu, not the goroutine itself. Afaik a goroutine can move from one thread to another, decided by the go scheduler. This obviously has some merits, however without pinning the actual goroutine...
Re:CPU Cache-Friendly Data Structures in Go: 10x Speed with ...
by tuetuopay on Thursday October 09, 2025 @11:54AM
(#45529430)
Oooh, that's what is happening. I assumed it locked some structure about the thread while touching it, to prevent races with the runtime. That's what I get for not RTFM'ing (in fairness, why Lock and not Pin, when both of these words have pretty well defined meanings in programming?)
Re:CPU Cache-Friendly Data Structures in Go: 10x Speed with ...
by tapirl on Thursday October 09, 2025 @11:55AM
(#45529455)
Too magic to be true. Source code of the benchmarks?
At least, the False Sharing and AddVectors trick don't work on my computer. (I only benchmarked the two. The "Data-Oriented Design" trick is a joke to me, so I stopped benchmarking more.)
And I never heard of this following trick. Can anyone explain it?
// Force 64-byte alignment for cache lines
type AlignedBuffer struct {
_ [0]byte // Magic trick for alignment
data [1024]float64
}
Re:CPU Cache-Friendly Data Structures in Go: 10x Speed with ...
by kbolino on Thursday October 09, 2025 @12:33PM
(#45529975)
I can't find any claim anywhere else about the [0]byte trick, and in fact my own experiments in the playground show that it doesn't do anything.
If you embed an AlignedBuffer in another struct type, with smaller fields in front of it, it doesn't get 64-byte alignment.
If you directly allocate an AlignedBuffer (as a stack var or with new), it seems to end up page-aligned (the allocator probably has size classes) regardless of the presence of the [0]byte field.
Example output (w is a wrapper, w.b is the field in the wrapper, x is an allocated int32 to try to push the heap base forward, b is an allocated AlignedStruct):
Re:CPU Cache-Friendly Data Structures in Go: 10x Speed with ...
by nasretdinov on Thursday October 09, 2025 @12:31PM
(#45529960)
Not necessarily: you can go quite far with Go alone. It also makes it trivial to run "green threads" code, so if you need both (decent) performance and easy async code then Go still might be a good fit. Despite Go being pretty high level GC language on the surface it actually allows you to control stuff like struct layout, CPU affinity, etc, which typically matter more for performance than just a programming language of choice. There's a reason why e.g. VictoriaMetrics is still in Go even though they could've easily chosen any other language too
Re:CPU Cache-Friendly Data Structures in Go: 10x Speed with ...
by OutOfHere on Thursday October 09, 2025 @04:48PM
(#45532905)
Asynchronous is a programming style; it does NOT apply to Go. The goroutines run in parallel. Also, don't use complicated words when simple words will do.
Re:CPU Cache-Friendly Data Structures in Go: 10x Speed with ...
by aforwardslash on Thursday October 09, 2025 @05:27PM
(#45533285)
In golang, there is no guarantee that goroutines will run in parallel; also, it is quite common to use channels as means of synchronization of results, akin to common async programming patterns.
Re:CPU Cache-Friendly Data Structures in Go: 10x Speed with ...
by OutOfHere on Thursday October 09, 2025 @05:48PM
(#45533456)
> In golang, there is no guarantee that goroutines will run in parallel;
That's not specific to Go lang. Most of the constraints you're thinking of apply to all parallel programming languages. It goes with the territory. All parallel programming languages impose certain flavors of their management of parallelism.
Re:CPU Cache-Friendly Data Structures in Go: 10x Speed with ...
by nasretdinov on Friday October 10, 2025 @02:46AM
(#45535985)
Well, essentially Go doesn't have a separate async keyword because all goroutines run asynchronously under the hood. In the beginning the advised (and default) way of running Go code was GOMAXPROCS=1, essentially ensuring there is no actual parallelism, just asynchronous code. Since then, of course, around Go 1.5, the default switched to number of cores, making goroutines both async and parallel
Re:CPU Cache-Friendly Data Structures in Go: 10x Speed with ...
by pjmlp on Friday October 10, 2025 @02:53AM
(#45536024)
News for most folks, even writing C does not help, if neither of these advices are taken into account on how to lay out structures, nor algorithms are written with mechanical sympathy in mind.
Re:CPU Cache-Friendly Data Structures in Go: 10x Speed with ...
by loeg on Thursday October 09, 2025 @03:34PM
(#45532086)
You need to do the exact same kinds of thing in C/C++/Rust. I believe Rust struct layout is not guaranteed to match program order unless you use an annotation forcing it (repr(C)). (So to answer the question: it's great; as good as any other language for micromanaging layout.)
Re:CPU Cache-Friendly Data Structures in Go: 10x Speed with ...
by jasonthorsness on Friday October 10, 2025 @12:36PM
(#45540825)
huh TIL
"On modern Intel architectures, spatial prefetcher is pulling pairs of 64-byte cache lines at a time, so we pessimistically assume that cache lines are 128 bytes long."
Re:CPU Cache-Friendly Data Structures in Go: 10x Speed with ...
by gr4vityWall on Thursday October 09, 2025 @02:13PM
(#45531142)
Good article.
Regarding AoS vs SoA, I'm curious about the impact in JS engines. I believe it would be a significant compute performance difference in favor of SoA if you use typed arrays.
Re:CPU Cache-Friendly Data Structures in Go: 10x Speed with ...
by matheusmoreira on Thursday October 09, 2025 @02:25PM
(#45531272)
Structure of arrays makes a lot of sense, reminds me of how old video games worked under the hood. It seems very difficult to work with though. I'm so used to packing things into neat little objects. Maybe I just need to tough it out.
Re:CPU Cache-Friendly Data Structures in Go: 10x Speed with ...
by another_twist on Friday October 10, 2025 @01:03AM
(#45535508)
Good programmers worry about the algorithms. Great ones worry about the data structures and the relationships between them. If memory serves, it was Kernighan.
Re:CPU Cache-Friendly Data Structures in Go: 10x Speed with ...
by vacuity on Saturday October 11, 2025 @05:38PM
(#45552933)
"Bad programmers worry about the code. Good programmers worry about data structures and their relationships." - Linus Torvalds
"Show me your flowchart and conceal your tables, and I shall continue to be mystified. Show me your tables, and I won't usually need your flowcharts; they’ll be obvious." - Fred Brooks, The Mythical Man Month
And two threads with some further discussion I found while looking for these quotes:
Re:CPU Cache-Friendly Data Structures in Go: 10x Speed with ...
by loeg on Thursday October 09, 2025 @03:32PM
(#45532076)
This is a really dense whirlwind summary of some common performance pitfalls. It's a nice overview in a sort of terse way. The same optimizations / patterns apply in other languages as well.
Re:CPU Cache-Friendly Data Structures in Go: 10x Speed with ...
by furyofantares on Thursday October 09, 2025 @08:38PM
(#45534448)
I waited half a day to post this, I think we aren't supposed to question if articles are LLM written - but this one really triggered my LLM-radar, while also being very well received.
I'd love to know how much LLM was used to write this if any, and how much effort went into it as well (if it was LLM-assisted.)
Re:CPU Cache-Friendly Data Structures in Go: 10x Speed with ...
by furyofantares on Friday October 10, 2025 @12:57AM
(#45535486)
> Are people supposed to be obligated to post such a report nowadays?
No, typically when I ask questions it's optional.
> I enjoyed the article and found it really interesting, but seeing these types of comments always kind of puts a damper on it afterwards.
That is why I waited half a day, and until after there were lots of comments praising the article. Still, I'm sorry if it put a damper on it for you.
Also the whole reason I asked about the source is because I think the article has a lot of merit and so I am curious if it's because the author put a lot of work in (LLM-assisted or not.) Usually when I get that feeling it's followed by a realization I'm wasting my time on something the author didn't even read closely.
But I didn't get that this time, and I'd love more examples of LLMs being used (with effort, presumably) to produce something the author could take pride in.
Re:CPU Cache-Friendly Data Structures in Go: 10x Speed with ...
by furyofantares on Friday October 10, 2025 @04:47PM
(#45543614)
> But I didn't get that this time,
Actually, I take it back. I did think I was wasting my time when I noticed it was written by an LLM. But then I came back to HN an saw only praise and decided to wait a bit to see if people kept finding it useful before commenting.
I was somewhat excited by the prospect of this article being useful, but I've started to come around to my initial impression after another day. I don't really trust it.
Re:CPU Cache-Friendly Data Structures in Go: 10x Speed with ...
by darccio on Friday October 10, 2025 @07:03AM
(#45537450)
The structure reads as LLM written. I don't mind this unless the content is utterly wrong. I was actually learning about cache-friendly data structures and I'm really interested in that cache-friendly Robin Hood hashing but now I worry it's a hallucination.
Re:CPU Cache-Friendly Data Structures in Go: 10x Speed with ...
by furyofantares on Saturday October 11, 2025 @02:32PM
(#45551472)
FWIW, which may be not much - I had codex cli try to verify the results. On my M2 Macbook Air only the first example (False Sharing) did anything - a 23x speedup compared to the article's 6x speedup. All the others didn't produce any speedup at all.
Of course I didn't verify the results I got either - I'm not about to spend hours trying to figure out if this is just slop. But I think it is.
Looks like the LLM invented somewhat different test for it than the article had. I tried again and have this with the same data structure as in the article:
Re:CPU Cache-Friendly Data Structures in Go: 10x Speed with ...
by tapirl on Tuesday October 14, 2025 @09:55AM
(#45580099)
The "_ [0]byte" trick has no base in my knowledge. For the author's specified example, [1024]float64 will be always allocated on one whole page, aka, always 64-byte aligned.
For "Array of Structs vs Struct of Arrays", using slices as fields is a good idea. If the purpose is to make fields allocated on their respective memory block, just use pointers instead.
Re:CPU Cache-Friendly Data Structures in Go: 10x Speed with ...
by furyofantares on Tuesday October 14, 2025 @02:25PM
(#45583175)
> The "_ [0]byte" trick has no base in my knowledge. For the author's specified example, [1024]float64 will be always allocated on one whole page, aka, always 64-byte aligned.
You're right - I read the results I had wrong on that one. That one is slower, not faster, on both my M2 and on x86 machine.
Re:CPU Cache-Friendly Data Structures in Go: 10x Speed with ...
by tapirl on Wednesday October 15, 2025 @03:47AM
(#45589293)
My last comment has imprecision and misunderstanding.
> ... [1024]float64 will be always allocated on one whole page, aka, always 64-byte aligned.
if it is allocated on heap and at the start of allocated memory block.
> For "Array of Structs vs Struct of Arrays", using slices as fields is a good idea. If the purpose is to make fields allocated on their respective memory block, just use pointers instead.
I misunderstood it.
It is like row-based database vs. column-based database. Both ways have their respective advantages and disadvantages.
Re:CPU Cache-Friendly Data Structures in Go: 10x Speed with ...
by kbolino on Friday October 10, 2025 @01:22PM
(#45541420)
I don't see this mentioned anywhere else, but Go may start experimenting with rearranging struct fields at some point. The marker type structs.HostLayout has been added in Go 1.24 to indicate that you want the struct to follow the platform's layout rules (think of it like #[repr(C)] in Rust). This may become necessary to ensure the padding actually sits between the two falsely shared fields. You could combine it with the padding technique like this:
This is worth adding in Go race detector's mechanism to warn developer
That's fine for most deployments, since the vast majority of deployments will go to x86_64 or arm64 these days. But Go supports PowerPC, Sparc, RISCV, S390X... I don't know enough about them, but I wouldn't be surprised if they weren't all 64-byte CPU cache lines. I can understand how a language runtime that is designed for architecture independence has difficulty with that.
If you use these tricks to align everything to 64-byte boundaries you'll see those speedups on most common systems but lose them on e.g. Apple's ARM64 chips, and POWER7, 8, and 9 chips (128 byte cache line), s390x (256 byte cache line), etc. Having some way of doing the alignment dynamically based on the build target would be optimal.
https://cpufun.substack.com/i/32474663/notable-differences
As noted by the other comments, Apple's M-series chips seem to use a 128-byte cache line. ARM doesn't mandate that their licensees must use a pre-specified cache line size: 64 bytes just happens to be the consensus-arrived standard.
Was there any particular part that felt like it needed more explanation?
https://golangci-lint.run/docs/linters/
I don't want my compiler adding more padding than bare minimum to every struct. I don't want it transforming an AoS to SoA when I choose AoS to match data access patterns. And so on...
At best Go could add some local directives for compiling these optimizations, but these code changes are really minimal anyways. I would rather see the padding explicitly than some abstract directive.
I guess this is largely provided by std::hardware_destructive_interference_size in C++17, but I'm not sure if there are other language equivalents.
https://en.cppreference.com/w/cpp/thread/hardware_destructiv...
Because if a compiler starts automatically padding all my structures to put all of the members on their own cache line I'm going to be quite peeved. It would be easy for it to do, yes, but it would be wrong 99%+ of the time.
A far more trenchant complaint is that Go won't automatically sort struct members if necessary to shrink them and you have to apply a linter to get that at linting time if you want it.
Likewise, one of the examples is moving from an array of structs to a struct of arrays; that's a lot more complex of a code reorganization than you'd want a compiler doing.
It would be good to have a static analyzer that could suggest these changes, but, at least in many cases, you don't want them done automatically.
In https://github.com/golang/go/issues/64926 it was a bridge-too-far for the Go developers (fair enough) but maybe it could still happen one day.
I'm curious about the Goroutine pinning though:
The way I read this snippet is it pins the go runtime thread that happens to run this goroutine to a cpu, not the goroutine itself. Afaik a goroutine can move from one thread to another, decided by the go scheduler. This obviously has some merits, however without pinning the actual goroutine...Thank you
I got hit by this. In a trading algorithm backtest, I shared a struct pointer between threads that changed different members of the same struct.
Once I split this struct in 2, one per core, I got almost 10x speedup.
At least, the False Sharing and AddVectors trick don't work on my computer. (I only benchmarked the two. The "Data-Oriented Design" trick is a joke to me, so I stopped benchmarking more.)
And I never heard of this following trick. Can anyone explain it?
If you embed an AlignedBuffer in another struct type, with smaller fields in front of it, it doesn't get 64-byte alignment.
If you directly allocate an AlignedBuffer (as a stack var or with new), it seems to end up page-aligned (the allocator probably has size classes) regardless of the presence of the [0]byte field.
https://go.dev/play/p/Ok7fFk3uhDn
Example output (w is a wrapper, w.b is the field in the wrapper, x is an allocated int32 to try to push the heap base forward, b is an allocated AlignedStruct):
Take out the [0]byte field and the results look similar.That's not specific to Go lang. Most of the constraints you're thinking of apply to all parallel programming languages. It goes with the territory. All parallel programming languages impose certain flavors of their management of parallelism.
> The goroutines run in parallel. Also, don't use complicated words when simple words will do.
That’s not called for, especially since you’re wrong.
"On modern Intel architectures, spatial prefetcher is pulling pairs of 64-byte cache lines at a time, so we pessimistically assume that cache lines are 128 bytes long."
Regarding AoS vs SoA, I'm curious about the impact in JS engines. I believe it would be a significant compute performance difference in favor of SoA if you use typed arrays.
"Show me your flowchart and conceal your tables, and I shall continue to be mystified. Show me your tables, and I won't usually need your flowcharts; they’ll be obvious." - Fred Brooks, The Mythical Man Month
And two threads with some further discussion I found while looking for these quotes:
https://news.ycombinator.com/item?id=17580598
https://news.ycombinator.com/item?id=10293795
I'd love to know how much LLM was used to write this if any, and how much effort went into it as well (if it was LLM-assisted.)
Are people supposed to be obligated to post such a report nowadays?
I enjoyed the article and found it really interesting, but seeing these types of comments always kind of puts a damper on it afterwards.
No, typically when I ask questions it's optional.
> I enjoyed the article and found it really interesting, but seeing these types of comments always kind of puts a damper on it afterwards.
That is why I waited half a day, and until after there were lots of comments praising the article. Still, I'm sorry if it put a damper on it for you.
Also the whole reason I asked about the source is because I think the article has a lot of merit and so I am curious if it's because the author put a lot of work in (LLM-assisted or not.) Usually when I get that feeling it's followed by a realization I'm wasting my time on something the author didn't even read closely.
But I didn't get that this time, and I'd love more examples of LLMs being used (with effort, presumably) to produce something the author could take pride in.
Actually, I take it back. I did think I was wasting my time when I noticed it was written by an LLM. But then I came back to HN an saw only praise and decided to wait a bit to see if people kept finding it useful before commenting.
I was somewhat excited by the prospect of this article being useful, but I've started to come around to my initial impression after another day. I don't really trust it.
Interesting and surprisingly, there are numerous praising comments here.
Of course I didn't verify the results I got either - I'm not about to spend hours trying to figure out if this is just slop. But I think it is.
https://pastebin.com/v9tczpus
Looks like the LLM invented somewhat different test for it than the article had. I tried again and have this with the same data structure as in the article:
https://pastebin.com/SDdcchZG
That gave similar results to the article.
All the other tests still give little-to-no speedup on my machine.
TIL.
For "Array of Structs vs Struct of Arrays", using slices as fields is a good idea. If the purpose is to make fields allocated on their respective memory block, just use pointers instead.
You're right - I read the results I had wrong on that one. That one is slower, not faster, on both my M2 and on x86 machine.
> ... [1024]float64 will be always allocated on one whole page, aka, always 64-byte aligned.
if it is allocated on heap and at the start of allocated memory block.
> For "Array of Structs vs Struct of Arrays", using slices as fields is a good idea. If the purpose is to make fields allocated on their respective memory block, just use pointers instead.
I misunderstood it.
It is like row-based database vs. column-based database. Both ways have their respective advantages and disadvantages.