secp256k1: ASM implementation of Field64 on AMD64 using MULX/ADX#3734
secp256k1: ASM implementation of Field64 on AMD64 using MULX/ADX#3734valery-osheter-cb wants to merge 3 commits into
Conversation
davecgh
left a comment
There was a problem hiding this comment.
Thank you very much for the PR. I will start taking a closer look at the asm in depth soon.
A couple of things I noticed in the mean time:
- Please add tests similar to how
blake256does it where it tests as many of the implementations (generic and specialized) as possible depending on the available hardware support when the tests run. (e.g. https://github.com/decred/dcrd/blob/master/crypto/blake256/internal/compress/blocks_amd64_test.go#L20) - Please provide comparative benchmarks for the generic versus specialized implementation. I don't have an Intel available at the moment and I'd prefer results form real hardware over emulation.
|
Something definitely looks off with this. Benchmarks from a Ryzen 5800X3D: |
After looking at it more closely, it's the With that change (i didn't update square, but it should be the same thing): This is why I requested benchmarks! |
davecgh
left a comment
There was a problem hiding this comment.
I've manually reviewed the asm for field64MulADX up through the REDUCE macro. I wanted to go ahead and give some feedback up to this point. I will continue reviewing the rest later.
| PEXTRQ $1, X1, R15 | ||
| MULXQ AX, R8, CX | ||
| MULXQ R13, R9, DI | ||
| ADCXQ CX, R9 |
There was a problem hiding this comment.
Commenting for future reviewers that this is safe because the XORQ BX, BX above unconditionally clears CF (and OF for the ADOX call below) per the Intel Software Developer's Manual (and AMD's) despite many other sources claiming that it only affects ZF and SF and leaves CF and OF unmodified.
There was a problem hiding this comment.
Alright, I finished reviewing all of the assembly and asserting correctness and making sure all carries are propagated properly through both independent CF and OF chains.
Very nice work overall!
I did notice in field64SquareADX that you could likely ever-so-slightly improve scheduling since MULX takes 3-4 cycles per agner fog's tables and you have three of them back-to-back when you could technically interleave them with the ADCX like you did in field64MulADX. That said, I doubt it would be very much difference, if any, and it's very easy to follow and prove correctness as it is, which I find valuable. So, I don't think it's worth changing it.
Based on my previous blake256 work, something I do highly suspect would be ~5-10% faster is to use the memory operands directly versus putting them into XMM registers and then having to extract the upper 64 bits from them via PEXTR. e.g.
MOVQ a+8(FP), SI
XORQ R13, R13
XORQ R15, R15
XORQ CX, CX
// Off-diagonal upper-triangle products into p1..p6 (a1->SI, a3->DI reused).
MOVQ 0(SI), DX
MOVQ 24(SI), DI
MOVQ 16(SI), R8
MOVQ 8(SI), SI
...
I know that theoretically dealing with the registers should be faster, but, my benchmarks consistently have shown that the above tends to win out when it's only a single load like this. Perhaps it is specific to the micro-architecture.
Out of curiosity, I went ahead and made that change and benchmarked it. It appears that the pattern holds: |
650952c to
a5eba07
Compare
|
I did the fixes:
I also don't have Intel hardware, my PC is M3, so I use CI for the tests. I did the branch field64-amd64-CI for that. |
|
Thanks for the updates. I've reviewed the updated assembly and reasserted correctness with the updates. I've also confirmed the fix for closures so it is no longer allocating. Here is a comparative benchmark from your original XMM implementation and the new direct load implementation for mul on a zen4. It's an even bigger increase to perf than I suspected it would be for mul. I only expected ~5-10% and instead it's ~10-15%. I am quite curious to know how it compares on Intel though, since these results are for AMD (it's similar for earlier generations of zen as well, btw). It's quite possible Intel handles the scheduling differently which could potentially make it more or less efficient, but I would not at all be surprised if this current implementation is also notably faster on Intel. It is essentially trading out 18 instructions that involve larger instructions to decode and extra deps between the SIMD and integer register files, for what is certainly going to be hot loads from half an L1 cache line. I would also expect it to be less work for the scheduler, because it also gets rid of the uops for register allocation and writeback. For your tests question, that's fine. I can also make any modifications later. I'm mostly concerned with there being tests in place at the time of merge to prevent any possible regressions in the mean time. |
Summary
field64_generic.gobehind apurego || !amd64build tag.field64_amd64.go.