Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

I have done a fair amount of low level performance optimization with Opus 5 and its reasoning is still very poor. Like why is CRC so slow and going through loops until I ask it if is using hardware instructions and it tells me it is using its own hand coded implementation poor. Reasoning about l1/l2/l3 cache hit ratios and their implications basically throwing darts at the wall, in the wrong room. If you give it a benchmark feedback loop then it might get there eventually but still massive alpha for low level systems engineers who instinctively know how this stuff works and can now automate 99% of the grind.
 help



> Reasoning about l1/l2/l3 cache hit ratios and their implications basically throwing darts at the wall, in the wrong room. If you give it a benchmark feedback loop then it might get there eventually but still massive alpha for low level systems engineers who instinctively know how this stuff works and can now automate 99% of the grind.

I suspect a lot of the training set for this sort of thing is people online speculating about cache performance incorrectly.


Speaking as somebody who is a performance geek, my knowledge came from relentless experimenting over the years (starting in the 8 bit era). Beyond the basics, I haven’t seen much on high-performance engineering online. To learn, you need to do the hard yards and I think performance tweaking becomes almost instinctive rather than something driven by a hard set of rules.

At a low enough level, every performance tweak becomes unique and bespoke.

Of course, you could find people online talking about how to write high-performance code, but beyond a few basic techniques, their advice may not work for you — nobody can write a generalist article about performance engineering that will definitely solve the problem you have right now.

Arguably, there are fewer patterns for an LLM to infer as highly optimised code tends to become more and more opaque in the search for a nanosecond here or there.


I don't disagree, but IMO, a lot of code doesn't get to the point where those very low level techniques drive performance. Like, yes, if you are doing some heavy floating point math then that's where you end up needing it. However, in a lot of code finding hot paths and often simply switching out a O(n^2) for an O(n log n) or faster.

Getting and using tools to find hotpaths is generally the most important performance tweaking skill.


I mean, sure, but it really does depend on what you're doing. If you're working on a library with collection-types and you want to make each iteration as fast as possible, then roll up your sleeves. If you're writing a compiler and you want your language's source-code to finish compiling this week, roll up the sleeves. If you're working on a game-engine and you want to draw more than everyone else, roll up the sleeves...

There are plenty of real-world reasons why you'd want to get knee deep in this stuff. I wasn't suggesting not using tools (I've literally spent the day buried in JetBrains' memory and tracing tools!), but those tools can only tell you what is happening now, not what to do to improve it.

Profiling is, of course, essential. But performance tweaking can be quite a laborious process: if you're judging things by big-O notation, then that's a different level above the real low-level tweaking (imho of course). Picking the correct data-structures is all in the 101 of performance engineering. That's in the literature. But it's all too basic and simplistic. Most performance minded engineers wouldn't need a profiling tool to know which data-structure to use.

At the smallest level there's a lot of mental theory building and experimentation as you try out different approaches, which is where the instinct and intuition starts to build. I never see any of that in discussions about performance engineering.


> but those tools can only tell you what is happening now, not what to do to improve it.

They tell you what's happening now, but they also tell you if what you've done has had a positive impact.

> If you're judging things by big-O notation, then that's a different level above the real low-level tweaking (imho of course).

I completely agree. My point isn't that Big-Oh is low level, but rather that Big-Oh is often enough for most programming problems. Even in some of your examples like a compiler, game engine, or collection library, the big oh matters and if it's wrong, that can be a lot more important than shaving 0.1% on writing a function in a low level fashion. Big-Oh is gotten wrong a surprising amount of time even though it's 101 level stuff.

> At the smallest level there's a lot of theory building and experimentation as you try out different approaches, which is where the instinct and intuition starts to build. I never see any of that in discussions about performance engineering.

Oh because people get these things wrong all the time. That's why performance engineering stresses that you test, test, test and know what your testing and know why your testing could be wrong or corrupted. You should not trust your intuition because things change and it isn't always correct.

A good example of how easy it is to get measuring wrong. Imagine you start tweaking a function and you measure that your application became 1% faster. Was it the work you did on that function? Surprisingly, not always (at least not directly). Sometimes, it's the case that when you work on a function you re-align other functions as the machine code has to go it memory. It's possible that an undiscovered misaligned while loops was actually causing a large portion of your performance spill and by tweaking the function here, you aligned the while loop (or maybe a few of them). And, importantly, a new change somewhere else might re-unalign that same while loop.

You walk away thinking you've learn some low level lesson when in actuality your bit twiddling simply accidentally fixed something somewhere else.

This is why measuring is so important but also good measuring is even more important.


> I completely agree. My point isn't that Big-Oh is low level, but rather that Big-Oh is often enough for most programming problems.

For what I work on, the hidden constant is often more important than big O. For example, a hash map has better complexity than just searching through a vector. But if the vector is small enough it will best the hash nap for actual time. Just plain searching until you find the element will even beat binary search on a sorted vector for small enough vectors. The reasons are complex, to do with cache, prefetch, branch prediction and also just how many instructions your tight inner loop has. (And the specific reasons will vary between desktop class CPUs and microcontrollers. But both exhibit this pattern.)

You could argue that at that point why bother optimising at all (there aren't a lot of elements in the collection after all). But there are two distinct cases I have come across over the years where it still matters (and for what I work with, they represent the common cases):

* You need to look up in a small collection a lot (either lots of lookups into a few small collections or a few lookups each into lots of different small collections, I have seen both cases).

* Hard realtime code where predictable latency matters. Hashmap has a bad worst case, binary trees and binary searching has badly predictable memory access patterns. And in this case the collections are usually small anyway (there are only so many actuators and sensors your equipment has, and/or the embedded microcontroller doesn't have a lot of memory anyway).


> But if the vector is small enough it will best the hash nap for actual time.

This will all depend on the size and type of object stored in a vector.

If you have a relatively small and flat struct that you are storing, then sure that will likely win. But if you are working with a collection of pointers, then the hash map will (almost) always win.

> Hard realtime code where predictable latency matters. Hashmap has a bad worst case

The hash map worst case is a linear search. It will be a lookup + the search. This also depends on the implementation. You could, for example, use a robinhood hash map which trades insertion times for lookup times.

A bad hashmap implementation will store collisions in linked lists. A better one will try and put them in a b-tree. An even better one will use a vector for collisions. And the most cache friendly version stores everything in the table and does probing on collisions.

There are specific cases where vectors are better, but those are the exception and not the rule in my experience.


It all depends. I was thinking mainly of small keys that are at most a machine word. But yes I conceded that for large keys (extremely unusual for what I do, thus I didn't think of that case before) a hashmap wins.

Also, while on a desktop class CPU any pointer chasing you do tends to dominate, that is not the case on small and medium microcontrollers (which I work with a lot). They have very short pipelines and their memory is all internal SRAM most of the time. Here classic cycle counting is still king, so you need to ask yourself if it is quicker to compare the key than to calculate the hash and compare hashes.

Code size in general matters a lot on embedded. Even a simple hashmap will always have more code than a simple vector. And a state of the art implementation with probing and tombstones (such as swisstable in C++ and hashbrown in Rust) will be way too large to be usable.


> Even in some of your examples like a compiler, game engine, or collection library, the big oh matters

Sure, but for example - today - I am literally building and optimising high-performance collections for my open-source library. Big-O is irrelevant, because I have built pretty much all of the fundamental collection types, what I care about is lower than that: what happens when enumerating any one of those collection types. Big-O tells you the scale of the problem, but not the per-element cost, which is still important when you're building core data-structures.

I am concerned about cache-friendly memory-layouts, how to do collection compositions without unnecessary memory allocations, keeping enough guards in place to make the types safe whilst removing as many branches as possible, reducing memory copying as much as possible, and catching stupid shit the compiler or JIT does and try to work around it. Literally what happens per-instruction, per-iteration, not how to pick a big-O based data-structure: trying to make all data-structures as fast as possible.

Anyway, we seem to be talking past each other. You're talking about the basics, I'm talking about the original source of this thread which was that (apparently) LLMs are surprisingly bad at optimisation. Which, I am trying to highlight becomes almost voodoo at a low-enough level and that highly-optimised code looks progressively more strange and opaque (in the hunt for a few nanoseconds here and there), which for an LLM wouldn't look statistically significant. I think the basics of data-structure choice should be easily within the realms of an LLM's current capabilities.


> I think the basics of data-structure choice should be easily within the realms of an LLM's current capabilities.

Surprisingly, it isn't. I catch the output of LLMs breaking these rules all the time. Just as it is pretty common in general programmer code.

The greatest sin I often find isn't necessarily Big-Oh related but rather multiple traversal problems. Much like regular programmers, LLMs love to do multiple passes over the same list to extract data. For example

    let cats = pets.filter((p)->p.isCat());
    let dogs = pets.filter((p)->p.isDog());
A lot of programmers are oblivious to that sort of performance issue. It comes up a lot.

I would think it's that the kinds of places which value this kind of knowledge often have major disincentive to share it. I'm thinking of HFT firms as one example.

Well if any labs want to scrape my knowledge of such things into model weights or faster training runs, I do consulting at movsoft.io :-)

It really is [as simple](https://github.com/ratatui/ratatui/pull/281#issuecomment-160...) as the Molly rocket video about terminals makes it seem; I am sure the models will catch up eventually. They have to overcome a lot of bad training data in most languages, but I find in Rust they usually do ok the first time.


Around six years ago, I worked for a switch manufacturer and we had insanely optimized networking code that will never see the public light of day.

I ran into the exact same wall when doing some SIMD + OpenMP optimisations.

Me: "Parallelise this loop without changing the results."

AI: "I can't do that, because of floating point accumulation."

Me: "So spin out a temporary fixed-length array, accumulate into that thread-locally, and then do an ordered summation of that at the end."

AI: "Oh, you're right!"

It's a fantastic tool for automating the implementation grind, but you still have to hand-feed it the strategy. I guess it will get there at some point.


>> I guess it will get there at some point.

Each tool in any domain "will get there at some point"


Can you give it the valgrind suite to loop over? I have yet to try that with AI, but maybe the cachegrind tool is enough to help it.

A new meta is emerging of having high taste + infinite compute with little domain expertise. One recent example is Jarred Sumner taking a crack at the Riemann Hypothesis

> Throughout this process, Jarred's input was mostly limited to sending Claude messages of encouragement (mostly variants of “keep going” or “believe in yourself”).

How much experience can you replace with infinite compute remains to be seen I guess.

https://news.ycombinator.com/item?id=49247070


I see the same thing, except I was working on high level performance reasoning. Whenever a piece of code has multiple steps that require multiple algorithms to work, AI almost always fails to guess which step is the slowest and what causes that step to be slow. Even Fable makes wrong guesses. You definitely need to give them a benchmark feedback loop.

Are there actually any humans who can reason about things like cache performance from first principles? I know there are some people who think they can, and I suspect they're fooling themselves. The one iron principle of micro-optimization at the level of cache hits is "measure, measure, measure", you just cannot think your way to the right answer on the first try. Processors and instruction sets today are too complicated, and tips that worked on one generation might be neutral or worse on the very next revision, making all the cargo cult knowledge passed around on this topic at best useless. I'll echo one of the sibling commenters here and say that LLMs probably bullshit their way to answers on questions like this because that's what humans online do as well.

If you give an LLM a proper testing harness and feedback loop to actually generate hypotheses, test them and revise them, I suspect it will do much better.


Yes, people can demonstrably do this with high reliability.

Some humans carry detailed models of CPU microarchitectures in their heads, against which they can design code from first principles that will be nearly ideal on the first try. It is repeatable and verifiable. The best people can accurately predict the measured performance before writing a line of code.

Measurement is useful in cases where the model of software and hardware interaction is materially incomplete. In most cases this is because the people writing the software have insufficient understanding of the hardware. Having a limited understanding of the hardware is a choice.

It would be surprising if this wasn't possible.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: