Faster CRC32 on the Apple M1
dougallj.wordpress.com
dougallj.wordpress.com
And certainly not all blogs have to, but it's nice when it is.
So this has little to do with RISC, except the general principle that the instructions that are used more frequently should be implemented to be faster, a principle that has been used by the M1 designers and by any other competent CPU designers.
In this case, ARM has added the polynomial multiplication instruction a few years after Intel, with the same main purpose of accelerating the authenticated encryption with AES. There is little doubt that ARM was inspired by the Intel Westmere new instructions (announced by Intel a few years before the Westmere launch in 2010).
The dedicated CRC32 instruction could have been made much faster, but the designers of the M1 core did not believe that this is worthwhile, because that instruction is not used often.
The polynomial multiplication is used by many more applications, because it can implement CRC computations based on any polynomial, no only that one specified for CRC32, and it can also be used in a great number of other algorithms that are based on the properties of the fields whose elements are polynomials with binary coefficients.
So it made sense to have a better implementation for the polynomial multiplication, which allows greater speeds in many algorithms, including the CRC computation.
Ok maybe it is just a point against really complex instructions. There's clearly an optimum middle ground.
But the microcode and hardware floating point implementations did it slightly differently. Then the MicroVAX dropped it, then picked it up again but wrong, then fixed it, then lost it again.
http://simh.trailing-edge.com/docs/vax_poly.pdf
https://documentation.help/VAX11/op_POLY.htm
https://en.wikipedia.org/wiki/Multiply%E2%80%93accumulate_op...
>Multiply–accumulate operation
>The Digital Equipment Corporation (DEC) VAX's POLY instruction is used for evaluating polynomials with Horner's rule using a succession of multiply and add steps. Instruction descriptions do not specify whether the multiply and add are performed using a single FMA step. This instruction has been a part of the VAX instruction set since its original 11/780 implementation in 1977.
https://news.ycombinator.com/item?id=20558618
>VAX was a crazy town ISA too, with stuff like single isntruction polynomial evaluation.
They get a bad rap because the only really complex ISA left is x86 and it just had especially bad ideas about which operations to use its shortest codes on. Nobody uses BOUND to the point some CPUs don’t even include it.
One point against them in SIMD is there definitely is an instruction explosion there, but I haven’t seen a convincing better idea, and I think the RISC-V people’s vector proposal is bad and shows they have serious knowing what they’re talking about issues.
That’s ok for big vectors you’d find in like scientific computations, but I believe it’s bad for anything I’ve ever written in SIMD. Games and multimedia usually use short vectors (say 4 ints) or mixed lengths (2 and 4 at once) and are pretty comfortable with how x86/ARM/PPC work.
Not saying it couldn’t be better, but the RISCV designers wrote an article about how their approach would be better basically entirely because they thought SIMD adds too many new instructions and isn’t aesthetically pretty enough. Which doesn’t matter.
Also I remember them calling SIMD something weird and politically incorrect in the article but can’t remember what it was…
Then you can do whatever you did for games and multimedia in the past, except that you can process N samples/pixels/vectors/whatever at once, where N = vector length / 4, and your code can automatically make use of chips that allow longer vectors without requiring a recompile.
Mind you, I don't know if that's the direction that the RISC-V people are taking. But it seems like a pretty obvious thing to do.
This way the CPU design can be simplified and all the complexity moved into compilers.
One thing I learned from the M1 transition is that people will do it if someone tells them to. I bought a Mac this weekend to do exactly that; lots of users complaining about lack of M1 support. Time to add it (in a way that I can test). I have no choice.
But GPU programs get compiled from source at runtime all the time, and sort of are all about vectors. (M1’s GPU doesn’t actually have vectors.)
They poured a lot of money into the compiler. It didn't work out.
Again and again, complexity is proven cancerous. RISC is the way to go.
The grandparent comment is about a compiler generating multiple code copies for different CPU architecture iterations not to support all legacy instructions or at least to allow to implement those via microcode emulation in later CPUs.
Do not want. Especially seeing how hostile to programmers they've become.
And it doesn't "loop" in one instruction, the vsetvl instruction will set the vector length to the _minimum_ of the vector register size and the amount of data left to process.
If there appears a new CPU with different vector size then you can just recompile the program and let compiler use new instructions. Therefore it doesn't make sense to sacrifice chip area or performance for this feature.
Regarding backward compatibility (running old software on a next generation CPU), I think that newer CPUs do not need to support obsolete instructions from previous generations. Instead they could just include a decoder that would (using microcode and therefore slowly) emulate old instructions. This allows user run their package manager or recompile their software and switch to new instructions. It is totally fine to completely redesign the ISA provided that there is a minimal converter for old instructions or at least support for software-based translator like qemu.
Also, the person who said graphics uses 3 or 4 element vectors so you only need short vector registers didn't understand how a vector processor such as RISC-V will be programmed for that take. Usually you have thousands of XYZ or XYZW points that you want to transfor my the same matrix or normalize or whatever. You don't load one point into one vector register. You load all the X coordinates from 4 or 8 or 64 or 256 points into one vector register, all the Y coordinates into another vector register, all the Z coordinates into another one. And you put the 9 or 12 elements of the matrices you want to transform them by into other vector registers (or scalar registers if you're transforming everything by the same matrix) and you transform a whole bunch of points in parallel.
What if you have a 4-vector and want its dot product? x86 SIMD has that. (It’s slow on AMD but was okay for me.)
Vector instructions could add that too, because they can add whatever they want. I’m just suspicious it would actually be efficient in hardware to be worth using.
ARM is developing SVE but is allowing hardware to require only powers of 2 vector sizes IIRC, which surely limits how you can use it.
"What if you have a 4-vector and want its dot product?"
Dot product with what?
Of course on RISC-V V you can load two 4-vectors into two vector registers (perhaps wasting most of the register if it has 256 or 512 or 1024 or more bits), multiply corresponding elements, then do an "add reduce" on the products.
But that's not very interesting. Don't you want to do dot products on hundreds or thousands of pairs of 4-vectors? Load the 0th elements of 4 or 8 or 64 etc into register v0, the 1st elements into v1, 2nd into v2, 3rd into v3. Then the same for the 4-vectors you want to dot product them with into v4, v5, v6, v7. Then do v0=v0v4;v0+=v1v5;v0+=v2v6;v0+=v3v7. And then store all the results from v0.
People used to short SIMD registers look at vector problems along the wrong axis.
Meant to say “two 4-vectors”.
> Don't you want to do dot products on hundreds or thousands of pairs of 4-vectors?
Unfortunately not. I was thinking of a raytracer there, and it doesn’t have any more data available. I could speculate some more data or rewrite the program entirely to get some more, but the OoO and multi-core CPU is a good fit for the simplest pixel at a time approach to raytracing.
In the other case I’d use SIMD for, video codecs, there is definitely not any more data available because it’s a decompression algorithm and so it’s maximally unpredictable what the next compressed bit is going to tell you to do.
1) Producing several uops for each cut in the vector register;
2) Producing a single uop with a size field, the vector unit know how to split the operation;
2) Producing a single uop size-agnostic, the vector unit know the size from the vector register metadata.
The worst is probably 1), it will stress the ROB, uop queue and the scheduler, and make the decoding way more complex. But solutions 2) and 3) keep the decoding simple and are much more resource efficient.
An implementation aspect that may be complicated is the vector register allocation depending on the vector-unit microarchitecture (more specifically, depending on the vector register file microarchitecture)
Also, I'm not clear why you think riscv style vector instructions will perform worse on 2-4 length vectors.
Assumes there’s no switching cost to changing it and there won’t be restrictions on the values it can be set to. Weird restrictions on memory loads are pretty traditional for SIMD instructions after all.
The switching cost is the biggest problem; this is exactly the kind of thing that x86 has specified, then found they can’t do without microcoding it or otherwise making it cripplingly inefficient on different HW generations.
> doesn't look good at all for big-little designs which seem to be the future Why not? It's not clear to me why OSes shouldn't provide a "don't migrate me while I'm checking CPU capabilities and then running this SIMD/vector kernel".
> very few applications use it [AVX-512] because it requires optimization for a very small portion of the market. Maybe so (though increasing), but the cost is also low - we literally just pay with binary size and compile time (for the extra codepath).
The potential of transparent SIMD use by compilers has apparently stayed marginal enough that no mainstream application languages in all that time have updated towards facilitating it.
The single instruction might also be more power-efficient and keep other resources free for other stuff.
Without those, the NEON version would fall to the same throughput as one CRC32X per cycle, or half the throughput on cores with 2x64bit ALUs.
This is even more true in the case of CRC, where there’s clearly almost always one branch that wins: this is perfect for branch prediction, which would mean the whole “if eq” condition is preemptively skipped.
So for a hypothetical example, it could be that using general purpose SIMD triggers the system to throttle up the CPUs and/or move to the high performance CPUs, whereas the dedicated CRC instructions might exist on the high-efficiency cores and not trigger any throttling.
I've forgotten all my computer architecture theory, but if I look back at Ohm's law and look at power, the equation is P = I^2 • R. Handwaving from my forgotten theory a bit here, ramping up the CPUs increases current, and we see that it is a squared factor. So by cutting the time by say a factor of 3 does mean you are done 3 times faster (which is a linear component), you still have to contend that you have a squared component in current which may have been increased.
I have no clue if the M1 actually does any of this, but merely stating that it is not obvious what is happening in terms of power efficiency. We've seen other examples of this. For example, I've read that Intel's AVX family instruction generally increases the power consumption and frequency of when utilized, but non-obviously, it often runs at a lower frequency when in 256 or 512 wide forms compared to the lesser widths (which then requires more work on the developer to figure out what is the optimal performance path as wider isn't necessarily faster). And as another example, when Apple shipped 2 video cards in their Macbooks, some general purpose Mac desktop application developers who cared about battery life were tip-toeing around different high level Apple APIs (e.g. Cocoa, Core Animation, etc.) because some APIs under the hood automatically triggered the high performance GPU to switch on (and eat power), while these general purpose desktop applications didn't want or need the extra performance (at the cost of eating the user's battery).
M1 has a heterogeneous ISA, FWIW.
I'd argue against "SIMD" as being "RISC", since you need all sorts of complicated instructions (ex: gather/scatter) to really support the methodology well in practice.
RISC vs CISC is about the simplicity of the instruction set, not about whether it's easy to use.
I'd argue AArch64 isn't particularly RISC by the standards of the past but it sets the bar and tone for RISC today.
And if we're talking about multiple instruction-sets designed for the same purpose, is this thing really RISC anymore? Or do you really mean "just not x86" when you say RISC ??
Not in NEON, and therefore not in M1. AVX512 and SVE add scatter/gather instructions.
Intel/AMD's AVX has vgather instructions, but is missing vscatter until AVX512.
> Having dedicated instructions for specific operations (whether for crc/aes/nnp or whatever) feels like a CISC-based approach, so I think I agree with the GP.
Not only are there AES instructions on ARM, but there's also SHA-instructions. The mix-columns step of AES more or less demands dedicated hardware if you want high-speed today, so everybody implements that as a hardware specific instruction.
It was a time of tight transistor budgets, and there was a make-or-break sweet spot of instruction set size and complexity that could be hardcoded in the ~100k transistors (vs microcoded[1], as was the norm).
IMO the fundamental idea was that you should optimize the instruction set for your applications to a certain extent, while still keeping it coherent for humans and stable across processor generations.
RISC still gives ease of use weight, without this your instruction set might be a cryptic machine learning box of mystery operations with bizarre temporal/hidden state semantics (think delay slots but much much worse) that gets rebooted on every CPU release.
[1] See eg this Motorola 68000 internals description on how the microcoded thing worked: http://www.easy68k.com/paulrsm/doc/dpbm68k1.htm
And this is really the CISC vs RISC argument and why all these RISC cpus have these CISC like instructions. You want top perf in general code you assure the rep sto and mov sequences (or whatever) run the fastest microcoded version possible on a given core. But intel sorta messed this up in the p6->nehalem timeframe (IIRC when they added the fast string flag) until they rediscovered this fact. IIRC Andy Glew admitted it was a bit of an oversight combined with an release/area issue on the original PPro they intended to fix, but then it took 10 years.
In general I agree with you though, optimizing memcpy implementations only against microbenchmarks is dumb.
[1] https://www.anandtech.com/show/16226/apple-silicon-m1-a14-de...
My largely uninformed guess is that they added the instructions to get fast CRCs for the filesystem 'for free'. There aren't many other cases where software CRC can be a bottleneck that also use these polynomials.
Easy gains are everywhere. The "gotcha", if you can call it that, is that optimizing particular operations comes with space tradeoffs that are more expensive when you do them in hardware.
It gets a bit messy, and we can't expect a ton from this approach - the same loop with only the loads only runs at ~86GB/s, but it'd be worth a shot.
(A(x) mod Q(x)) * (B(x) mod Q(x)) = (A(x) * B(x)) mod Q(x)
If the chunk size N is known beforehand you can pre-calculate x^N mod Q(x), so appending N zeros will be an O(1) multiplication.
Only if the chunk size is not known, you have to calculate x^N mod Q(x) via modular exponentiation, which is O(log n). But you only need to do this once, and then you can reuse the value for all subsequent chunks.
Was Apple silicon always this best-in-class and we weren’t looking this closely as a community?
It hasn't really been a huge deal though because people don't develop directly on an iPhone, so it doesn't affect their every day productivity all that much. Also phone's have reached the point of "fast enough" a few years ago, it's hard to tell the difference between an iPhone 13 Pro and an 11 Pro unless you use them side by side. But with the release of the M1 chip, people are getting the performance & energy efficiency gains in their every day workflows.
It seems a good idea to start a Code Golf competition.
I'm using an 8th(?) generation Intel, i7-8665U.
https://github.com/htot/crc32c has some interesting implementations of CRC32 algorithms of different speeds, the highest I see is (function, aligned, bytes, MiB/s) :
crc32cIntelC true 16 3907.613
crc32cIntelC true 64 15096.758
crc32cIntelC true 128 24692.803
crc32cIntelC true 192 22732.392
crc32cIntelC true 256 16233.397
crc32cIntelC true 288 16748.952
crc32cIntelC true 512 19862.039
crc32cIntelC true 1024 22373.350
crc32cIntelC true 1032 22482.031
crc32cIntelC true 4096 24690.531
crc32cIntelC true 8192 24992.827
So pushing 25GiB/s on a 3ish year old CPU.This article seems to miss that distinction but appears to be testing CRC32, so it's not quite correct to compare against something using Intel's CRC32C instruction.
crc32cIntelC true 64 26210.561
crc32cIntelC true 128 35870.309
crc32cIntelC true 192 36850.224
crc32cIntelC true 256 30343.690
crc32cIntelC true 288 30671.327
crc32cIntelC true 512 32443.251
crc32cIntelC true 1024 34654.719
crc32cIntelC true 1032 34265.440
crc32cIntelC true 4096 38111.089
crc32cIntelC true 8192 38634.925 crc32cIntelC true 16 4025.334
crc32cIntelC true 64 15749.095
crc32cIntelC true 128 26608.064
crc32cIntelC true 192 25828.486
crc32cIntelC true 256 17448.436
crc32cIntelC true 288 18336.381
crc32cIntelC true 512 22635.590
crc32cIntelC true 1024 24654.248
crc32cIntelC true 1032 24180.107
crc32cIntelC true 4096 28251.903
crc32cIntelC true 8192 28768.134I measured it on my computer to run at 32GB/s, i7 4770k.
Looking at https://developer.arm.com/documentation/ddi0596/2020-12/Base..., based upon the polynomial constant, the CRC32 class of instructions appear to calculate a CCITT 32 reversed polynomial. Are there any ARM developers who can help me out here? Does this apply in the same way to the M1?
The Castagnoli polynomial, 0x1edc6f41, is used to compute a crc in Btrfs, Ext4, iSCSI and various other places.
Edit: He invented the crypto not the CRC, which Phil Katz was already using.
He wasn't even the first to put a CRC in an archive format, as the predecessor format ARC had a CRC-16 doing the same thing.
(It's a very bad cypher, vulnerable to known plaintext and other attacks, don't use it for anything except light scrambling).