I translated a simple C program to x86_64 and it was slower
ecc-comp.blogspot.com
ecc-comp.blogspot.com
1) Try to load things sequentially don’t load from offset 32, then 0, then 16.
2) Don’t use the loop instruction
3) this code:
dec n
cmp n, 0
jne 1b
Can be replaced with: sub n, 1
jnz 1b
Which will actually be executed by the processor as a single instruction (macro-op fusion).4) Interleave your expensive instructions with less expensive ones. Try to interleave multiple dependency chains to let the processor see more of the parallelism.
Your two divisions one after another will be limited by available execution units capable of doing the divide.
5) Lastly align your loop labels to 16-byte offsets. The assembler will do this for you with the ALIGN directive.
I suspect the assembly generated by the compiler is pretty fancy.
Edit: Just briefly looking at the objdump again, gcc is using a lot of scalar variants of instructions, instead of vector. Maybe this is giving the CPU better hints?
Maybe my code would run better on a more modern CPU (while still constraining the original C program to SSE3 only) ?
If you have a lot of practice the compiler is not difficult to beat. But it definitely takes some effort and study of the optimization manuals.
You get named constants and named labels that way, and you can even choose the sane Intel syntax with -masm=intel which you can also do with objdump.
At a first glance, it seems the compiler version is better at hiding the latency of some of the div instructions. It might be hiding memory access latency, too. But that's more involved analysis.
That being said, you're not going to see the really crazy optimizations that compilers can do, because they need a bit of a higher level view of what you're trying to do.
the original from intel, iaca: https://software.intel.com/en-us/articles/intel-architecture...
newer clone, shipped with llvm: https://llvm.org/docs/CommandGuide/llvm-mca.html
And the follow up https://youtu.be/nAbCKa0FzjQ.
But sometimes things that look like dependencies are fake.
XMM0 = XMM1 + XMM2 XMM3 = XMM0 + XMM4 XMM0 = XMM5 + XMM6 XMM7 = XMM0 + XMM8
At first glance it may look like you have to calculate these instructions serially. But, by renaming the last two `XMM0` you eliminate the dependency on the specific register, and can calculate instructions 1 and 3 in parallel, followed by 2 and 4 in parallel.
Regiser renaming is designed assuming the optimizer
Then after that you can merge the operations where you can. (for SSE4 at most you are going to get is 2x because you are using doubles)
You may think the full inlining is cheating but the compiler has the same information as your bodies list is entirely constant. (since your dt and your masses are constant they can also potentially be folded).
https://benchmarksgame-team.pages.debian.net/benchmarksgame/...
The faster programs usually use SIMD.
https://benchmarksgame-team.pages.debian.net/benchmarksgame/...
Wouldn't expect a C++ implementation to use 200x the memory of a C implementation. Different algorithms presumably? Or a significant compiler optimisation being missed?
Those graphs show the same as they did in 2013:
https://web.archive.org/web/20130411161423/http://benchmarks...
Do you mean these:
https://web.archive.org/web/20130520174141im_/http://benchma...
Do you mean these:
https://web.archive.org/web/20140608064549im_/http://benchma...
What problem would that solve for you ?
Not "cute" enough?
(More people complained about those charts than thought them cute.)
My first attempt was massively slower than the compiled code. I had to get into the whole pipelining that was going on and other hardware optimisations.
When I discovered putting a NOP in a tight loop sped up the code, I realised I wasn't going to beat the compiler most of the time!
After much investigation what I found out was that the original code without the NOP was actually running at only 1/2 the speed that it should. Due to very bad luck, the addresses of the jump targets in the inner loop where placed in a configuration where the branch predictor failed to predict the jumps (perhaps because of collisions in the internal "hash tables" used by the jump predictor). Any nudge to the executable would get the program out of the pathological configuration. Using a different compiler version, different OS , or different CPU model all did the trick. But the most fun of course was that adding or removing a NOP also made the difference :)
https://devblogs.microsoft.com/oldnewthing/20110112-00/?p=11...
My anecdote from hand optimization is that I didn't even know some instructions existed.
It's true. C compilers are so optimized, the day of re-write in assembly have long passed for most of us.
All nitpicking about the quality of the assembly is valid of course, but it does inadvertently help prove the point, especially since most or all of these things are things compilers do today.
Watch from 27:00 where he talks about using const wherever possible so the compiler can optimise. It reduces a full function call to work out a colour in 3D space from thousands of instructions to one.
After watching this and a few others particularly dynamic dispatch in Swift, I find myself adding as many hints as I can and applying the most restrictions / scoping possible. Final, const, Private etc.
https://developer.apple.com/swift/blog/?id=27
Even though actually it can work this out in your module!
Of course, for most people and most uses, its not worth the effort.
Again, the point isn’t that better assembly couldn’t be written. It’s that it most likely wouldn't be significantly better than the compiler because all of the suggestions are things compilers would be doing anyways. There are some cases where this isn’t true, especially when dealing with vectorization, but those are mostly just exceptions (and intrinsics often offer easier ways to do such optimizations...)
But here’s the point that I feel is often ignored when it comes to programming language debates in general: just because you are experienced with and aware of advanced usages of the environment you’re programming in, does not mean the complexity and especially cognitive overhead of said complexity has disappeared. Looking at the C version, it doesn’t really look especially optimized, which is not really something that you would see in assembler, at least not in my opinion. Complexity adds up over time; abstractions are the antidote to that problem.
On top of that, assembly language is obviously not portable, which IMO is even more reason to use a high level language and drop to asm only when needed; you can easily swap implementations and have a fallback for architectures that aren’t specifically optimized.
I’m not arguing that it’s worth it or that it’s easy to beat the compiler. I certainly am not going to bother writing assembly (maybe some intrinsic for SIMD but certainly not raw assembly, outside of embedded systems, although even then it’s not really worth it usually).
I’m simply saying that you can’t expect to be good at something unless you practice it.
But that doesn’t mean people shouldn’t try but to learn. For example, somebody has to implement the optimisations isn’t he compiler and that person needs to have a great understanding of how to produce high performance assembly code. Plus learning new things is always worthwhile if you have the time.
During the 80's and early 90's it was a different matter, because CPUs were dumb, hardware was relatively static specially on 8 and 16 bit consumer systems and high level optimizers were pretty dumb given the resource constraints of those platforms.
Back in the day, you could easily know all opcodes for a given CPU, and their clock cycle timings.
This is the SIMD guide for the Intel CPUs,
https://software.intel.com/sites/landingpage/IntrinsicsGuide...
Which is only a tiny subset of all the opcodes that a modern Intel CPU is able to understand, let alone what AMD also offers.
You need tools like VTune from each CPU vendor to actually understand the CPU clock timings of each opcode in micro-ops (microcode execution unit).
While you can master a specific subset, like knowing AVX instructions, mastering Assembly back to back like in the old days, only when writing Assembly for stuff like small PIC microcontrollers.
Trying to master a language like C++ is easier, which says a lot about how modern CPUs look like.
LOOP is a bit of a weird case. I've seen it benchmark both slower and faster than dec/jnz depending on the surrounding instructions.
now, does anyone know why?
My guess is virtual stack pointer update prediction latency.
To expand on that, Intel's CPUs have had for a long time a separate piece of hardware dedicated to a "virtual" stack which speeds up push/pop instructions. If pushes and pops are not mismatched, then all stack operations can stay entirely within that and there's no need to update the "real" stack pointer nor stack entries upon leaving the loop.
In my experience it's good to first attempt a rewrite in C using intrinsics. This gets you thinking about data and register layout at a high level and let's you better identify mid level optimizations before committing to assembly.
To the people with suggestions, especially jdsully: thank you! I will probably give these a shot.
To the people who say "blah blah someone who doesnt know x giving an opinion on y blah blah", ok, what is wrong with sharing an experience? I'm not sure how to interpret this other than you take what I write way too seriously? It's an explorative piece...
Edit: I wanted to mention too that it's really cool reading other's stories around this topic. Thank you for sharing!
On the other hand, with branchy "business logic"/general-purpose code and algorithms that can't really be vectorised, it's pretty easy to beat a compiler with handwritten Asm --- on speed, size, or often both.
You can optimize a small routine. Anything that's not a toy rapidly becomes impossible to optimize by hand.
Just the register allocation itself can be a gigantic pain to do by hand, even with a limited register set of x86, let alone other RISCs with much larger register files.
EDIT: Added clarification in the wording.
Just the register allocation itself can be a gigantic pain to do by hand, even with a limited register set of x86,
Actually, that's one of the main advantages of using Asm --- compilers are relatively horrible at register allocation, because they don't take a more "holistic" view of the program, and end up shuffling values between registers or registers and memory far more often. This is why handwritten Asm has a unique "texture" to it.
let alone other RISCs with much larger register files
There's much less room for optimisation with handwritten Asm on a RISC, because with x86 the opportunities are precisely those the compiler can't easily see, unlike a very uniform and boring RISC.
First, you are setting up a classic "no true Scotsman". By accepting that "toy" routines can be optimized, you grant a response where (by your definition) if it can be optimized it must be a "toy". Such arguments usually aren't very satisfying to participate in.
But perhaps we can start from there. Assume we have a complex program that relies on a numerical computation that takes 90% of the runtime. While this isn't the norm for a modern GUI program used at home, I'll assert from authority and experience that such programs do exist within the realm of scientific computing.
I'll then go on to assert from authority and experience that you can often optimize such routines with vectorized assembly to run 2x as fast on a particular modern processor. In theory, a compiler could do the same. In practice, they don't --- or at least, they don't unless you are willing to write performance-fragile and non-portable code that's actually more fragile than clear assembly
As for a specific example, how about this one: http://chemfp.com. By using an approach that was possible with assembly but not with C, I was able to give about a 40% overall boost in speed to this program when used for a particular task. I optimized it in assembly, it's not a toy, and it's faster as a result.
It's also true that I was able to get about a 30% boost while sticking with compiler vector intrinsics and a particular version of a particular compiler, but the performance was so fragile to version changes that it seemed safer to go with inline assembly. And the only reason I was able to get this boost was that I already knew the assembly I was trying to generate.
I think instead the right lessons are:
1) It's often possible for an experienced programmer to make things run significantly faster on a specific generation of processors by optimizing in assembly.
2) Blind translation to assembly usually won't achieve much of an improvement, and might even hurt. It requires deeper knowledge of the internals of how processors work, roughly comparable to the level of understanding a compiler designer might need.
3) Outside of special cases where extreme performance is necessary, it's rarely worth pursuing this sort of optimization in one-off programs. Usually, the effort is better spent in libraries and compilers that will allow greater leverage.
Since I'm interested in this, I'd like to see the case to reproduce with an analysis of the performance difference. BLAS developers have made statements about GCC optimization failures (at least vectorization) that aren't correct in my experience with at all recent GCC. BLIS' generic C kernel already runs at ~60% (I forget the exact number) of the speed of the Haswell assembler code for large DGEMM without any tuning attempt, perhaps with GCC extensions. (I didn't check whether the blocking is right for Haswell or pursue an analysis with MAQAO or something.)
mov [mem], reg
div [mem]
rather than simply div reg
I'm no asm guru, but I expected the L1 cache to be slower than a register, and certainly not a round-trip via L1 to be several percent faster.The reason I stumbled upon this was that the compiler emitted the first variant and I tried to optimize it to the second, but lost speed. That's when I decided I'll stick to optimizing my high-level code...
FWIW this div was in a somewhat tight loop, but sadly I've totally forgotten the rest of details. I do recall adding nops here and there to play with loop alignment, it wasn't that.
But in that case the performance could totally change again if just one more instruction is added.
There are plenty of instances where compilers go haywire and generate code far worse than even a half-competent novice would, because compilers don't know the same things about your code that you do.
Seems just to be speculation.
Might we just as easily speculate that — if the compiler generates bad code for a program,that still looks a lot better than the comparison interpreters so people will submit it ?
I started to take a look at your problem, but I wasn't able to run the code that you posted to test.
For the C code, the "#include" statements at the top are missing their <header.h>. Easy to solve now that you posted the link to the original, but maybe you could fix?
For the assembly, you are relying on "list.macro", which I couldn't find. Could you post this, or even better link to a repository that can be used?
Separately, it would probably be helpful to post the exact model number of your processor. Speed doesn't (shouldn't?) really matter, but the "generation" is essential.
Also, I don't think you mention the number of iterations you are running for your timing. [I see now in the assembly that you seem to be using 5000000?]
Post a bit more info, and someone here (maybe me, maybe not) will figure it what's causing the slowdown.
Yep, it's 5 million iterations.
I will update the post with list.macros immediately!
This is my CPU: Intel(R) Pentium(R) CPU N3700 @ 1.60GHz
Edit: it is updated. :) list.macros has been inlined in the snippet.
In the meantime, I'll mention that my first quick discovery is that clang seems to be significantly faster than gcc on the standard C code. The ratio changes with different versions and compilation options, but on Skylake with "-Ofast -march=native" I find clang-6.0 to be almost twice as fast as gcc-8. So if you have clang installed, check and see if it might be a better baseline.
Also, what system are you running? This shouldn't make a difference with execution speed, but will make it easier to make tool suggestions. If you are running some sort of Linux, now would be a good time to get familiar with 'perf record'!
Edit: > Intel(R) Pentium(R) CPU N3700 @ 1.60GHz Hmm, that's a "Braswell" part, which unfortunately isn't covered in Agner's standard guide to instruction timings (https://www.agner.org/optimize/microarchitecture.pdf) and I'm not familiar with it's characteristics. This might make profiling a little more approximate.
I added the build instruction for the assembly but I'll add it here too: gcc nbodies.s -no-pie -o nbodies.out
Shoot me an email if you get around to it! :D
I'll check out perf record.
The preliminary results I get disagree with what you are seeing. I'm not sure if this is my error, your error, or just genuine differences between processors. Specifically, on Skylake, I get your assembly to be much faster than GCC, although slightly slower than Clang. And that's without trying to use options to limit the compiler:
gcc-8 -Ofast -march=native bodies.c -Wall -lm -o bodies_gcc_Ofast_native_c
perf stat bodies_gcc_Ofast_native_c 5000000
2,031,645,589 cycles # 3.691 GHz
2,181,200,813 instructions # 1.07 insns per cycle
gcc bodies.S -o bodies_S
perf stat bodies_S
1,293,433,853 cycles # 3.691 GHz
2,641,011,827 instructions # 2.04 insns per cycle
clang-6.0 -Ofast -march=native bodies.c -Wall -lm -o bodies_clang_Ofast_native_c
perf stat bodies_clang_Ofast_native_c 5000000
1,158,569,067 cycles # 3.691 GHz
2,331,116,659 instructions # 2.01 insns per cycle
Not having understood the assembly yet, my guess from skimming is that the compiler isn't able to vectorize this code well, and thus the SSE/AVX distinction isn't going to matter much. "-Ofast" should be comparable to "-O3" here, although I don't recall the exact differences. I didn't use "-no-pie" with your assembly, but don't think this matters.Are you able to do a similar comparison and report results with "perf stat"? Cycles and "instructions per cycle" are going to be better metrics to compare than clock time. No hurry. I'm East Coast US, and done for the night.
Edit: A quick glance at "perf record" and "perf report" suggests that clang is slightly faster than you (on Skylake, using these options) because it's making use of fused multiply-adds. Which slightly contradicts what I said about the SSE/AVX distinction not mattering, although it's only a minor effect. For both routines, the majority of the time spent is in the chain starting with the division. I'm wondering if there is some major difference in architecture with Braswell --- perhaps it only has a single floating point multiplication unit or something? Or one of the operations is relative _much_ slower.
Edit 2: Looking at https://en.wikichip.org/wiki/intel/cores/braswell, I now see that Braswell is the name of the "system on a chip", and the CPU microarchitecture is Airmont. This isn't in Agner either, but at least I've heard of it! https://en.wikichip.org/wiki/intel/microarchitectures/airmon... says Airmont is basically the same as Silvermont, which Agner does cover, so we should be able to figure out timings. See page 320 here: https://www.agner.org/optimize/instruction_tables.pdf.
Edit 3: I hadn't actually looked at your code yet. So you are trying to vectorize, it's just that you are limited in doing so because you can only do 2 doubles at a time. But since you are working in 3D, this doesn't fit evenly, so you do [x,y] then [z,null]. Thus you expected it to be something like 1.5x faster on your processor, and instead it comes out 2x slower. I think the issue might be that your processor doesn't really have full vector units --- instead it's doing some sort of emulation. Look closely at the timings on Page 338 of the instruction table I linked in Edit 2. Note that DIVPD takes just about twice as long as DIVSD. Then check Skylake on Page 252 -- same time for packed and single XMM. While this isn't the full answer, I think it's a strong hint at the issue. The quickest fix (if you actually want to make this faster on your processor) is to use the single instruction for the [z,null] case. This isn't going to help much overall, since it takes twice as long for the packed, but it might at least get them back to parity with the compiler! If you actually want higher speed from your vectorization, you may have to switch to a processor that has better vectorized speeds.
What is the speed with restriction to SSE3? That would finalize the tests.
Do you mind if I directly quote you in a follow up post? This is really good stuff.
I think this is the right incantation to restrict to SSE3:
clang-6.0 -O3 bodies.c -msse3 -Wall -lm -o bodies_clang_O3_sse3_c
perf stat bodies_clang_O3_sse3_c 5000000
1,470,870,787 cycles # 3.691 GHz
3,391,153,749 instructions # 2.31 insns per cycle
gcc-8 -O3 bodies.c -msse3 -Wall -lm -o bodies_gcc_O3_sse3_c
perf stat bodies_gcc_O3_sse3_c 5000000
2,256,550,525 cycles # 3.691 GHz
3,306,361,186 instructions # 1.47 insns per cycle
It would be interesting to analyze the difference between GCC and Clang here. Just glancing without comprehension, it looks like one big difference is that Clang might be calling out to a different square root routine rather than using the assembly builtin. Hmm, although it makes me wonder if maybe that library routine is using some more advanced instruction set?> Do you mind if I directly quote you in a follow up post?
Sure, but realize that I'm speculating here. Essentially, I'm an expert (or at least was a couple years ago) on integer vector operations on modern Intel server processors. But I'm not familiar with their consumer models, and I'm much less fluent in floating point. The result is that I know where to look for answers, but don't actually know them off hand. So quote the primary sources instead when you can.
Please do write a followup, and send me an email when you post it. My address is in my HN profile (click on username). Also, I might be able to provide you remote access to testing machines if it would help you test.
gcc-8 -fno-math-errno -O3 bodies.c -msse3 -Wall -lm -o bodies_gcc_O3_sse3_c
perf stat bodies_gcc_O3_sse3_c 5000000
1,398,805,282 cycles # 3.691 GHz
3,181,041,562 instructions # 2.27 insns per cycle
I haven't thought about it much, but more info here: https://stackoverflow.com/questions/37117809/why-cant-gcc-op.... While the question is about C++, it hints that the spec might require sqrt() to set errno if given a negative number. Clang special cases this by adding a never taken branch, but gcc does not (unless told it doesn't need to care).This is how I learned to program assembler for Linux. I already knew TASM / MASM / a86 assembler languages for DOS/Windows - all similar but different in their own regards - but gas is a different un-backward beast, and of course the system calls are totally different between Linux and DOS/Windows.
For example, here are 6 different ways to program helloworld on an ARM cpu (Raspberry Pi) running Raspbian Linux: https://github.com/ksaj/helloworld I kept each of them as close to the same as possible, so that it is easy enough to see the differences in how each call is set up before execution.
I'm sure if you added timer code before and after the example code, you'd very quickly discover which methods are more optimal and then begin to dissect why that would be, and how to use that knowledge in future programs.
Keep in mind that the order of opcodes will impact execution speed, because of the many techniques modern cpus use to speed things up (branching, prediction, encoding techniques, entirely rewriting opcodes to something else with the same functionality - eg, most compilers understand that mov ax,0, sub ax,ax and xor ax,ax all do the same thing), etc.
OpenRCT2 is a code conversion to C++. You can turn off all the visual tweaks they have implemented on top of the original game and it still requires significantly more CPU time than the original. And thats with a compiler being able to take advantage of modern CPU instructions while the original game can't do that.
Might you or anyone else have the updated link(s) to the nbody problem?
Looks like the subdomain was changed at some point.
You can get some gains from SIMD code (i.e. autovectorization is hard), e.g. you've laid out data deliberately but the compiler doesn't quite see it, but modern CPUs are so complicated even executing scalar instructions that I wouldn't bother. I think optimizing memory is more productive half the time anyway, most programs don't spend that much time number crunching.
Some execution fails: hardware has pathological instruction sequences which force sub optimal choices to be made.
Alternative algorithms cannot always be identified from an instruction sequence. What if a higher order function identifed better seeds and led to faster detection in a large field of candidate solutions?
https://en.wikipedia.org/wiki/Midpoint_circle_algorithm
The non-english articles have code.
- compute the set of bits (called a ‘region’) to operate on
- call the appropriate function to erase/fill/… that region
I would guess drawing ovals was done this way:
- create a region for a circle with radius equal to that of the radius of the corners.
- insert horizontal parts to each row of bits in the region to ‘stretch’ the region horizontally into a rounded rectangle that has the correct width, but is only as high as the circle.
- insert vertical parts to each column of bits in the region to ‘stretch’ the region vertically into a rounded rectangle that has the correct width and height.
The first step was identical for the code for drawing circles; the region data structure made the last two operations cheap; they did not require any memory allocations. You had to walk the entire data structure, but for small corner radiuses, it wasn’t that large. Also, one could probably optimize for speed by doing that while creating the region for the circle.
I can’t find a good description of the region data structure, but https://www.folklore.org/StoryView.py?project=Macintosh&stor... might be enough for some to figure out how it, conceptually, worked.
And remember: the original Mac had about 28 kilobytes of RAM free for applications. The system unloaded icons, code and fonts that were available on disk all the time. Few objects were ‘tiny’ at the time.
Isn’t that the whole point?
So, to sum it up: guy who has no clue writes worse code then one generated by a decent compiler.
I thought that the latest consensus on HN was that C is nowhere closer to the hardware.
Caching, out-of-order execution, branch prediction, speculation... all of those things are just as opaque to assembly as they are to C.
C isn't even close to that anymore, now that so many processors have SIMD ISAs which C can't usefully model.
1. Processors no longer look all that much like C's idea of what a processor is.
2. Processors are optimized to be able to execute C code very well. But an optimizing compiler can make it much better.
There's a fair amount of magic in compilers to make this work.