Benchmarking division and libdivide on Apple M1 and Intel AVX512
ridiculousfish.com
ridiculousfish.com
>On the Integer side, whose in-flight instructions and renaming physical register file capacity we estimate at around 354 entries, we find at least 7 execution ports for actual arithmetic operations. These include 4 simple ALUs capable of ADD instructions, 2 complex units which feature also MUL (multiply) capabilities, and what appears to be a dedicated integer division unit. The core is able to handle 2 branches per cycle, which I think is enabled by also one or two dedicated branch forwarding ports, but I wasn’t able to 100% confirm the layout of the design here.
On the floating point and vector execution side of things, the new Firestorm cores are actually more impressive as they a 33% increase in capabilities, enabled by Apple’s addition of a fourth execution pipeline. The FP rename registers here seem to land at 384 entries, which is again comparatively massive. The four 128-bit NEON pipelines thus on paper match the current throughput capabilities of desktop cores from AMD and Intel, albeit with smaller vectors. Floating-point operations throughput here is 1:1 with the pipeline count, meaning Firestorm can do 4 FADDs and 4 FMULs per cycle with respectively 3 and 4 cycles latency. That’s quadruple the per-cycle throughput of Intel CPUs and previous AMD CPUs, and still double that of the recent Zen3, of course, still running at lower frequency. This might be one reason why Apples does so well in browser benchmarks (JavaScript numbers are floating-point doubles).
Vector abilities of the 4 pipelines seem to be identical, with the only instructions that see lower throughput being FP divisions, reciprocals and square-root operations that only have an throughput of 1, on one of the four pipes.
https://www.anandtech.com/show/16226/apple-silicon-m1-a14-de...
Reminder that browsers try to avoid using doubles for the Number type, preferring integers with overflow checks. Much of layout uses fixed point for subpixels, too. Using doubles all the time would be a notable perf regression.
Seems kind of gross to me to have such a language specific instruction to be honest.
At a logic level there are no changes to the expensive part of rounding, only changes to the overflow values in the result.
Much like his idea of $149 AirPods were made close to BOM cost. And that is how the whole world went on to believe all the wrong information.
It’s not a complex instruction, essentially using an explicit set of non default rounding flags. All in order to match what x86 does.
So if that instruction does help arm, it is only in getting rid of an advantage x86 had in being the dominant arch 25 years ago.
That's actually less than most desktop CPUs these days, and much less than Xeons.
https://en.wikichip.org/wiki/intel/microarchitectures/skylak...
If you have a source I'm happy to read it but otherwise I think you're confused. Especially about Intel client and server cores having different numbers of registers. The lowest level difference between them I've heard of that wasn't features being fused off is different L3 cache sizes.
I do remember I heard that physical register file was around 500 registers, but I believe my memory fails me now.
I mean, seriously, all that tradition and experience and you have a phone company make circles around you on your own field.
Strap a lot of cash and volume behind that after they get bought, and the results speak for themselves. There is no shame in it. Intel has been stumbling at the moment, but others were stumbling in the decade from Core 2 to Skylake.
PA Semi aren't a phone company, they just work for one ...
"These guys aren't just going to walk in and ..."
(famous last words, LXXXV)
and
"x86 has been a continuously supported backwards compatible architecture for 35 years, and enabled the existence of most computers for most of those 35 years"
are basically saying the same thing. Depending on which way you look at it, Intel can feel bad about it, or feel good about it.
Apple is in a unique position of being able to force a new architecture on its customers, without losing them. They have done it twice. They even aren't exactly compatible with normal ARM, due to a special agreement with ARM Holdings.
Intel had I960, quite cool and successful, and could not capitalize on it in the long term for low-power devices. Intel bought rights to ARM, and could not capitalize on it in the long term either, even though ARM was well-suited for battery-powered devices, and sold it!
Intel used to be the king of data centres, using an architecture from 1980s, extended and pimped up to the brim — but still beholden by backwards compatibility. And a king it still is. But this pillar seems to shake more and more.
I don't think this is a result of poor engineering. It was, to my mind, a set of business bets, which worked well, until they didn't any more.
I read the rest after typing this.
Intel was...it was king.
To be honest - Apple actually got some pretty damn good performance out of the PowerPC chips and architecture - my Quad-Core G5 tower with 16GB RAM is still used for finalization of my music projects, due to its insanely smooth performance - and tbh coming from a very experienced user of modern Macs it still kicks ass.
It is true that Apple implemented a bunch of custom optional features (some of which are, arguably, in violation of architectural expectations), and they definitely have some kind of deal with ARM to be able to do this, but from a developer perspective they are all optional and can be ignored. I don't think Apple exposes any of them directly to iOS/macOS developers. They only use them internally in their own software and libraries (some are for Rosetta, some are used in Accelerate.framework, some are used to implement MAP_JIT and pthread_jit_write_protect_np, some are only used by the kernel).
They very clearly have a special, Apple-only relationship with ARM.
FWIW, the compresssion instructions are used by the kernel, and I don't even know if they work from userspace. I've only ever tried them in EL2.
https://developer.arm.com/documentation/ddi0595/2021-03/AArc...
Apple's, in marketing speak, brand permission has given them extraordinary latitude over the past 15 to 20 years. They've been able to get off with making abrupt transitions and other relatively wrenching choices that tech pubs and doubtless forums like this wailed about but which their customers were mostly fine with. Things that Microsoft and WinTel laptops, for example, couldn't with respect to ports, limited options, etc. couldn't.
The main challenge is seamlessly migrating users to the new platform and Apple did a great job at it using Rosetta.
Both Linux and especially Windows struggle at this because they lack something as well integrated as Rosetta and require all applications to be recompiled to a new architecture.
As a result Microsoft has to worry about what the OEMs want and how they will use the product. In contrast Apple only cares what the end user wants, what their experience is, what features they get and how they work.
An OEM cares about whether they're making ARM laptops or Intel laptops. They care about and want input into the implementation details. An end user doesn't care if Photoshop is running on an ARM chip or an Intel chip, they care about how well it runs and what the battery life is. They (generally speaking) don't care about the implementation details.
Far less than a lot of people probably think though. AMD has an excellent x86 core, faster single threaded and throughput than the M1, on a generation older process technology, and quite possibly a smaller design and development budget than Apple, although not so power efficient.
Microsoft tried that with the Surface X and failed
When Apple rolls out a product, there's some transitional overlap, but you can see them getting ready to burn their viking ships in that period. Microsoft's efforts always have lacked that kind of commit factor. IMO.
Lack of key software from Microsoft doomed them to a niche.
People say this without thinking. There is no real evidence at all it is true.
Something like x86 support on an IA64 chip costs extra transistors. But there is no real fundamental reason why it should make anything slower.
This is even more so for AVX512 instructions, which aren't backwards compatible in anyway.
So - exactly - how would dropping backwards compatibility speed up AVX512 division?
But that's one switch, implemented in hardware in the decode pipeline.
It makes implementation more complicated, but no reason it has to be slower.
Now I wonder why x64 can't re-encode the instructions: Put a flag somewhere that enables the new encoding, but keep the semantics. This would make costs for the switch low. There will be some trouble, e.g you can't use full 64bit values. But mostly it seems manageable.
This is incorrect.
Intel Skylake has 5 parallel decoders (I think M1 has 8): https://en.wikichip.org/wiki/intel/microarchitectures/skylak...
AMD Zen has 4: https://en.wikichip.org/wiki/amd/microarchitectures/zen#Deco...
Which is impressive, if you think about it. But it is also complicated machinery for a part that's basically free when insns are fixed width and you wire the buffer straight to the instruction decoders. Expanding the pre decoder to 32 bytes would take a lot of hardware, while fixed width just means a few more wires.
https://stackoverflow.com/questions/23788236/get-size-of-ass...
The same technique could be extended to cover all of them and and it's not so difficult to implement this in verilog.
As long as this state machine runs at the same throughput as the icache bandwidth then it is not the bottleneck. It shouldn't be too difficult to achieve that.
But it is definitely extra complexity, and requires space and power.
Then again, both Intel and AMD make it work, so there must be a way, if you're willing to pay the hardware cost. Now I think about it, the same linear to logarithmic trick for adders can be done here: Put a state machine before every possible byte, and throw away any result where the previous predecoder said skip
This also demonstrates where it really hurts is when you want to do something low cost, and very low power, with a small die. And that's where ARM and RISCV shine. The same ISA (and therefore toolchain, in theory), can do everything from the tiniest microcontroller to the huge server. This is not the case for x86.
When I've been micro-optimizing performance-critical code, integer division shows up as a hot spot regularly. I assume most developers don't think about the performance implications of coding up a / or % between two runtime values, preventing the compiler from doing any strength reduction. Apple must have seen this in their surely voluminous profiling of real-world applications.
Also keep in mind that this Xeon might not be really made for number crunching (not really sure)?
An other possibility is that Apple has a very different profiling base e.g. iOS applications, whereas Intel and AMD would have more artificial workloads, or be bound by workloads / profiles from scientific computing or the like (video games)?
What can definitely play a role is that (I don't think it's as much of a problem these days, but it definitely has been in the past) is the standard "benchmark" suites that chipmakers can beat each other over the head with e.g. I think it was Itanium that had a bunch of integer functional units mainly for the purpose of getting better SPEC numbers rather than working on the things that actually make programs fast (MEMORY) - I was maybe 1 or 2 when this chip came out, so this is nth-hand gossip, however.
A compiler code generator that knows about a hypothetical two divide units (or just a much more efficient single unit) could be much more effective statically scheduling around them.
I’d guess that the bulk of the software running on the highest margin Intel Xeons was compiled some years ago and tuned for microarchitectures even older.
I'm still completely blind to how they are actually used but GCC and LLVM both have pretty good internal representations of the microarchitecture they are compiling for. If I ever work it out I'll write a blog post about it, but this is an area where GCC and LLVM are both equally impenetrable.
Keep in mind what calling conversions are:
"Calling conventions act as a contract between subroutines at the assembly level."
https://levelup.gitconnected.com/x86-calling-conventions-a34...
Let's enumerate what Apple has access to in more detail:
- third-party apps (in bytecode form)
- first-party apps
- the kernel
- a vast array of performance-critical libraries
- c/swift compilers
- machine code optimizers
- silicon design
- product stakeholders
The difference is that with Apple, the interaction between the various layers in the stack would be much lower friction than between Intel, AMD and Microsoft. The Apple Silicon team are truly single-customer. Equally as importantly, the Apple Product teams are truly single-buyer (eventually) and unlike Microsoft which has to split loyalties between Intel, AMD, ARM, and three dozen OEMs with their own motherboard designs tying all these bits together.And unlike Intel or Microsoft, everyone at every layer of Apple knows that their success will ultimately be judged on the same, singular metric: the quality/performance of the Apple product being released in September, and then the Apple product being released next March. That kind of unified focus is almost unheard of in the commodity PC space.
tl;dr 80 developers working on an open stack, even with coordination friction, are going to outperform 20 developers on a closed stack.
Using what metric?
It also assumes that more developers is inherently better. For a rebuttal, see The Mythical Man Month.
It's fair to say that three choices is better than two, but every additional choice will have diminishing returns. And at some point, the chances of an additional choice having any novel appraoach or distinctive features approaches zero.
To get the same level as access as Apple, Intel would have to cut a deal with Microsoft for access to their most sensitive telemetry data (and - likely - also to modify windows to collect more).
I think it's quite unlikely that MS would agree to that readily, if at all.
Apple doesn't have any such conflicting motivations because they sell the product rather than commodity parts. For Apple, improving the compiler as a unified part of the Apple Silicon development process means they can sell a better product with the same unit cost, with only residual benefit to competitors that don't have access to Apple Silicon hardware.
It's the packaging of all tools and libraries that Intel provides, extending to much more than "a compiler".
This example is pretty artificial - I can't really think of an optimization you could make knowing that you rarely turn left (maybe something with the axel?) but yea - more data means that you can turn your product to behave better in optimal situations - this comes at a cost but if you have a big stack of data you can make it so that your product generally wins that trade off most of the time.
1. https://www.bromfordlab.com/lab-diary/2019/4/9/why-do-ups-tr...
A real-world example are old NASCAR race cars. Optimized heavily for turning left, they were quite good at it at the expense of turning right.
https://www.anandtech.com/show/16226/apple-silicon-m1-a14-de...
Now I'm really curious. In my experience, integer division practically never happens (except by powers of 2 that are easily optimized down to a shift), to the point where I was frowning in puzzlement about why Apple spent resources optimizing it.
How is the code you are looking at, bottlenecking on integer division? What on earth is it doing, to make that a frequent operation?
The Xeon processor that he's comparing against is a six thousand dollar processor from two years ago that is absolutely destroyed by modern processors that cost less than one eighth of the price.
It is expensive because it is designed for 8-processor systems, with 4.5TB memory support, and it runs cores that are glacially slow individually, but meant to make up for it by the massive amount of them (24).
Finally, the test is effectively a single-core test, since he's measuring the execution units.
I think Apple deserves some kudos for having delivered a chip that would be competitive on the open market (if it were available as such), but all of these ridiculous comparisons just don't paint a picture that has any dose of reality.
The M1 is one process node ahead of AMD and two process nodes ahead of Intel. With this advantage, the M1 is performance equivalent[1] to AMD Zen 2 processors (3XXX series, which is one architecture behind) but much more expensive. They are price equivalent[2] to AMD Zen 3 processors, but much slower total performance (though the M1 has about a 25% perf-per-watt lead against the 5600X when limited to the same power envelope). The integrated graphics, however, is a step above.
And before I get downvoted to hell, the easiest way to see the actual performance of the M1 as compared to other processors is to look at Apple-to-Apple comparisons (pun partially intended) by comparing benchmarks from an Apple M1 to an Apple Intel, and looking at that same processor off of the Apple platform compared against other processors.
If you look at Phoronix Apple M1 to Apple i7-8700B benchmarks (ignoring Rosetta benchmarks since those unfairly favor Intel), you'll see that they perform similarly (in some cases Intel pulls ahead, in some cases the M1 pulls ahead, and in some cases they tie). Then when you compare that 6-core processor to a modern AMD six core processor (5600X), the AMD processor is 33% faster in single-core and 80% faster in multi-core benchmarks.
1. Cut out the synthetic benchmarks, because they are provably biased (e.g. intel biased userbenchmark). Look for application benchmarks that depend on compute, like file compression, or software compiles, or even real-workload browser benchmarks) 2. At MSRP, before current issues with chip availability
I would think that's a much better starting point for trying to understand the μ-architectural behavior.
Apologies if I'm overlooking that info, but I can't spot it anywhere in the article.
I really hope AMD doesn't adopt AVX512 and if they do I hope it's just the minimum for software compatibility.
On a related note, my Ryzen 2400G does not benefit from recompiling code with -march=x86-64-v3 in fact it seems a tiny bit slower. I assume Zen2 and 3 will actually run faster with that option.
Too late. Zen 4 would have AVX512.
> if they do I hope it's just the minimum for software compatibility.
Well,that might be the case of AVX512 doing interleaving as 2xAVX2. We'll see
As the nodes get denser i am sure AVX512 will perform better power and heat wise.
As for AMD adding support I am sure they will just for compatibility reason.
Sort of, in Skylake AVX512 fuses the 256-bit p0 and p1 together for one 512-bit µop, and p5 becomes 512-bit wide. So theoretically you get 2x 512-bit pipelines versus AVX2's 3x 256-bit pipelines (two of which can do multiplies.)
Unfortunately, p5 doesn't support integer multiplies, even in SKUs where p5 does support 512-bit floating-point multiplies. So AVX512 has no additional throughput for integer multiplies on current implementations.
https://uops.info/html-instr/VADDPD_YMM_YMM_YMM.html
Here is 256 bit VPMULUDQ: https://uops.info/html-instr/VPMULUDQ_YMM_YMM_YMM.html
Here is 512 bit VPMULUDQ: https://uops.info/html-instr/VPMULUDQ_ZMM_ZMM_ZMM.html
The 256 bit and 512 bit versions both have a reciprocal throughput of 0.5 cycles/op, using p01 for 256 bit and p05 for 512 bit (where, as you note p0 for 512 bit really means both 0 and 1).
So, given the same clock speed, this multiplication should have twice the throughput with 512 bit vectors as with 256 bit. This isn't true for those CPUs without p5, like icelake-client, tigerlake, and rocketlake. But should be true for the Xeon ridiculousfish benchmarked on.
The latency is also impressively good even at 64 bits, but the benchmark should not be div-latency bound in any case as, each division can be started without waiting for the previous division to finish.
edit: of course it is possible that internally the divider is not fully pipelined, but there are actually multiple dividers. It is only exposed as a single unit (i.e only a single port) because it wouldn't be able to start more than one operation per clock cycle in any case. If I had to guess, the additional SIMD dividers are repurposed for this, but that doesn't explain why 64 bit still has the same throughput as 32bit.
edit2: SIMD is on the same port as fdiv, not integer. fdiv has a throughput of 1 div per clock cycle!
[1] https://dougallj.github.io/applecpu/measurements/firestorm/S...
May I offer a nitpicking correction? 1.058ns compared to 6.998ns is an 85% savings, not 88%. The listing you have suggests that going down to 1.058ns is a bigger speed-up than going down to 0.891ns.
(PS - Verizon's sale of Yahoo has been in the news lately so I thought of you and the other regulars of the Programming chat room the other day. Hope all is well.)
Chaining the divisions (as a series of dependencies) would enable one to see the full latency of a single divide. You could use this data to estimate the number of divide units on the core.
* What's the variance of the measurements?
* Per core, the two processors actually (keep in mind based on Intel's TDP figure) have a roughly similar power budget i.e. 205/26 vs. 39/(4 or 8 depending on if you count the bigs, littles or both), so taking into account that the Apple processor is on a process that is something like 4 or 5 times denser it's not that surprising to me that its faster.
Edit: Yeah, that's only used for floating point. Looks like integer division is usually an algorithm called SRT: https://en.wikipedia.org/wiki/Division_algorithm#SRT_divisio...
Just one semi-random example, dividing two 1b unsigned integers is not going to need the same hardware as dividing two 1024b signed integers.
Why would a shell need a library to speed up integer division though?
I guess it can be good to have, but for the use case, sounds like overoptimization. Are the returns in speed that good for the use case?
If he referred to himself he could just say "my library for" instead of "fish's library for".
thanks for very interesting article, again!
Do you think the very fast division on M1 has any implications for 128/64 narrowing division as well? Do you know of a faster way than the method by Moller and Granlund? Do you plan on to include 128/64 division in libdivide?
And I asked this question before but the parent post got flagged so I'm trying once more: at the very bottom of the Labor of Division (Episode V) post [1], is it really possible for the second `qhat` (i.e. `q0`) to be off by 2? Do you have any examples of that?
[1] https://ridiculousfish.com/blog/posts/labor-of-division-epis...
Yes. I don't have an example in front of me, though. I think there may be one in Knuth vol 2. I'll take a look after toddler bedtime is over =)
Regarding the second question, it is possible to be off by 2. Consider (base 10) 500 ÷ 59. The estimated quotient qhat is 50 ÷ 5 = 10, but the true digit is 8. So if our partial remainder is 50, we'll be off by 2 in the second digit.
Multiplication will depend on whether the chip has one or two FMA units. If so, you can run vpmuludq on ports 0 and 5, which is a 2x speedup compared to AVX2's ports 0 and 1. This 8275CL Xeon does have 2 FMA units.
Looking at the two inner loops, we have
up:
vmovdqa64 zmm0,ZMMWORD PTR [rdi+rax*4] # p23
add rax,0x10 # p0156
vpmuludq zmm1,zmm0,zmm4 # p05 or only p0
vpsrlq zmm2,zmm1,0x20 # p0
vpsrlq zmm1,zmm0,0x20 # p0
vpmuludq zmm1,zmm1,zmm4 # p05 or only p0
vpandd zmm1,zmm6,zmm1 # p05
vpord zmm1,zmm1,zmm2 # p05
vpsubd zmm0,zmm0,zmm1 # p05
vpsrld zmm0,zmm0,0x1 # p0
vpaddd zmm0,zmm0,zmm1 # p05
vpsrld zmm0,zmm0,xmm5 # p0+p5
vpaddd zmm3,zmm0,zmm3 # p05
cmp rax,rdx
jb up
up:
vmovdqa ymm0,YMMWORD PTR [rdi+rax*4] # p23
add rax,0x8 # p0156
vpmuludq ymm1,ymm0,ymm4 # p01
vpsrlq ymm2,ymm1,0x20 # p01
vpsrlq ymm1,ymm0,0x20 # p01
vpmuludq ymm1,ymm1,ymm4 # p01
vpand ymm1,ymm1,ymm6 # p015
vpor ymm1,ymm1,ymm2 # p015
vpsubd ymm0,ymm0,ymm1 # p015
vpsrld ymm0,ymm0,0x1 # p01
vpaddd ymm0,ymm0,ymm1 # p015
vpsrld ymm0,ymm0,xmm5 # p01+p5
vpaddd ymm3,ymm0,ymm3 # p01
cmp rax,rdx
jb up
All other things being equal, we have on average, and counting only the differing instructions, a throughput of ~2.27 instructions per cycle on the AVX2 loop, whereas it is somewhere around ~1.45-1.60 for AVX-512, depending whether you have 1 or 2 FMA units to run multiplications on port 5.So based on this approximation, the AVX-512 code should probably run around 2*(1.5/2.27) ~ 1.33 times faster. Add to this that vpmuludq is actually one of the most thermally insensitive instructions around and will reduce your core's frequency by 100-200 MHz, and the small speedup you see is more or less explainable. (I actually do see some more noticeable speedup here when switching to AVX-512; 0.25 vs 0.21).
PS: The Intel Icelake and later chips also manage to achieve a throughput of 1/2 divisions per cycle for 32-bit divisors, and 1/3 divisions per cycle for 64-bit divisors.
PPS: Some of your functions could use blends. For example,
__m256i libdivide_mullhi_u32_vec256_(__m256i a, __m256i b) {
__m256i hi_product_0Z2Z = _mm256_srli_epi64(_mm256_mul_epu32(a, b), 32);
__m256i a1X3X = _mm256_srli_epi64(a, 32);
__m256i hi_product_Z1Z3 = _mm256_mul_epu32(a1X3X, b);
return _mm256_blend_epi32(hi_product_0Z2Z, hi_product_Z1Z3, 0xAA);
}
__m512i libdivide_mullhi_u32_vec512_(__m512i a, __m512i b) {
__m512i hi_product_0Z2Z = _mm512_srli_epi64(_mm512_mul_epu32(a, b), 32);
__m512i a1X3X = _mm512_srli_epi64(a, 32);
__m512i hi_product_Z1Z3 = _mm512_mul_epu32(a1X3X, b);
return _mm512_mask_blend_epi32((__mmask16)0xAAAA, hi_product_0Z2Z, hi_product_Z1Z3);
}* Would be wise to compare x86_64 under Rosetta as it'll support some AVX translation if I remember correctly.
* I didn't see use of Apple's accelerate framework. To comply with ARM64 additional custom Apple magic is within Private extensions / ops that should use higher level frameworks such as Accelerate
Rosetta2 supports up through SSE2. That's the latest instruction set to no longer be patented as of around 2020. They can use x86_64 only because AMD released x86_64 spec in 1999 (even though actual chips came much later).
% arch -x86_64 sysctl -a | grep machdep.cpu.features
machdep.cpu.features: FPU VME DE PSE TSC MSR PAE MCE CX8 APIC SEP MTRR PGE MCA CMOV PAT PSE36 CLFSH DS ACPI MMX FXSR SSE SSE2 SS HTT TM PBE SSE3 PCLMULQDQ DTSE64 MON DSCPL VMX EST TM2 SSSE3 CX16 TPR PDCM SSE4.1 SSE4.2 AES SEGLIM64
Go into reader mode, the article is great.
It should not be performing like this. I bought a base spec m1 mac mini for testing apple silicon applications on and it has ended up displacing a 2019 16" MBP unexpectedly - the mac mini is far faster in real world usage for me, and your workflow is exactly what I'm doing with it.