Decreasing the number of memory accesses
johnnysswlab.com
johnnysswlab.com
* Exploiting cache locality (related data in same cache line, operations hit the data before it leaves cache, etc.)
* Exploiting cache prefetching (understanding how to lay out your data and operations so that CPU prefetches data from memory into cache BEFORE it is needed)
* Ensuring high density of data being accessed (make sure you only access what you need and the cache lines you access contain only data you need and not random other things that are not involved in the operation)
... at least in my own experience.
These can be so powerful that may make a dumb algorithm perform like an absolute superstar.
A lot of developers overthink algorithms. They try to create something that is super efficient on paper forgetting that the algorithm is not being executed on paper but by a real CPU. Sometimes it is better to sacrifice "paper efficiency" and just ensure cache locality, prefetching, data density and the result is speedup so large it more than covers for a little bit of inefficiency.
No, it is not. For example, you can have an application that is doing a lot of memory accesses, but the working set is controlled to fit the cache except for couple of things that need to be fetched into the cache but you make sure those things fit predictable pattern that the CPU prefetcher can recognise and supply in advance of you needing it.
Now, having less memory accesses usually helps, but it is kinda newb way of looking at the problem. What if you have to make a lot of accesses because your application is memory heavy (say it is a database)? Does it mean there is nothing you can do to improve it? Of course there are things you can do to improve it -- but you need a bit more in depth knowledge.
That's an over-generalization that is only valid for small input sizes. Consider:
- an O(N*log N) heap-sort implemented by a decent programmer in idiomatic high-level code.
- a dumb O(N^2) bubble-sort coded by an absolute assembly optimization wizard.
Your O(N^2) sort will probably be much faster for small input sizes, but it won't take a large input size for it to become dog-crap slow. You can improve constant factors all day, but that won't obviate the need for doing millions or billions of comparisons.
You can only take a dumb algorithm so far before it breaks down completely in practice.
This is not a theoretical thing either - in unoptimized codebases, I've often seen people finding low-hanging fruit by looking for inadvertent O(N^2) or even O(N^3) behaviour (usually it's not obvious nested loops, but looping hidden by indirection i.e. function calls) and replacing it with an O(N*log N) or O(N) alternative.
> That's an over-generalization
I think this is exactly the problem with these types of comparisons. As the original commenter also hinted: You can not generalize best choice about the algorithm.
As I mentioned, the idea is to be open to alternative ways of thinking.
Performance is incredibly difficult or impossible to generalise. I have many times worked on a project where people said confidently it is not possible to speed something up and then I speeded it up by multiple orders of magnitude.
I worked for one of largest banks in the world and we had a batch task on MongoDB that took 12 hours to complete. The previous team, before I joined, took it down to 4 hours but then said it is not possible to improve it anymore. The consultants from MongoDB confirmed it. This was a huge problem because there was a delay of 4 hours between end of trading day and the report being finished which was unacceptable.
I took it down to ~15 seconds. Without changing data structure or the database or the application technology. The only significant thing I did is I made both the data suppliers to stream it to our app and the consumers to stream the results during the data. So the data was being processed during trading day and partial results were already being pushed to the client. The only thing that needed to be done was to sync between the sources and the clients and it took about 15 s to do it (and I could probably take it even further but it was not worth the effort).
So you see, it is all about being open minded.
bubble sort is never the way to go
generally in the case you're describing, where you have a few thousand elements that need to stay sorted in the face of insertions, a b-tree would be a better choice, or failing that, merge in a batch of updates whenever the update file gets big enough
either of these can beat bubble sort or insertion sort in this situation by more than an order of magnitude
I think most people here will interpret the word "can" in the sentence you're quoting as a caveat emptor; and I would add the same applies to TFA. Loop fusion can etc. but measure if it actually does in your exact context.
> You can improve constant factors all day, but that won't obviate the need for doing millions comparisons. > You can only take a dumb algorithm so far before it breaks down completely in practice.
If the dumb algorithm relies entirely on arithmetic while the better algorithm has conditional jumps, branch prediction misses can play against it for unexpectedly large input sizes.
Micro-optimizations may fall short quickly, but big-O() approved algorithms don't always win either, in practice.
It seems that today, very few compilers are willing/able to mess with data structures and rearrange them.
I'd like to see compilers that can at a minimum rearrange arrays of structures into structures of arrays depending on likely data access patterns of the different members of the structure.
I'd also like to see compilers 'factoring out' things like vtables or structure members when they identify that they are constants or never read/written.
Instead these should be explicit programming language features (like defining a regular struct but then slice this struct into an SoA).
The rest should be handled by debugging and profiling tools which need to be much better integrated into the programming workflow than what is common today.
(I’m sympathetic to the concern of things breaking in unexpected ways.)
For instance Zig has a MultiArrayList container in its stdlib which slices its generic item type into one array for each struct item. This isn't a special compiler feature, but a regular usage of Zig's comptime stuff:
https://github.com/ziglang/zig/blob/master/lib/std/multi_arr...
On the other hand, giving up that control and designing languages where more automatic optimizations are possible also seems really great. A lot of programming shouldn’t be concerned with low level perf anymore.
I don't blame the compiler guys. They have to deal with tortuously complex real-world code. Sight unseen. And that code may only work correctly because of some unwritten assumptions, side-effects, and undefined behaviors, which no good program would ever rely upon.
Stop trying to be as lazy as possible in such a bad way. Making compilers make up for a lack of programming ability is a mistake.
Our intuitions are wrong. In reality it takes a shockingly small problem to break these optimizations and make them do something in violation of the semantics of the language, and as a result, the compiler can't perform the optimization even if it is smart enough to try. Many, many, many compilers have tried. It is not a new idea, it is one that has been tried and failed over and over again.
This is one of the reasons why the functional programming crew advocate so hard for simpler semantics like pervasively immutable variables. The idea is that if the program's semantics promise less, the optimizations ought to be a lot easier. And at least at the surface level this is arguably true; Haskell does quite significant map (rather than "loop") fusion successfully. However, to date, the "sufficiently smart compiler" has not manifested to the extent that we'd like, the one that turns functional code into 2x - 3x faster code than imperative code. Generally the real-world smart compilers are straining just to keep up with C, as long as you write in a particular subset of the language.
I have quoted the term "sufficiently smart compiler" because if you want to dig further in, it is a good search term.
Many of our intuitions about our code are similarly broken. There was a lot of hope for exploiting "implicit paralellization" in our codebases; surely, our brains thought, there's just a ton of things in every code base that can be automatically turned into parallel code. That turned out to be false. There was a paper that took some representative code bases and extracted out the theoretical maximum parallelization that could even be done by any conceivable compiler optimization by analyzing the data dependencies themselves and it turned out to be shockingly small, thus effectively killing the idea. This story has been repeated a lot; another search term you can play with is "supercompiler", which amounts to "compilers are pretty good at optimizations in the small, surely our code bases are rife with opportunities to optimize at much higher levels across packages or the entire code base?" to which the answer to date has amounted to "No, not really."
In my opinion, the only hope for any of these ideas is someone sitting down and designing a language from the very get-go intended to work with these ideas. However, given the amount of experience necessary to do that and the subtlety of the task, I'm not sure that I'm not specifying a super-human degree of work by saying that.
void get_ids_adult_users(const std::vector<user_t>& allUsers, std::vector<int>& adultUsers)
{
for (const user_t& user: allUsers) {
if (user.age >= 18) {
adultUsers.push_back(user.age);
}
}
}
probably after calling reserve() on the adultUsers vector to avoid resizing.Curious if this kind of loop fusion would be something you get for free in purely functional language? That is, if you were to write a program (say in Haskell) for min, max plus that same code combined into a single program, would you indeed empirically see double the speed?
it's how we managed to get away with doing anything at all when our PCs had like 16-32 MEGABYTES of ram in them - or less, those are generous amounts
it's why those computers can still play video
Meanwhile, new computers that are thousands or tens of thousands of times faster cannot play the same video.
> This series of five manuals describes everything you need to know about optimizing code for x86 and x86-64 family microprocessors, including optimization advices for C++ and assembly language, details about the microarchitecture and instruction timings of most Intel, AMD and VIA processors, and details about different compilers and calling conventions.
> Operating systems covered: DOS, Windows, Linux, BSD, Mac OS X Intel based, 32 and 64 bits.
https://github.com/wichtounet/gcc-loop-fusion/commits/master...
Get an ML model to take a stab at optimizing, have a solid system for validating that two pieces of code are equivalent according to the C abstract machine, throw away the ML-suggested optimizations which change the semantics of the code, keep the ones which keep semantics the same.
More realistically you can use an AI to generate vaguely realistic PGO data (profile guidance is where the real speed comes from). This has already been done at least by researchers.
That's not a bad idea, but PGO is pretty limited. I'm thinking about things like rewriting a loop to use vector instructions; something which compilers are absolutely terrible at but ChatGPT is actually pretty good at.
Enabling PGO when building the D compiler made it almost 50% faster on some parts of the test suite (and fairly reasonable big files at that) - speedup elsewhere was not as dramatic but still a tidy win.
What ideas do you have for doing that in anything under exponential time?
And maybe you can't make a polynomial time verifier which works in the general case, but maybe you can make a useful one which handles a lot of cases.
What we need instead is "always on" profiling/debugging/inspection tools so that I can see directly and immediately the performance impact of my code changes while I'm typing the code (down to the compiler output as disassembly, annotated with performance warnings like predicted cache misses). Basically the same sort of evolution which gave us warning and error squiggles while typing, but for performance (think godbolt.org, but in realtime and with hot code reloading).
Unless the inner loop is something long or complicated (in which case for reading & maintainability it should maybe be broken down into functions), I generally find the fused version easier for both.
For readability: it is immediately obvious that everything is working on exactly the same data, and the same range within that data, and that one section is not expected to have side effects that affect the running of the subsequent ones.
For maintainability: there are less possibilities of an edit to one part that should affect all being missed elsewhere.