Mispredicted branches can multiply your running times
lemire.me
lemire.me
looks like over-engineering of (bits + 7) >> 3
Why not use: (x > 0) - (x < 0)
What if we had a modern, pipelined CPU architecture with no branch predictor, which instead unconditionally executed the X instructions immediately following every branch before (possibly) proceeding with the branch's target instruction?
Would compilers and programmers be able to find most of the efficiencies that we currently rely on the branch predictor to find? What would be the common cases where it would be hard to do that?
How much circuitry would we save by leaving out the predictor? Enough to allow a measurable speedup in the CPU clock speed?
Notably, branching on a flag f on the GPU was so slow that most of the time it was faster to (manually) compute both branches b1 and b2 and then calculate the result as r = f * b1 + (1-f) * b2 (where f is either 0 or 1).
You trade it off with increased code size which might spill the cache but for small tight loops not exceeding the cache line/ size it would still be a good win.
Two issues:
- It is not easy for the compiler to always fill up a single delay slot. Filling dozen of them (as required for a deeply pipelined modern processor) would be significantly harder.
- The number of delay slots would depend on the depth of the cpu pipeline. If you do not want to expose microarchitectural details in your ISA, you need either JIT or install time specialization. edit: or stick with a potentially suboptimal number.
There's no way you could cover all variable latency scenarios with fixed delay slots, unless you have a highly specific scenario like a GPU where you control all the internals.
sub eax, 1
(several non-flags-touching instructions)
jmpne loop_start
I would expect that a branch predictor wouldn't be needed as the jump only needs the flags register to figure out where the branch is going to go.This is not specific of OoO cpus. In order cpus work the same way, assuming they have a predictor of course.
edit: to be more precise, jumps are not not resolved at the fetch stage, but in one of the early stages, in fact there is often a bubble for taken branches as the fetcher will fetch the next instruction in the stream by default.
Considering the complex predictors on current PCs, we would save quite a lot of circuitry. But that circuitry is there because it is the most effective place to increase the CPU speed, if you used it for something else, speed would go down, not up (but power consumption would improve).
Also, actual clock speed isn't really relevant and has a complex relation to CPU speed. That circuitry isn't affecting clock speed, so it wouldn't change.
Power consumption per second might go down, but overall may go up, depending on how many more seconds the computation takes.
The rule of thumb you learned is useful for optimizing PC software, but has no place in CPU architecture discussions.
Also, "usually" leaves a lot of important cases out.
Warning! This is just a stone's throw from the kind of thing that got us Spectre, so keep in mind this sort of optimization can occasionally come back to bite you…
Pretty much any thing on the market with the compute power of a cellphone, is going to have a feature like this too
Why not? VC++ emits cmov quite often, for operator ? and similar code.
https://stackoverflow.com/questions/11227809/why-is-processi...
No kidding!
But the article does call attention to a very important technique. It merits serious thought over all the ways it can be applied, that may not look much like this one, on the surface.
Well, that also means that some loops are fully unrolled :-)
for(i=0;i<howmany;i++) {
out[index] = random();
}
Ofcourse variable length loops would still have the branches.Compilers prefer to order the instructions so they take two or more cycles.
If the last number generated is even, it will still appear in the result set in the new code, and not in the old code.
Here, trying to access out[index-1] would error.
Yes, you could then guard against that of course, but then that's even more code to maintain.
If this code is a critical hot-path then sure, micro-optimizations can make sense but doing so without over-commenting and a rigorous test suite to catch introduced bugs is a recipe for disaster.
while (howmany != 0) {
val = random();
if( val is odd) {
out[index] = val;
index += 1;
}
howmany--;
}
vs while (howmany != 0) {
val = random();
out[index] = val;
index += (val bitand 1);
howmany--;
}
Both of these store a list of odd numbers in out[], with "index" containing the resulting count of how many numbers are in out[]. Both will have an "index" (count) value of 0 if all inputs were even, and neither attempts to access out[index-1].> count of how many numbers are in out[]
is not true, in the latter case it's a count of how many numbers you want to be in out[].
Consider what happens if howmany is 1 and it generates a single even number. In the original you have an empty array and index 0, in the newer one you have an array e.g. [2] and an index 0.
Yes, you can solve this by 'manually' only returning the first "index" elements but it's just asking for trouble and a source of bugs as seen in your attempt to describe it have a difference between the reality and your description.
You might not consider that to matter, but the point I'm trying to make isn't just nitpicking, it's that there are certain expected behaviours from code, and an array which is "all odd numbers, except maybe all odd but one last even number" is bound to lead to broken expectations.
> is not true, in the latter case it's a count of how many numbers you want to be in out[].
I'm failing to see how "index" is the count of how many numbers you want to be in out[] rather than the count of how many numbers actually ARE in out[], or why this would be different between code examples.
> Consider what happens if howmany is 1 and it generates a single even number. In the original you have an empty array and index 0, in the newer one you have an array e.g. [2] and an index 0.
Yes, that's the point, as was explicitly stated in the article.
> Yes, you can solve this by 'manually' only returning the first "index" elements but it's just asking for trouble and a source of bugs as seen in your attempt to describe it have a difference between the reality and your description.
What is there to solve? The end result in both cases is that variable "index" is 0, because 0 of the values in out[] are valid.
> You might not consider that to matter, but the point I'm trying to make isn't just nitpicking, it's that there are certain expected behaviours from code, and an array which is "all odd numbers, except maybe all odd but one last even number" is bound to lead to broken expectations.
Yes, and the expectation is that "index" tells you where the first garbage element is in the array (in both examples). You're either going to have an array with all garbage (index = 0), all garbage except the first element (index = 1), all garbage except the first and second elements (index = 2), and so on. What actual values are in a garbage index (even or odd or 0 or 1 or 2 or Martin Luther's birth year), are irrelevant because you're not going to read from those indices.
howmany -= (val bitand 1);
But that might complicate the benchmark.Coincidentally, this was the subject of the first Stack Overflow question Bjarne addressed in an interview the other day:
https://stackoverflow.com/questions/11227809/why-is-processi...
If only the function had a unit test to catch such bugs
It is important to understand your platform all the way down to the CPU, including things like branch prediction and caches if you want to have performant software.
Software has been getting slower more rapidly than hardware has been getting faster for nearly a decade, and the free performance gains that software people have taken advantage of when better hardware is made available are just about exhausted.
It's time to learn your platforms, software people.
[later addition] This kind of thing is also one of the reasons that teaching OOP principles as they are taught today is so bad for software performance. Modeling object relationships to match the real world will, in every non-toy program, produce object structures that are actively unfriendly to cache efficiency, and will therefore produce software which performs very poorly when compared to software that was written with the hardware platform in mind.
Surprisingly, In JavaScript the technique described in the article is quite efficient on Firefox whereas the gain is almost negligible on Chrome.
https://jsperf.com/mispredicted-branches
Edit: Jsperf seeems to be down. Here are the 2 snippets of code I tested:
// Unoptimized
let howmany = 5000000;
let index = 0;
const out = [];
while (howmany) {
const val = Math.floor(Number.MAX_SAFE_INTEGER * Math.random());
if (val % 2) {
out[index] = val;
index += 1;
}
howmany--;
}
// Optimized for branch prediction
let howmany = 5000000;
let index = 0;
const out = [];
while (howmany) {
const val = Math.floor(Number.MAX_SAFE_INTEGER * Math.random());
out[index] = val;
index += (val & 1);
howmany--;
}This single person is also the only JS developer I know who has any clue how to actually improve performance in existing code.
Also it ran 30x faster in chrome... (~200ms vs ~6000ms)
This is a shame because not only are there lots of developers who write JS that have low-level backgrounds there are also a lot who haven't and are still interested. It seems rather snobby to jump in and write off a bunch of people because of the language they use on the assumption that they use the language without being aware of the implications.
On OOP versus more cache friendly approaches and branch prediction this talk from CPPCon 2019 is great: https://www.youtube.com/watch?v=HG6c4Kwbv4I
The interesting result is that although the DoD approach is eventually faster a branch misprediction causes havoc until uncovered.
Programmers make the web is a true statement of fact it’s just that it’s a subset of programmers that do it.
The OPs statement is a lazy opinion about JavaScript developers in general unambiguously applied. So I welcome their later amendement.
Most people aren't able to outsmart their compiler optimizers.
One reason mainframes still use bytecode as executable format is that it allows for AOT compiling the world after and OS or hardware upgrade.
And there are solutions for doing that to plain native opcodes as well.
Finally there is the whole issue that many micro-opmizations, while fun to implement, seldom contribute for visible optimizations of the business case at hand.
Speaking of which, better catch up on the C++ interpreters and JIT related talks from CppCon 2019.
The tone of my previous remark is that optimization algorithms are orthogonal to programming languages, an language agnostic backend optimizer working on AST and SSA passes doesn't care what the input language was all about.
Let's not drastically increase job requirements for no good reason.
>It's time to learn your platforms, software people.
Many of these platforms have undocumented CPU instructions, so until you get a full accounting of that, what's the point? You can't learn the platform fully if they keep that a secret.
Secondly, we've had CPU-level issues like spectre and meltdown introduced that affected performance in some cases. We can't even trust the platform makers to get it right!
Well, I would say that it's a very good reason, and that learning about branch prediction and caches is not a "drastic" step by any means.
Is there any software that you write whose users would not be made happier if the software performed better? Any at all?
> Many of these platforms have undocumented CPU instructions
You don't need to know the secrets of a platform to understand branch prediction and caches, or to use that knowledge to produce software that performs far better than software written without that knowledge. You don't need to know the platform on a logic gate level, you need to understand how the platform executes your code, so that you can take advantage of the strengths of the platform. You don't need to know any hidden instructions or secrets to take advantage of the platform.
> we've had CPU-level issues like spectre and meltdown introduced that affected performance in some cases
those things didn't affect performance, the fixes for those things did. The fixes also required no code changes outside of the firmware and the operating system, and knowing the platform is still the best way to write performant software, no matter what is going on to avoid hardware vulnerabilities.
I'd say security is a bigger issue than performance most of the time.
And most gains are going to happen within the code itself by, e.g., not writing n^2 when there's a log(n) solution or something similar.
Plus we're talking about javascript, and that's likely to be software with network concerns, so your optimizations might be a rounding error compared to performance degradation from slow network connections.
>You don't need to know any hidden instructions or secrets to take advantage of the platform.
You don't know what they do though. Some of those ops could be more advantageous to performance to use in some cases. You can't fully know the platform if there are secret ops.
You can still make optimizations with partial knowledge, but don't pretend to know the platform when the platform manufacturer doesn't tell you everything about it.
>The fixes also required no code changes outside of the firmware and the operating system, and knowing the platform is still the best way to write performant software, no matter what is going on to avoid hardware vulnerabilities.
Good algorithm knowledge and practice is the most cost-effective way of writing performant code and is more than likely going to be the lion's share of issues.
I’d love to see that common thought validated because in practice I’ve seen it to not be true at all.
There are lots of cases where the complexity effects of the algorithm are swamped by cache effects. In fact basic foundational assumptions about complexity analysis are dangerously untrue on modern systems.
In my experience in either high throughput or low latency systems algorithmic complexity is never the issue. It’s always cache coherence, CPU prefectching/prediction, lock contention or over copying of data.
If you have Javascript devs that came out of some boot camp with no knowledge of either, do you teach algorithms 101 or low-level CPU programming 101 first?
I would argue your codebase would benefit from teaching them algorithms first, then the other one.
>In my experience in either high throughput or low latency systems algorithmic complexity is never the issue.
Do you encounter those workloads written in Javascript often?
Do you also go to a doctor that came out of a bootcamp? Is your house built by a constructor that came out of a bootcamp? Would you fly with an aviator that came out of a bootcamp? Would you run banking software made by a developer that came out of a bootcamp?
Even if you go "INTERNSHIP". Well what, is everyone supposed to stick the unpaid intern on toy apps that don't give them any actual real world experience writing actual production software?
Cause then the next argument will just be "Do you also go to a doctor that came out of an internship? Is you house built by a constructor that came out of an internship?".
Elitism at its finest.
Do you think doctors in training get to do open heart surgeries by themselves fresh out of university? Do you think we train aviators that can only fly using the autopilot? Because that's the way we treat software developers. This has nothing to do with elitism and everything with professionalism. Our industry has built training wheels in form of various VMs and high level languages because it missed the opportunity to properly train its workforce.
You keep going to the "OH MY GOD, PEOPLE WOULD DIE" examples to try and make a fairly weak point.
No one is going to die, because some noob made a crappy little site out of the millions of crappy little sites, and it's not performing like a demi-god.
VMs and high level languages aren't "training wheels". Especially not VMs, that's just complete and utter non-sense. Unless you think literally every website on the web should have a 100% dedicated server box.
VMs are good for a great many of things, both noob-friendly and not.
As for high level languages, they were meant for one particular thing. To get a task done quickly. Which is largely the real reason why so much software out in the wild performs like crap.
Anyone can sit down and spend years making a highly performant piece of software. But when things have to move fast, corners get cut & there's not enough time dedicated to researching to get said product to be as highly performant.
No I don’t think people die (although I wouldn’t be surprised if that was the case). I just don’t want to have to buy a 3000$ PC so I can run a fucking chat app, an editor and a browser somewhat decently. The opportunity cost of bad software is paid by billions of users every day.
By the way, my language VM is your language runtime.
The latter. It's also known as "computer architecture" and is something typically taught in the first two years of a bachelor's degree. How can you hope to learn to properly program a computer if you don't know what a computer is?
Too bad for HN, then, because commodifying web development has done it no favors.
> Many are still enjoying a low skill/high pay career and want to remain blissfully ignorant of their future.
That's where I was a few years ago. I'm a web developer who recently went back and re-learned all the low level stuff I forgot and didn't think was necessary 20 years ago. I was wrong.
There is certainly worth in awareness of performance issues - the key is to recognize the - for most of us - rare instances where it matters.
Yes! I mean, isn't the tradeoff exactly what got us Spectre? "Hey, let's aggressively pre-compute and cache for performance. Oh, darn, turns out that leaks information..."
https://en.wikipedia.org/wiki/Spectre_(security_vulnerabilit...
If anything, job requirements for programming are way too low. I wouldn't let a mechanic anywhere near my car if I knew s/he didn't understand the basics of an Otto engine. I'm the first to admit I don't know as much about modern CPUs as I should, but I don't live in a fantasy world where I convince myself that I don't need to know it.
>Many of these platforms have undocumented CPU instructions
So we should get the vendors to publish a proper documentation. This doesn't change anything about software developers responsibilities.
>CPU-level issues
Perfectly answered in a sibling comment so I'll just leave it at that.
That's hilarious because most of the electronics of cars are utter shit and it has nothing to do with how well those developers knew the CPU.
>So we should get the vendors to publish a proper documentation. This doesn't change anything about software developers responsibilities.
Most responsibilities are not concerned with low-level optimizations when javascript in particular typically deals with network connections
That was an analogy. I wasn't talking about car electronics. I was talking about the lack of professional education in programming.
>Most responsibilities are not concerned with low-level optimizations when javascript in particular typically deals with network connections
I have yet to encounter a javascript program that doesn't run on a CPU. Also, javascript is one of the most versatile and widely used languages. It runs on clients, servers, high end machines and embedded systems. So of course performance matters. If it didn't we wouldn't need WASM, asm.js and countless ridiculous JS engine optimizations. The time "saved" having to learn proper programming by using JS is dwarfed by the time wasted in CPU and end user time.
Yeah and I'm pointing out that for Javascript dev, there's a lot more knowledge with a higher priority to learn than low-level CPU code.
That's why we shouldn't add it to job descriptions unncessarily.
>If it didn't we wouldn't need WASM, asm.js and countless ridiculous JS engine optimizations.
This is probably why Javascript from the OP was probably the worst language to choose to make an argument. If you want to know the platform, you'll have to learn these things as well.
OTOH you can just write your super high performance thing in C or assembly and spend less time learning the extra layers between JS and the CPU. Then the low-level tuning becomes more important and obvious, and its merit is way more understandable.
This kind of knowledge is totally okay for jobs in C or assembly. It's not a great idea as a requirement for javascript jobs.
There are so many challenges trying to understand what your Javascript is doing under the hood. Take Node.js development for example. Your code is running on V8, likely running within a Docker container, perhaps even on a Kubernetes node or other distributed cluster. To truly understand what your code is doing at the CPU level is a _very_ difficult thing to do. More often than not, the performance issues I've run into have to do with topics like: slow performing network communication, poorly thought out algorithm design (excessive code), "noisy neighbors" where other containers are causing performance problems unrelated to your own code (for example, one issue we had with excessive times for DNS resolution was caused by the networking stack/tech in our k8s cluster being misconfigured and doing a GC every 60 seconds for 4-5 seconds at a time), or poor database-access patterns that cause excessive times for queries.
The JavaScript code itself and inefficiencies of what the resulting machine code is seems to rarely be the case for me. But that may also be due to the domain that I work in (it's actually one of the things that has me super bored with the software I work on these days).
To echo your own points, I find myself most often coaching junior developers on data-access patterns and design, minimizing network activity (if you think missing the cache and going to memory is slow for software, just imagine hitting the network and going to different machines), etc.
Like I said though, it may just be due to the domains I work in and being on the backend.
And all of this from a person who is _loving_ learning more about the internals and playing around with lower-level programming. I feel like the more I have learned at that level has made me a much better programmer as well. I wish I would have learned this stuff 10-15 years ago. So I'm torn.
By that logic we should never learn anything.
Those ops could be better in some cases, and until you know what they do, you can't answer that.
Here's the short version of branch predictor awareness. If you can write your inner loop with fewer conditionals, it will probably perform better.
Is it relevant and worth doing? At some level, only if your loop is frequent and fairly tight -- otherwise the difference is likely to be in the noise. On the other hand, writing your code to avoid branches in general can be a reasonable style and get you in the right place by default.
Everybody is claiming to be a 'Full Stack' developer these days, but apparently that doesn't include much of the stack.
Programming paradigms are orthogonal to that.
Everything from your structs and objects all the way up should be written with the data in mind.
Think about what pieces of data are needed at the same time, or in sequence, and not what classes you need to perform a certain action. Structuring your data types to keep pieces of data that will be operated on together, or read together alone can dramatically improve performance.
It's difficult for me to explain, and I'm confident once one sees examples that demonstrates the concept well, it will become clear pretty quickly.
My question is quite orthogonal to that. OOP lets you model your application in a way that somewhat natural. It's principles are well-researched. Resulting code is widely understood. It can be applied across many different domains.
I know no competing way to model software that comes even close to that qualities. Maybe I am wrong and data driven approach provides all that.
Perhaps it would be better to simply have appropriate tests and benchmarks to see what's slow on what platform?
Otherwise it's guess work based on incomplete understanding of the many layers below.
Outside of microcontrollers, I can't think of a processor in common use that doesn't have branch prediction or caches.
While, I'm a big fan not not writing poorly performing code, as you get death by a 1000 cuts. Is it not better to test rather than educated guess?
ie The reason there is alot of poor performing code, is not a lack of understanding at a low level about caches and branch prediction, but a lack of care about performance?
No one is going to write a single piece of code that runs on both a z80 and a 56-core Xeon, for example.
I would agree it can be useful to understand the lower levels, for the majority of developers macro optimisations are far far more important. Things like not forcing unnecessary redraws client-side, or excess DOM manipulations in general, caching values and references instead of rederiving them on each iteration of a loop, avoiding excess database hits, not using a naive sorting method, being aware of network latency issues, not properly indexing the database, and so on, will dwarf the effect of micro-optimisations for branch prediction and CPU caching if you get them wrong.
> understand your platform all the way down to the CPU
Which platform though? Not everyone is a back-end or embedded dev with constrained hardware to support. Are you targetting amd64 or ARM or something else? Any particular generation of CPU? In many cases an optimisation for one will have no effect elsewhere or worse will make others slower. Unless you are doing much tight-loop number crunching, is faffing around at this level really worth your time? Some understanding of cache concepts will help with overall algorithm design but most of what that knowledge will help you with generally (rather than for specific CPUs) is good practise for other reasons too.
> JavaScript developers
JS and other JIT compiled languages make tweaks for specific platforms even less important. For the most part let the optimising compiler worry about that for the platform it is compiling for at the time or consider a lower level language.
> never written in a low level language
I've written in assembly in the distant past (6502 & related, Z80, early x86 and a little of later x86). I did once used knowledge of cache sizes and population behaviour to optimise a little image processing (having the routine work on blocks of the pixel data that were small enough to remain in cache for longer than they would if not dividing into smaller chunks or if jumping around more randomly) done in inline assembly because Delphi's compiler produced a massively less optimal result if it wasn't.
I have enough low-level knowledge to feel safe saying I know that most developers (especially junior devs away from embedded (or other low-resource) systems) don't really need it, at least not in as much detail, in the current multi-platform world. They aren't working on code where the benefit of optimisations enabled by that knowledge aren't dwarfed by many other factors.
In most cases you don't need to go that deep to fix performance issues. Your SPA is probably slow because your app bundle is 10mb, not because you have an inner loop that is tripping the branch predictor.