Re: [PATCH] crypto: ecc - Optimize vli additive operations using compiler builtins
Fabian <[email protected]> Mon, 3 Aug 2026 10:32:12 +0200
| Newsgroups | org.kernel.vger.linux-crypto,org.kernel.vger.linux-kernel |
|---|---|
| Message-ID | <CAGtAT=n+wEp+BLVBo57cx3nYX8Juj5NXvmhU=ueAVshSL1Bd6g@mail.gmail.com> |
On Sun, 5 Jul 2026 at 07:33, Herbert Xu <[email protected]> wrote: > > On Sun, Jun 07, 2026 at 01:24:35PM +0200, Fabian Blatter wrote: > > Replace the software carry flag emulation with compiler builtins. > > > > Even the newest compilers struggle with taking advantage of the > > hardware carry flag. Compiler builtins allow the compiler to > > much more easily achieve this while still remaining constant-time. > > > > This yields an approximately 6-7% performance improvement > > on the ecc_gen_privkey, ecc_make_pub_key and crypto_ecdh_shared_secret > > functions on x86_64 on all curve sizes. > > > > Additionally, the code becomes much more readable. > > > > Signed-off-by: Fabian Blatter <[email protected]> > > --- > > > > Hi, > > > > I'd like to expand on the benchmarks, compare the generated assembly, > > and clarify some things. > > > > > > Use of compiler builtins: > > > > This patch uses __builtin_addcll, __builtin_subcll when available and > > otherwise __builtin_uaddll_overflow, __builtin_usubll_overflow. the > > latter have existed since ancient gcc versions, so no third fallback > > is needed. > > > > I have put the add_carry and sub_borrow inline functions with the > > preprocessor logic for builtin selection directly in crypto/ecc.c. > > Please let me know if you would like them to be somewhere else. > > > > They do not emit data-dependent branches, and so remain constant-time. > > > > > > Benchmarks: > > > > All benchmarks were run single-threaded on my AMD 7700X CPU limited to > > 5.6Ghz. I have measured both nanoseconds and clock cycles, since their > > combination can hint at downclocking issues and allows calculation of > > the clock speed during the benchmark. > > > > I have omitted the raw output from the benchmarking code, as they much > > exceed the 72 character limit. > > > > I have calculated the percent differences, included clock speed > > calculations and relevant summaries. > > > > > > Macro benchmarks: > > > > These were run in a virtualized environment using virtme-ng on the > > compiled linux kernel image compiled with default flags. > > > > (the first value is the original time per operation, the second the > > patched one. cc is short for clock cycles) > > > > Curve keypair generation (ecc_gen_privkey + ecc_make_pub_key): > > > > P256: > > - 646963ns/op -> 600632ns/op = -7.71% > > - 2911300cc/op -> 2702854cc/op = -7.71% > > - 4.4999Ghz -> 4.5000Ghz = no difference > > > > P384: > > - 1239160ns/op -> 1153940ns/op = -7.38% > > - 5576250cc/op -> 5192749cc/op = -7.38% > > - 4.5000Ghz -> 4.5000Ghz = no difference > > > > Shared secret generation (crypto_ecdh_shared_secret): > > > > P256: > > - 320114ns/op -> 297548ns/op = -7.58% > > - 1440521cc/op -> 1338972cc/op = -7.58% > > - 4.5000Ghz -> 4.5000Ghz = no difference > > > > P384: > > - 620768ns/op -> 582560ns/op = -6.55% > > - 2793467cc/op -> 2621529cc/op = -6.55% > > - 4.5000Ghz -> 4.5000Ghz = no difference > > > > The benchmarks clearly indicate a roughly 6-7% performance increase on > > the public API functions. It also appears that virtme-ng limited the > > clock speed to 4.5Ghz > > > > > > Micro benchmarks: > > > > Since the vli additive functions only rely on u64 being defined, these > > were run without virtualization and with varying compilers and > > compiler flags. > > > > The microbenchmarks show much more mixed results, depending > > heavily on the compiler and optimization level used. > > > > For instance, on gcc and O2, the vli_add present in the > > patch is actually 25.3% slower than the original one. I have tracked > > this down to gcc using a weird way to restore the carry flag after > > each iteration, causing way more dependent instructions, preventing > > ILP from executing multiple at once. > > > > This is quite interesting, since, as far as I know, the kernel compiles > > with gcc and O2 by default, yet the macro-level benchmarks still show a > > performance increase. The effect seems to be reversed when crypto/ecc.c > > gets compiled. Or maybe the linux kernel uses some additional > > optimization flags, I am unsure. > > > > However, most of the time, the patched version outperforms the original > > one by a wide margin: > > - On clang -O2 or -O3, vli_add and vli_uadd show a 4.074x and 5.384x > > speedup. > > - On gcc, vli_uadd shows a 74% performance increase at O2, > > and a 2.07x speedup at O3. > > > > The performance profile of vli_sub and vli_usub is almost identical to > > that of vli_add and vli_uadd. > > > > > > Assembly comparison: > > > > I have put together a piece of code on Compiler explorer, to make sure > > it compiles on old gcc versions, view instructions and play around with > > compiler settings. > > > > If you would like, you can play around yourself here: > > https://godbolt.org/z/1jT5zesz8 > > > > When using clang 22.1 at -O3 -march=lunarlake, the difference between > > the patched and original version is particularly clear. The patched > > version produces this assembly in the unrolled vli_add loop: > > > > mov rax, qword ptr [rsi + 8*rcx + 16] > > adc rax, qword ptr [rdx + 8*rcx + 16] > > mov qword ptr [rdi + 8*rcx + 16], rax > > mov rax, qword ptr [rsi + 8*rcx + 24] > > adc rax, qword ptr [rdx + 8*rcx + 24] > > mov qword ptr [rdi + 8*rcx + 24], rax > > mov rax, qword ptr [rsi + 8*rcx + 32] > > adc rax, qword ptr [rdx + 8*rcx + 32] > > mov qword ptr [rdi + 8*rcx + 32], rax > > mov rax, qword ptr [rsi + 8*rcx + 40] > > adc rax, qword ptr [rdx + 8*rcx + 40] > > mov qword ptr [rdi + 8*rcx + 40], rax > > mov rax, qword ptr [rsi + 8*rcx + 48] > > adc rax, qword ptr [rdx + 8*rcx + 48] > > > > This is basically optimal for an inner loop. It's pure adc and mov > > instructions. The loop counting part is still nowhere near perfect, > > and still uses setc instructions. But it is still better than what > > the original version produces with the same compiler and flags: > > > > mov r10, qword ptr [rsi + 8*rcx] > > lea r11, [r10 + rax] > > add r11, qword ptr [rdx + 8*rcx] > > xor ebx, ebx > > cmp r11, r10 > > setb bl > > cmove rbx, rax > > mov qword ptr [rdi + 8*rcx], r11 > > mov rax, qword ptr [rsi + 8*rcx + 8] > > lea r10, [rax + rbx] > > add r10, qword ptr [rdx + 8*rcx + 8] > > xor r11d, r11d > > cmp r10, rax > > setb r11b > > cmove r11, rbx > > mov qword ptr [rdi + 8*rcx + 8], r10 > > mov rax, qword ptr [rsi + 8*rcx + 16] > > lea r10, [rax + r11] > > add r10, qword ptr [rdx + 8*rcx + 16] > > xor ebx, ebx > > cmp r10, rax > > setb bl > > cmove rbx, r11 > > mov qword ptr [rdi + 8*rcx + 16], r10 > > mov rax, qword ptr [rsi + 8*rcx + 24] > > lea r10, [rax + rbx] > > add r10, qword ptr [rdx + 8*rcx + 24] > > xor r11d, r11d > > cmp r10, rax > > setb r11b > > cmove r11, rbx > > > > This is downright horrendous. that entire block of processes only 4 > > limbs, thats 8 instructions per limb! The add instructions > > are also not adc instructions, showing that the carry flag is > > being fully emulated. This demonstrates how even on the newest > > compilers and at the highest optimization level, still cannot > > generate hardware carry chains without explicit use of builtins. > > > > I should note that not just clang 22.1.0 with -O3 -march=lunarlake > > does this. Gcc and clang show this behaviour on every version i have > > tested, regardless of target architecture. > > > > I am not very familiar with ARM or RISC-V assembly, but looking at > > compiler explorer, the effect clearly persists, and in the case of > > RISC-V actually gets much worse. > > > > This affects all architectures across all compilers and compiler > > flags. > > > > > > If you have gotten this far, thank you for reading this and I am looking > > forward to any feedback! If you would like any changes to this patch, > > I am very happy to send a v2. > > > > crypto/ecc.c | 98 ++++++++++++++++++++++++++++++++-------------------- > > 1 file changed, 60 insertions(+), 38 deletions(-) > > Patch applied. Thanks. > -- > Email: Herbert Xu <[email protected]> > Home Page: http://gondor.apana.org.au/~herbert/ > PGP Key: http://gondor.apana.org.au/~herbert/pubkey.txt Hi, So about the rewrite of crypto/ecc.c to a constant-time Montgomery ladder, I'd like to just make sure it is still on the table. I also have a few questions about the rewrite: The paper (https://eprint.iacr.org/2020/956.pdf), which is mentioned in the conversation Lukas sent, uses a different type of curve than the current implementation. Is a rewrite using entirely different elliptic curves intended, or is there some other algorithm that should be used with the existing NIST curves? The paper also talks about assembly and how to make it constant-time by avoiding memory operations and doing everything in registers. Is it intended to be an Assembly implementation or is C acceptable? If it is Assembly, I could only reasonably contribute an x64 version. Regards, Fabian