Add initial vectorized chacha20 implementation for 2-3x speedup #34083

pull theuni wants to merge 3 commits into bitcoin:master from theuni:chacha20-vectorized-initial changing 5 files +434 −5
  1. theuni commented at 8:42 PM on December 16, 2025: member

    Exploit modern simd to calculate 2/4/6/8/16 states at a time depending on the size of the input.

    This demonstrates a 2x speedup on x86-64 and 3x for arm+neon. Platforms which require runtime detection (avx2/avx512) improve performance even further, and will come as a follow-up.

    Rather than hand-writing assembly or using arch-specific intrinsics, this is written using compiler built-ins understood by gcc and clang.

    In practice (at least on x86_64 and armv8), the compilers are able to produce assembly that's not much worse than hand-written.

    This means that every architecture can benefit from its own vectorized instructions without having to write/maintain an implementation for each one. But because each will vary in ability to exploit the parallelism, we allow (via ifdefs) each architecture to opt-out of some or all multi-state calculation at compile-time..

    Here, as a starting point, x86-64 and arm+neon have been defined based on local benchmarks.

    Local profiling revealed that chacha20 accounts for a substantial amount of the network thread's time. It's not clear to me if speeding up chacha20 will improve network performance/latency, but it will definitely make it more efficient.

    This is part 1 of a series of PR's for chacha20. I think it makes sense to take a look at the generic implementation and tune the architecture-specific defines for parallel blocks before adding the runtime-dependent platforms.

    My WIP branch which includes avx2/avx512 can be seen here: https://github.com/theuni/bitcoin/commits/chacha20-vectorized/

    I've been hacking on this for quite a while, trying every imaginable tweak and comparing lots of resulting asm/ir. I'm happy to answer any questions about any choices made which aren't immediately obvious.

    Edit: some more impl details:

    I wrestled with gcc/clang a good bit, tweaking something and comparing the generated code output. A few things I found, which may explain some of the decisions I made:

    • gcc really wanted to inline some of the helpers, which comes at a very substantial performance cost due to register clobbering (and with avx2, vzeroupper). Hence, all helpers are decorated with ALWAYS_INLINE.
    • gcc/clang do well with the vec256 loads/stores with minimal fussing. Though loading each element with [] is clumsy and verbose, it avoids compiler-specific layout assumptions. Other things I tried (which produced the same asm):
      • casting directly to using unaligned_vec256 __attribute__((aligned (1))) = vec256
      • memcpy into ^^
      • clang's __builtin_masked_load
    • Loop unrolling was hit-or-miss without #pragma GCC unroll n, and I tried to avoid macros for loops, hence the awkward recursive inline template loops. But in practice, I see those unrolled 100% of the time.
    • I used std::get in the helpers for some extra compile-time safety (this actually pointed out some off-by-one's that would've been annoying to track down)
    • I avoided using any lambdas or classes for fear of compilers missing obvious optimizations
    • All vec256 are passed by reference to avoid an annoying clang warning about returning a vector changing the abi (this is specific to x86 when not compiling with -avx). Even though our functions are all inlined, I didn't see any harm in making that adjustment.
  2. DrahtBot commented at 8:42 PM on December 16, 2025: contributor

    <!--e57a25ab6845829454e8d69fc972939a-->

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    <!--006a51241073e994b41acfe9ec718e94-->

    Code Coverage & Benchmarks

    For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/34083.

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline and AI policy for information on the review process.

    Type Reviewers
    Concept ACK jonatack

    If your review is incorrectly listed, please copy-paste <code>&lt;!--meta-tag:bot-skip--&gt;</code> into the comment that the bot should ignore.

    <!--174a7506f384e20aa4161008e828411d-->

    Conflicts

    No conflicts as of last run.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

    LLM Linter (✨ experimental)

    Possible typos and grammar issues:

    • Read 32bytes of input, xor with calculated state, write to output. -> Read 32 bytes of input, xor with calculated state, write to output. [“32bytes” is a typo that hurts readability]

    <sup>2026-08-25 16:16:08</sup>

  3. theuni commented at 8:49 PM on December 16, 2025: member

    Adding pings for a few people I've discussed this with: @sipa @ajtowns @l0rinc

  4. in src/crypto/chacha20_vec_base.cpp:26 in 3bddf59cd3 outdated
      21 | +#  define CHACHA20_VEC_DISABLE_STATES_6
      22 | +#  define CHACHA20_VEC_DISABLE_STATES_4
      23 | +#  define CHACHA20_VEC_DISABLE_STATES_2
      24 | +#endif
      25 | +
      26 | +#include <crypto/chacha20_vec.ipp>
    


    ajtowns commented at 1:11 AM on December 17, 2025:

    Why separate this into an .ipp file if it's only included in a single .cpp file?


    theuni commented at 4:14 PM on December 17, 2025:

    See branch here: https://github.com/theuni/bitcoin/commits/chacha20-vectorized/

    I decided to exclude the impls which require runtime detection from this PR, as I think how that should be done is a separate conversation.

    To answer your question more specifically: some impls may require different compilation flags (-mavx2/-mavx512vl), which have to be in their own compilation units.

  5. in src/crypto/chacha20_vec_base.cpp:12 in 3bddf59cd3 outdated
       7 | +// This file should define which states should be en/disabled for all
       8 | +// supported architectures. For some, like x86-64 and armv8, simd features
       9 | +// (sse2 and neon respectively) are safe to use without runtime detection.
      10 | +
      11 | +#if defined(__x86_64__) || defined(__amd64__)
      12 | +#  define CHACHA20_VEC_DISABLE_STATES_16
    


    ajtowns commented at 1:39 AM on December 17, 2025:

    Using #if !defined for these seems old school. Why not static constexpr bools, and if constexpr (..)?

    Writing:

    #if defined(__x86_64__) || defined(__amd64__)
    #  define CHACHA20_VEC_DISABLE_STATES_16 true
    #  define CHACHA20_VEC_DISABLE_STATES_8  true
    #  define CHACHA20_VEC_DISABLE_STATES_6  true
    #  define CHACHA20_VEC_DISABLE_STATES_4  false
    #  define CHACHA20_VEC_DISABLE_STATES_2  false
    #elif defined(__ARM_NEON)
    ...
    
    static constexpr bool CHACHA20_VEC_ALL_MULTI_STATES_DISABLED =
        CHACHA20_VEC_DISABLE_STATES_16 &&
        CHACHA20_VEC_DISABLE_STATES_8 &&
        CHACHA20_VEC_DISABLE_STATES_6 &&
        CHACHA20_VEC_DISABLE_STATES_4 &&
        CHACHA20_VEC_DISABLE_STATES_2;
    
     void chacha20_crypt_vectorized(std::span<const std::byte>& in_bytes, std::span<std::byte>& out_bytes, const std::array<uint32_t, 12>& input) noexcept
     {
        if constexpr (CHACHA20_VEC_ALL_MULTI_STATES_DISABLED) return;
    ...
        if constexpr (!CHACHA20_VEC_DISABLE_STATES_16) {
             while(in_bytes.size() >= CHACHA20_VEC_BLOCKLEN * 16) {
    ...
    

    seems to work right, and also seems like it avoids needing to put all the potentially unused code in a #if block.


    ajtowns commented at 7:58 AM on December 17, 2025:

    Here's a branch that replaces the #defines and abstracts the recursive template stuff a bit more for your perusal https://github.com/ajtowns/bitcoin/commits/202512-pr34083-templates/


    l0rinc commented at 3:26 PM on December 17, 2025:

    +1 for if constexpr, should simplify the code a lot (especially if we migrate from recursive templates as well)


    theuni commented at 4:25 PM on December 17, 2025:

    if constexpr certainly makes more sense where possible. I'll have a look, thanks!

  6. in src/crypto/chacha20_vec.ipp:38 in 3bddf59cd3 outdated
      33 | +#endif
      34 | +
      35 | +
      36 | +namespace {
      37 | +
      38 | +using vec256 = uint32_t __attribute__((__vector_size__(32)));
    


    ajtowns commented at 2:03 AM on December 17, 2025:

    static_assert(sizeof(vec256) == 32) ? (Or sizeof(vec256) == 32 || ALL_MULTI_STATES_DISABLED)


    l0rinc commented at 3:39 PM on December 17, 2025:

    I haven't used this before but the internets tells me we could use the attribute syntax here, something like:

    template <typename T, size_t N>
    using VectorType [[gnu::vector_size(sizeof(T) * N)]] = T;
    
    using vec256 = VectorType<uint32_t, 8>;
    

    theuni commented at 5:03 PM on December 17, 2025:

    Because they're compiler-specific, the code makes no assumptions about the size/structure/alignment of vec256. The only accesses are via operator[]. So afaik, there's no need to check for this.


    ajtowns commented at 1:22 AM on December 18, 2025:

    Sorry, I meant just so you get an error if you're using a compiler ignores the __attribute__ entirely but still passes the #if in _base.cpp.

  7. in src/crypto/chacha_vec_impl.h:101 in 3bddf59cd3 outdated
      96 | +
      97 | +    x += y;
      98 | +    z ^= x;
      99 | +    vec_rotl<BITS>(z);
     100 | +
     101 | +    if constexpr(ITER + 1 < I ) arr_add_xor_rot<BITS, I, ITER + 1>(arr0, arr1, arr2);
    


    ajtowns commented at 2:04 AM on December 17, 2025:

    if constexpr (ITER + 1 < I) (we have a space after if constexpr elsewhere)


    l0rinc commented at 12:27 AM on August 30, 2026:

    That's fine in a draft I guess, but in the final version I'd also appreciate a reformat - my OCD keeps flaring up for these :p

  8. ajtowns commented at 2:07 AM on December 17, 2025: contributor

    Some nits. Approach looks very nice, and at least going by the bench results, gives a good improvement.

  9. l0rinc commented at 8:11 AM on December 17, 2025: contributor

    Looking forward to reviewing it - quick question before I do: was it tested on any big-endian systems which don't revert to non-vectorized run?

  10. in src/crypto/chacha_vec_impl.h:121 in 3bddf59cd3 outdated
     116 | +
     117 | +After the first round, arr_shuf0, arr_shuf1, and arr_shuf2 are used to shuffle
     118 | +the layout to prepare for the second round.
     119 | +
     120 | +After the second round, they are used (in reverse) to restore the original
     121 | +layout.
    


    ajtowns commented at 8:48 AM on December 17, 2025:

    My understanding is that you're inverting the logic here: instead of

    #define QUARTERROUND(a,b,c,d) \
        X(a,b,d,16) X(c,d,b,12) X(a,d,b,8) X(c,d,b,7)
    

    This does the X(a,b,d,16) all four times via simd, then X(c,d,b,12) all four times, etc. And the simd part relies on the data for those four steps being exactly +4 units apart, which is why the shuffling and unshuffling is necessary for the second round. (Okay, not four times but eight times because each vec256 is two blocks)

    Could use a little more explanation in the comment I think? But makes sense to me.


    sedited commented at 1:49 PM on December 28, 2025:

    Makes sense to me too, but I'm not sure why doing it this way is preferable. Intuitively I would have expected that the vectors hold the same word spread over multiple blocks. But if I understand your approach here, we have multiple words over two blocks. Is it just easier to express the quarter rounds in this way?

  11. in src/crypto/chacha20.cpp:294 in 3bddf59cd3 outdated
     291 | +    assert(in_bytes.size() == out_bytes.size());
     292 | +    size_t blocks = out_bytes.size() / ChaCha20Aligned::BLOCKLEN;
     293 | +    assert(blocks * ChaCha20Aligned::BLOCKLEN == out_bytes.size());
     294 | +#ifdef ENABLE_CHACHA20_VEC
     295 | +    // Only use the vectorized implementations if the counter will not overflow.
     296 | +    const bool overflow = static_cast<uint64_t>(input[8]) + blocks > std::numeric_limits<uint32_t>::max();
    


    ajtowns commented at 8:58 AM on December 17, 2025:

    Overflow happens every 274GB I guess, which presumably isn't worth putting much effort in to optimising around.


    theuni commented at 5:10 PM on December 17, 2025:

    This is actually doing the opposite.. it's keeping the vectorized impl from having to worry about this case. In the current code, we have:

    ++j12;
    if (!j12) ++j13;
    ...
    input[8] = j12;
    input[9] = j13;
    

    So effectively input[8] and input[9] are treated as a single uint64_t. It's not possible to express "cast to uint64_t elements and increment" or "increment and overflow over there" with the vector extensions, so it turned out to be easier to just forbid the overflow cases from being vectorized at all.


    ajtowns commented at 1:23 AM on December 18, 2025:

    Yeah, I was thinking about whether the code should just do the non-vectorized stuff to get past the overflow then immediately go back to vectorizing, rather than waiting for the next call. I think what you've got makes sense.


    l0rinc commented at 5:43 PM on May 4, 2026:

    If you decide to keep it, consider subtracting from max instead:

        const bool overflow = blocks > std::numeric_limits<uint32_t>::max() - input[8];
    
  12. in src/crypto/chacha20_vec.ipp:46 in 3bddf59cd3 outdated
      41 | +ALWAYS_INLINE void vec_byteswap(vec256& vec)
      42 | +{
      43 | +    if constexpr (std::endian::native == std::endian::big)
      44 | +    {
      45 | +        vec256 ret;
      46 | +        ret[0] = __builtin_bswap32(vec[0]);
    


    l0rinc commented at 9:24 AM on December 17, 2025:

    can we assume that all big endian systems have __builtin_bswap32 available? https://github.com/bitcoin/bitcoin/blob/432b18ca8d0654318a8d882b28b20af2cb2d2e5d/src/compat/byteswap.h#L14-L16 indicates we could use our existing helpers here instead:

    ALWAYS_INLINE void vec_byteswap(vec256& vec)
    {
        if constexpr (std::endian::native == std::endian::big) {
            for (size_t i = 0; i < 8; ++i) {
                vec[i] = internal_bswap_32(vec[i]);
            }
        }
    }
    

    A small, fixed-size loop like this should be unrolled by every compiler, it's what we did in https://github.com/bitcoin/bitcoin/pull/31144/files#diff-f26d4597a7f5a5d4aa30053032d49427b402ef4fd7a1b3194cb75b2551d58ca4R48 as well.

    (nit: could you please reformat the patch, it differs slightly from how clang-format is set for new code)


    theuni commented at 5:12 PM on December 17, 2025:

    Thanks, yes, I meant to change that to internal_bswap_32 before pushing. Will do.


    l0rinc commented at 11:24 PM on August 29, 2026:

    This still stands: vec_byteswap duplicates the host-endian branch around raw __builtin_bswap32 calls, while compat/endian.h already provides the same lane conversion through htole32_internal.

    <details><summary>reuse endian helper for ChaCha20</summary>

    diff --git a/src/crypto/chacha20_vec_128impl.h b/src/crypto/chacha20_vec_128impl.h
    index 92b0148d75..85b4e77395 100644
    --- a/src/crypto/chacha20_vec_128impl.h
    +++ b/src/crypto/chacha20_vec_128impl.h
    @@ -6,11 +6,11 @@
     #define BITCOIN_CRYPTO_CHACHA20_VEC_128IMPL_H
     
     #include <attributes.h>
    +#include <compat/endian.h>
     #include <crypto/chacha20_vec.h>
     
     #include <algorithm>
     #include <array>
    -#include <bit>
     #include <cassert>
     #include <cstdint>
     #include <cstring>
    @@ -22,18 +22,10 @@ static constexpr size_t BLOCKLEN = 64;
     
     using vec128 = uint32_t __attribute__((__vector_size__(16)));
     
    -/** Endian-conversion for big-endian */
    +/** Convert every lane to little-endian byte order (a no-op on little-endian hosts) */
     ALWAYS_INLINE void vec_byteswap(vec128& vec)
     {
    -    if constexpr (std::endian::native == std::endian::big)
    -    {
    -        vec128 ret;
    -        ret[0] = __builtin_bswap32(vec[0]);
    -        ret[1] = __builtin_bswap32(vec[1]);
    -        ret[2] = __builtin_bswap32(vec[2]);
    -        ret[3] = __builtin_bswap32(vec[3]);
    -        vec = ret;
    -    }
    +    vec = vec128{htole32_internal(vec[0]), htole32_internal(vec[1]), htole32_internal(vec[2]), htole32_internal(vec[3])};
     }
     
     /** Left-rotate vector */
    

    </details>

    But we could go a step futher and just inline it and use ReadLE32 and WriteLE32 which already express the required unaligned endian conversion.

    <details><summary>reuse ChaCha20 endian I/O </summary>

    diff --git a/src/crypto/chacha20_vec_128impl.h b/src/crypto/chacha20_vec_128impl.h
    index 92b0148d75..988174d02c 100644
    --- a/src/crypto/chacha20_vec_128impl.h
    +++ b/src/crypto/chacha20_vec_128impl.h
    @@ -6,14 +6,13 @@
     #define BITCOIN_CRYPTO_CHACHA20_VEC_128IMPL_H
     
     #include <attributes.h>
    +#include <crypto/common.h>
     #include <crypto/chacha20_vec.h>
     
     #include <algorithm>
     #include <array>
    -#include <bit>
     #include <cassert>
     #include <cstdint>
    -#include <cstring>
     #include <span>
     
     namespace chacha20_vec128 {
    @@ -22,20 +21,6 @@ static constexpr size_t BLOCKLEN = 64;
     
     using vec128 = uint32_t __attribute__((__vector_size__(16)));
     
    -/** Endian-conversion for big-endian */
    -ALWAYS_INLINE void vec_byteswap(vec128& vec)
    -{
    -    if constexpr (std::endian::native == std::endian::big)
    -    {
    -        vec128 ret;
    -        ret[0] = __builtin_bswap32(vec[0]);
    -        ret[1] = __builtin_bswap32(vec[1]);
    -        ret[2] = __builtin_bswap32(vec[2]);
    -        ret[3] = __builtin_bswap32(vec[3]);
    -        vec = ret;
    -    }
    -}
    -
     /** Left-rotate vector */
     template <size_t BITS>
     ALWAYS_INLINE void vec_rotl(vec128& vec)
    @@ -161,13 +146,11 @@ ALWAYS_INLINE void doubleround(std::array<vec128, I>& arr0, std::array<vec128, I
     /** XOR 16 input bytes with one state row and write them without assuming alignment or vec128's memory layout */
     ALWAYS_INLINE void vec_read_xor_write(std::span<const std::byte, 16> in_bytes, std::span<std::byte, 16> out_bytes, const vec128& vec)
     {
    -    std::array<uint32_t, 4> temparr;
    -    memcpy(temparr.data(), in_bytes.data(), in_bytes.size());
    -    vec128 tempvec = vec;
    -    vec_byteswap(tempvec);
    -    tempvec ^= (vec128){temparr[0], temparr[1], temparr[2], temparr[3]};
    -    temparr = {tempvec[0], tempvec[1], tempvec[2], tempvec[3]};
    -    memcpy(out_bytes.data(), temparr.data(), out_bytes.size());
    +    const vec128 result{vec ^ vec128{ReadLE32(in_bytes.data()), ReadLE32(in_bytes.data() + 4), ReadLE32(in_bytes.data() + 8), ReadLE32(in_bytes.data() + 12)}};
    +    WriteLE32(out_bytes.data(), result[0]);
    +    WriteLE32(out_bytes.data() + 4, result[1]);
    +    WriteLE32(out_bytes.data() + 8, result[2]);
    +    WriteLE32(out_bytes.data() + 12, result[3]);
     }
     
     /** Write each 64-byte state in row order */
    

    </details>

    And while we're here, the implementation indexes vec128 as four 32-bit lanes, but does not verify that the compiler honored the vector_size attribute, let's assert it to be sure.

    <details><summary>verify ChaCha20 vector width</summary>

    diff --git a/src/crypto/chacha20_vec_128impl.h b/src/crypto/chacha20_vec_128impl.h
    index 32dc166952..9545c4d8da 100644
    --- a/src/crypto/chacha20_vec_128impl.h
    +++ b/src/crypto/chacha20_vec_128impl.h
    @@ -20,6 +20,7 @@ namespace chacha20_vec128 {
     static constexpr size_t BLOCKLEN = 64;
     
     using vec128 = uint32_t __attribute__((__vector_size__(16)));
    +static_assert(sizeof(vec128) == 16);
     
     /** Left-rotate vector */
     template <size_t BITS>
    

    </details>

  13. in src/crypto/chacha_vec_impl.h:67 in 3bddf59cd3 outdated
      62 | +    vec = (vec << BITS) | (vec >> (32 - BITS));
      63 | +}
      64 | +
      65 | +/** Store a vector in all array elements */
      66 | +template <size_t I, size_t ITER = 0>
      67 | +ALWAYS_INLINE void arr_set_vec256(std::array<vec256, I>& arr, const vec256& vec)
    


    l0rinc commented at 9:25 AM on December 17, 2025:

    It seems to me the existing standard test vectors are too short (mostly < 128 bytes) to trigger the vectorized optimization - can we extend the tests to runa and compare the new specializations (could show as skipped when the given architecture isn't available locally)?


    theuni commented at 5:12 PM on December 17, 2025:

    Yep, will do.

  14. in src/crypto/chacha20_vec_base.cpp:23 in 3bddf59cd3 outdated
      18 | +// Be conservative and require platforms to opt-in
      19 | +#  define CHACHA20_VEC_DISABLE_STATES_16
      20 | +#  define CHACHA20_VEC_DISABLE_STATES_8
      21 | +#  define CHACHA20_VEC_DISABLE_STATES_6
      22 | +#  define CHACHA20_VEC_DISABLE_STATES_4
      23 | +#  define CHACHA20_VEC_DISABLE_STATES_2
    


    l0rinc commented at 2:33 PM on December 17, 2025:

    Forcing vectorized operations on an emulated big-endian system (otherwise it falls back to the scalar implementation due to the safety checks):

    diff --git a/src/crypto/chacha20_vec_base.cpp b/src/crypto/chacha20_vec_base.cpp
    index 9fda9452a1..a2aa1e5552 100644
    --- a/src/crypto/chacha20_vec_base.cpp
    +++ b/src/crypto/chacha20_vec_base.cpp
    @@ -20,7 +20,7 @@
     #  define CHACHA20_VEC_DISABLE_STATES_8
     #  define CHACHA20_VEC_DISABLE_STATES_6
     #  define CHACHA20_VEC_DISABLE_STATES_4
    -#  define CHACHA20_VEC_DISABLE_STATES_2
    +//#  define CHACHA20_VEC_DISABLE_STATES_2
     #endif
    
     #include <crypto/chacha20_vec.ipp>
    

    and running the crypto_tests:

    brew install podman pigz qemu
    podman machine init
    podman machine start
    
    podman run --platform linux/s390x -it --rm ubuntu:latest /bin/bash -c \
      'apt-get update && \
       DEBIAN_FRONTEND=noninteractive apt-get install -y \
       git build-essential cmake ccache pkg-config \
       libevent-dev libboost-dev libssl-dev libsqlite3-dev python3 && \
       git clone https://github.com/bitcoin/bitcoin.git && cd bitcoin && \
       git fetch origin pull/34083/head:chacha20-vec && git checkout chacha20-vec && \
       sed -i "s/#  define CHACHA20_VEC_DISABLE_STATES_2/\/\/#  define CHACHA20_VEC_DISABLE_STATES_2/" src/crypto/chacha20_vec_base.cpp && \
       cmake -B build -DBUILD_BENCH=OFF -DBUILD_GUI=OFF -DBUILD_DAEMON=OFF -DBUILD_TX=OFF -DENABLE_IPC=OFF -DENABLE_WALLET=OFF -DENABLE_ZMQ=OFF -DENABLE_UPNP=OFF -DENABLE_NATPMP=OFF && \
       cmake --build build --target test_bitcoin -j1 && \ ./build/bin/test_bitcoin --run_test=crypto_tests'
    

    We're getting a lot of failures:

    Running 17 test cases...
    ./test/crypto_tests.cpp(155): error: in "crypto_tests/chacha20_testvector": check hexout == HexStr(outres) has failed [a3fbf07df3fa2fde4f376ca23e82737041605d9f4f4f57bd8cff2c1d4b7955ec2a97948bd3722915c8f3d337f7d370050e9e96d647b7c39f56e031ca5eb6250d4042e02785ececfa4b4bb5e8ead0440e20b6e8db09d881a7c6132f420e52795042bdfa7773d8a9051447b3291ce1411c680465552aa6c405b7764d5e87bea85ad00f8449ed8f72d0d662ab052691ca66424bc86d2df80ea41f43abf937d3259dc4b2d0dfb48a6c9139ddd7f76966e928e635553ba76c5c879d7b35d49eb2e62b0871cdac638939e25e8a1e0ef9d5280fa8ca328b351c3c765989cbcf3daa8b6ccc3aaf9f3979c92b3720fc88dc95ed84a1be059c6499b9fda236e7e818b04b0bc39c1e876b193bfe5569753f88128cc08aaa9b63d1a16f80ef2554d7189c411f5869ca52c5b83fa36ff216b9c1d30062bebcfd2dc5bce0911934fda79a86f6e698ced759c3ff9b6477338f3da4f9cd8514ea9982ccafb341b2384dd902f3d1ab7ac61dd29c6f21ba5b862f3730e37cfdc4fd806c22f221 != c2ece71ceded38c04f376ca225cc3d6b463409986f263e9db1994a204b6844ec6e9695cfc52b7003e3b6961ceac96a18138981cb4ee5919649b263d542b82b114a57f52d8ba2a2f4540af4f7f49f0b1072a7f9891b97ceb5c61c20420143685f169add2373c4b505122eda2f5df3535d555637680dc5a722b83209519ae7f147c11a9158f48479c9926ea7412ac69d6a584ac97768e412e1514fa7b737ce389dc4bbd9df9cc422b95ccfc5926171fe20e9284834a77646878d7a34c485b3e7300c3589a8448b3bc53b980c6bced42938afc1398c2f1a3a6c2887c5be68a18039cb2fba983271c1202a73af95c79ae29faafb40973694b4afa523f2ef13b84300c39c1e876b193bfe5569753f88128cc08aaa9b63d1a16f80ef2554d7189c411f5869ca52c5b83fa36ff216b9c1d30062bebcfd2dc5bce0911934fda79a86f6e698ced759c3ff9b6477338f3da4f9cd8514ea9982ccafb341b2384dd902f3d1ab7ac61dd29c6f21ba5b862f3730e37cfdc4fd806c22f221]
    ...
    ./test/crypto_tests.cpp(185): error: in "crypto_tests/chacha20_testvector": check hexout == HexStr(outres) has failed [a3fbf07df3fa2fde4f376ca23e82737041605d9f4f4f57bd8cff2c1d4b7955ec2a97948bd3722915c8f3d337f7d370050e9e96d647b7c39f56e031ca5eb6250d4042e02785ececfa4b4bb5e8ead0440e20b6e8db09d881a7c6132f420e52795042bdfa7773d8a9051447b3291ce1411c680465552aa6c405b7764d5e87bea85ad00f8449ed8f72d0d662ab052691ca66424bc86d2df80ea41f43abf937d3259dc4b2d0dfb48a6c9139ddd7f76966e928e635553ba76c5c879d7b35d49eb2e62b0871cdac638939e25e8a1e0ef9d5280fa8ca328b351c3c765989cbcf3daa8b6ccc3aaf9f3979c92b3720fc88dc95ed84a1be059c6499b9fda236e7e818b04b0bc39c1e876b193bfe5569753f88128cc08aaa9b63d1a16f80ef2554d7189c411f5869ca52c5b83fa36ff216b9c1d30062bebcfd2dc5bce0911934fda79a86f6e698ced759c3ff9b6477338f3da4f9cd8514ea9982ccafb341b2384dd902f3d1ab7ac61dd29c6f21ba5b862f3730e37cfdc4fd806c22f221 != a3fbf07df3fa2fde4f376ca23e82737041605d9f4f4f57bd8cff2c1d4b7955ec2a97948bd3722915c8f3d337f7d370050e9e96d647b7c39f56e031ca5eb6250d4a57f52d8ba2a2f4540af4f7f49f0b1072a7f9891b97ceb5c61c20420143685f169add2373c4b505122eda2f5df3535d555637680dc5a722b83209519ae7f147c11a9158f48479c9926ea7412ac69d6a584ac97768e412e1514fa7b737ce389dc4bbd9df9cc422b95ccfc5926171fe20e9284834a77646878d7a34c485b3e7300871cdac638939e25e8a1e0ef9d5280fa8ca328b351c3c765989cbcf3daa8b6ccc3aaf9f3979c92b3720fc88dc95ed84a1be059c6499b9fda236e7e818b04b0bc39c1e876b193bfe5569753f88128cc08aaa9b63d1a16f80ef2554d7189c411f5869ca52c5b83fa36ff216b9c1d30062bebcfd2dc5bce0911934fda79a86f6e698ced759c3ff9b6477338f3da4f9cd8514ea9982ccafb341b2384dd902f3d1ab7ac61dd29c6f21ba5b862f3730e37cfdc4fd806c22f221]
    ./test/crypto_tests.cpp(272): error: in "crypto_tests/chacha20poly1305_testvectors": check cipher == expected_cipher has failed
    ...
    ./test/crypto_tests.cpp(337): error: in "crypto_tests/chacha20poly1305_testvectors": check decipher == plain has failed
    
    *** 37 failures are detected in the test module "Bitcoin Core Test Suite"
    

    This appears to be an endianness issue in vec_read_xor_write: the current implementation swaps the result of the XOR, but on Big Endian systems we must swap the state vec before the XOR.

    Changing it to:

    diff --git a/src/crypto/chacha20_vec.ipp b/src/crypto/chacha20_vec.ipp
    index 46a159ce01..cfc0535a92 100644
    --- a/src/crypto/chacha20_vec.ipp
    +++ b/src/crypto/chacha20_vec.ipp
    @@ -174,8 +174,9 @@ ALWAYS_INLINE void vec_read_xor_write(std::span<const std::byte, 32> in_bytes, s
     {
         std::array<uint32_t, 8> temparr;
         memcpy(temparr.data(), in_bytes.data(), in_bytes.size());
    -    vec256 tempvec = vec ^ (vec256){temparr[0], temparr[1], temparr[2], temparr[3], temparr[4], temparr[5], temparr[6], temparr[7]};
    +    vec256 tempvec = vec;
         vec_byteswap(tempvec);
    +    tempvec ^= (vec256){temparr[0], temparr[1], temparr[2], temparr[3], temparr[4], temparr[5], temparr[6], temparr[7]};
         temparr = {tempvec[0], tempvec[1], tempvec[2], tempvec[3], tempvec[4], tempvec[5], tempvec[6], tempvec[7]};
         memcpy(out_bytes.data(), temparr.data(), out_bytes.size());
     }
    

    Makes it pass for me.

    We should find a way to exercise this via CI and benchmarks, maybe similarly to SHA256AutoDetect in https://github.com/bitcoin/bitcoin/blob/bdb8eadcdc193f398ebad83911d3297b5257e721/src/crypto/sha256.cpp#L585-L690 which would enable us running benchmarks and tests selectively.


    theuni commented at 5:17 PM on December 17, 2025:

    Thanks for catching this! I forgot to mention in the PR description that big-endian was best-effort and untested. I figured our c-i would catch any obvious bugs. Agree it's not great that it didn't :(

    Thanks for the quick fix too :)


    l0rinc commented at 5:22 PM on December 17, 2025:

    I had the same disappointment when implementing the obfuscation optimization. :) @maflcko do we have a big-endian nightly that supports vectorized operations? Would it have caught this?


    maflcko commented at 5:50 PM on December 17, 2025:

    See #33436, but it was slow despite having the gui and the fuzz tests disabled. I am running it as part of nightly, so any issues will be caught before a release, but I am not sure if catching everything in pull requests is possible.

    Maybe it is possible to split the task into two: One for a cross-compile, which should be faster than a "native" compile via qemu. And another to run the tests, similar to the windows-cross tests.


    maflcko commented at 6:13 AM on December 18, 2025:

    Actually, I ran the CI config locally, but it didn't catch this, as the platform opts out. The CI doesn't run the sed -i "s/# define CHACHA20_VEC_DISABLE_STATES_2/\/\/# define CHACHA20_VEC_DISABLE_STATES_2/" src/crypto/chacha20_vec_base.cpp && \ portion, so it would not have caught this, even if the task was run.


    l0rinc commented at 11:47 AM on December 18, 2025:

    Thanks for checking @maflcko, that's why I asked. The vectorized operations are obviously supported, but for some reason were not triggered for me either. @theuni, is this just an emulation anomaly or we were just too cautious? I understdood that @achow101 has access to real big-endian power9 machine that we might be able to test this on when it's ready.


    theuni commented at 3:22 PM on December 18, 2025:

    @l0rinc As the code is written at the moment, all platforms must opt-in to vectorization as opposed to opting out. I'm not sure that's the best approach, but I figured that was a reasonable starting point.

    My reasoning for that was: consider non-x86, non-arm+neon platforms. For the most part, I'm assuming they're under-powered. Enabling (for example) 4x blocks/sec for mipsel would probably cause a drastic slowdown. Obviously there are lots of other powerful platforms, but I figured those would be added over time.

    So if there's a big-endian architecture (or more ideally, a specific instruction set ala __ARM_NEON) that demonstrates a performance gain from calculating multiple states at once, it should be added here.

    Of course, we could go the opposite direction and opt all platforms IN to all states, and instead opt-out the slow ones.

    tl;dr: If we want a big-endian platform to be supported, we need to opt one in or provide an override.

  15. in src/crypto/chacha_vec_impl.h:102 in 3bddf59cd3 outdated
      97 | +    x += y;
      98 | +    z ^= x;
      99 | +    vec_rotl<BITS>(z);
     100 | +
     101 | +    if constexpr(ITER + 1 < I ) arr_add_xor_rot<BITS, I, ITER + 1>(arr0, arr1, arr2);
     102 | +}
    


    l0rinc commented at 3:17 PM on December 17, 2025:

    Hmmm, it seems to me we're doing heavy recursive templates here to unroll loops. My understanding (and experience with the mentioned Obfuscation PR) is that modern compilers are good at unrolling fixed-bound loops. Can you please try if this also works and results in the same speedup?

    template <size_t BITS, size_t I>
    ALWAYS_INLINE void arr_add_xor_rot(std::array<vec256, I>& arr0, const std::array<vec256, I>& arr1, std::array<vec256, I>& arr2)
    {
        for (size_t i{0}; i < I; ++i) {
            arr0[i] += arr1[i];
            arr2[i] ^= arr0[i];
            vec_rotl<BITS>(arr2[i]);
        }
    }
    

    (nit: the size is often N instead of I)


    sipa commented at 4:10 PM on December 17, 2025:

    Alternatively, with a compile-time loop:

    /** Perform add/xor/rotate for the round function */
    template <size_t BITS, size_t I>
    ALWAYS_INLINE void arr_add_xor_rot(std::array<vec256, I>& arr0, const std::array<vec256, I>& arr1, std::array<vec256, I>& arr2)
    {
        [&]<size_t... ITER>(std::index_sequence<ITER...>) {
            ((
                arr0[ITER] += arr1[ITER],
                arr2[ITER] ^= arr0[ITER],
                vec_rotl<BITS>(arr2[ITER])
            ), ...);
        }(std::make_index_sequence<I>());
    }
    

    theuni commented at 5:25 PM on December 17, 2025:

    See updated title description where I touched on this.

    I found (with lots of experimentation here) that different compilers use complicated and unpredictable heuristics to decide whether or not to unroll loops. Even if an unroll-able loop is detected, unrolling may be skipped because of the function size (as is the case here because it's huge).

    So, I'd like to not take chances on unrolling. That means one of:

    • Manual unrolling
    • Macro-based (as-in REPEAT10())
    • A pragma
    • Recursive template based

    c++26 introduces template for, which is what we really want.

    I'm up for whichever of those is generally preferred, as long as it's explicit rather than implicit.


    l0rinc commented at 5:30 PM on December 17, 2025:

    Is there any way we could help that would make you reconsider? I don't mind running the benchmarks on a few platforms, as long as we can have simpler code. The current template magic is not something I would like to see more of - modern C++20 should be able to handle the situations we have here. I don't mind experimenting with this if you don't want to.


    sipa commented at 5:41 PM on December 17, 2025:

    @theuni See my std::make_index_sequence based I demonstrated above. It's template based, but doesn't need recursion, and doesn't rely on compiler unrolling. It simply expands to an expression (a[0] = v, a[1] = v, a[2] = v, ...) e.g.


    l0rinc commented at 5:45 PM on December 17, 2025:

    https://www.agner.org/optimize/optimizing_cpp.pdf contains a few useful examples:

    The automatic vectorization works best if the following conditions are satisfied:

    1. Use a compiler with good support for automatic vectorization, such as Gnu, Clang, or Intel.

    2. Use the latest version of the compiler. The compilers are becoming better and better at vectorization.

    3. If the arrays or structures are accessed through pointers or references then tell the compiler explicitly that pointers do not alias, if appropriate, using the __restrict or restrict keyword.

    4. Use appropriate compiler options to enable the desired instruction set (/arch:SSE2, /arch:AVX etc. for Windows, -msse2, -mavx512f, etc. for Linux)

    5. Use the less restrictive floating point options. For Gnu and Clang compilers, use the options -O2 -fno-trapping-math -fno-math-errno -fno-signed-zeros (-ffast-math works as well, but functions like isnan(x) do not work under -ffast-math).

    6. Align arrays and big structures by 16 for SSE2, preferably 32 for AVX and preferably 64 for AVX512.

    7. The loop count should preferably be a constant that is divisible by the number of elements in a vector.

    8. If arrays are accessed through pointers so that the alignment is not visible in the scope of the function where you want vectorization then follow the advice given above.

    9. Minimize the use of branches at the vector element level.

    10. Avoid table lookup at the vector element level.

    Similarly in https://en.algorithmica.org/hpc/simd/auto-vectorization useful hints:

    The other way, specific to SIMD, is the “ignore vector dependencies” pragma. It is a general way to inform the compiler that there are no dependencies between the loop iterations:

    #pragma GCC ivdep for (int i = 0; i < n; i++)

  16. in src/crypto/chacha_vec_impl.h:33 in 3bddf59cd3 outdated
      28 | +#  endif
      29 | +#endif
      30 | +
      31 | +#if !defined(ALWAYS_INLINE)
      32 | +#  define ALWAYS_INLINE inline
      33 | +#endif
    


  17. in src/crypto/chacha20_vec.ipp:309 in 3bddf59cd3 outdated
     304 | +    while(in_bytes.size() >= CHACHA20_VEC_BLOCKLEN * 8) {
     305 | +        multi_block_crypt<8>(in_bytes, out_bytes, state0, state1, state2);
     306 | +        state2 += (vec256){8, 0, 0, 0, 8, 0, 0, 0};
     307 | +        in_bytes = in_bytes.subspan(CHACHA20_VEC_BLOCKLEN * 8);
     308 | +        out_bytes = out_bytes.subspan(CHACHA20_VEC_BLOCKLEN * 8);
     309 | +    }
    


    l0rinc commented at 3:25 PM on December 17, 2025:

    There's a lot of repetition here - could we extract that to an ALWAYS_INLINE lambda and have something like:

        if constexpr (ENABLE_16) process_blocks(16);
        else if constexpr (ENABLE_8)  process_blocks(8);
    ...
    

    I haven't implemented it locally, maybe it's naive, let me know what you think.

  18. l0rinc changes_requested
  19. l0rinc commented at 3:46 PM on December 17, 2025: contributor

    I went through the code roughly, I like that we're focusing on this and looking forward to specializing other parts of the code that are this critical (looking at you, SipHash!).

    My biggest objection currently is that it's broken on big-endian systems - left a suggestion how to reproduce and fix it. This also reveals the lack of testing - we have to find a way to selectively enable and disable these optimizations to make sure we have tests that compare their outputs. I agree with AJ that we could modernize this a bit with constexpr and less general recursive template magic since C++20 allows us to use simple loops and constexpr conditions and lambdas - it could reduce a lot of duplication while maintaining performance. There's also some repetition (e.g. ALWAYS_INLINE and internal_bswap_32), already defined elsewhere which I think we could use instead. Which leads me to think we should extract the new primitives used here (__builtin_shufflevector, vec_rotl, vec_byteswap, vec256) to a reusable header - tested and benchmarked separately from the chacha work.

  20. in src/crypto/chacha_vec_impl.h:70 in 3bddf59cd3 outdated
      65 | +/** Store a vector in all array elements */
      66 | +template <size_t I, size_t ITER = 0>
      67 | +ALWAYS_INLINE void arr_set_vec256(std::array<vec256, I>& arr, const vec256& vec)
      68 | +{
      69 | +    std::get<ITER>(arr) = vec;
      70 | +    if constexpr(ITER + 1 < I ) arr_set_vec256<I, ITER + 1>(arr, vec);
    


    sipa commented at 3:59 PM on December 17, 2025:

    I believe it's possible to use compile-time loops here instead of recursive templates (if runtime loops don't get optimized sufficiently):

    /** Store a vector in all array elements */
    template <size_t I>
    ALWAYS_INLINE void arr_set_vec256(std::array<vec256, I>& arr, const vec256& vec)
    {
        [&]<size_t... ITER>(std::index_sequence<ITER...>) {
            ((std::get<ITER>(arr) = vec),...);
        }(std::make_index_sequence<I>());
    }
    

    theuni commented at 5:41 PM on December 17, 2025:

    I avoided lambdas as I've historically observed lots of optims being skipped (with clang, at least) when using them. But since you/@ajtowns/@l0rinc have all made the same comment, I'll play around and see if these indeed compile down to nothing as one would hope.


    sipa commented at 5:55 PM on December 17, 2025:

    It can be done with a helper function too, but that's not as concise:

    template <size_t I, size_t... ITER>
    ALWAYS_INLINE void arr_set_vec256_inner(std::array<vec256, I>& arr, const vec256& vec, std::index_sequence<ITER...>)
    {
        ((std::get<ITER>(arr) = vec),...);
    }
    
    /** Store a vector in all array elements */
    template <size_t I>
    ALWAYS_INLINE void arr_set_vec256(std::array<vec256, I>& arr, const vec256& vec)
    {
        arr_set_vec256_inner(arr, vec, std::make_index_sequence<I>());
    }
    

    ajtowns commented at 1:58 AM on December 18, 2025:

    Writing this as:

        template<size_t... ITER> using iseq = std::index_sequence<ITER>;
        static constexpr ISEQ = std::make_index_sequence<I>();
        
        template<size_t... ITER>
        ALWAYS_INLINE void arr_set_vec256(iseq<ITER>, std::array<vec256, I>& arr, const vec256& vec)
        {
            ((std::get<ITER>(arr) = vec), ...);
        }
    
       ...
            arr_set_vec256(ISEQ, arr0, num256);
    

    doesn't seem too bad, and avoids lambdas? Possibly still a bit clumsy if you can't capture I by putting everything in a class? Might still be a bit clumsy for add_xor_rot.


    sedited commented at 11:04 PM on December 24, 2025:

    Is there a particular reason for the aversion to the current approach? It seems easier to read and probably also to debug to me.

  21. theuni commented at 5:48 PM on December 17, 2025: member

    An additional note about the actual vectorizing algorithm itself...

    There's a different (and arguably more obvious) algorithm that's possible when calculating exactly 8 states. Rather than loading each vec256 with partial info from 2 states, it's possible to use 16 vec256 where each vector contains 1 element for each of the 8 states. It looks like:

    vec256 x0, x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13, x14, x15;
    
    broadcast(x0, 0x61707865);
    broadcast(x1, 0x3320646e);
    broadcast(x2, 0x79622d32);
    broadcast(x3, 0x6b206574);
    broadcast(x4, input[0]);
    ...
    broadcast(x15, input[11]);
    
    QUARTERROUND( x0, x4, x8,x12);
    QUARTERROUND( x1, x5, x9,x13);
    ...
    
    vec256 j0, j1, j2, j3, ... j31;
    extract_column(x0, x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13, x14, x15, 0, j0, j1);
    extract_column(x0, x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13, x14, x15, 1, j2, j3);
    ...
    extract_column(x0, x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13, x14, x15, 15, j30, j31);
    
    vec_read_xor_write(in_bytes, out_bytes, j0);
    vec_read_xor_write(in_bytes, out_bytes, j1);
    ...
    
    

    The problem with this is the overhead of the transpose at the end. My experiments showed (on x86_64+avx2, at least) that this was actually slower than what's currently implemented. It's possible that this approach is better for other architectures, but I left it out of this PR to reduce the initial complexity.

    Edit: This is what the linux kernel does for x86_64+avx2. I grabbed their .S and hacked it into Core to benchmark, only to find that the impl here actually outperformed their hand-written asm. That was neat :)

  22. theuni commented at 9:46 PM on December 17, 2025: member

    Ok, after some experimenting, I am very hesitant to add the helpers as suggested above. For example, consider @ajtowns's seemingly innocuous Repeat function:

    template <size_t REPS, typename Fn>
    ALWAYS_INLINE void Repeat(Fn&& fn)
    {
        if constexpr (REPS > 0) {
            fn();
            Repeat<REPS-1>(std::forward<Fn>(fn));
        }
    }
    

    This causes a 10% slowdown on this branch, but with avx2 the performance is abysmal:

    Baseline(master):

    |                1.38 |      723,275,584.44 | `CHACHA20_1MB`
    

    This PR:

    |                0.71 |    1,408,751,284.34 | `CHACHA20_1MB`
    

    202512-pr34083-templates

    |                0.79 |    1,258,748,451.87 | `CHACHA20_1MB`
    

    This PR + avx2 commits:

    |                0.50 |    1,988,653,922.83 |  `CHACHA20_1MB`
    

    202512-pr34083-templates + avx2 commits:

    |                1.34 |      748,148,324.75 | `CHACHA20_1MB`
    

    The problem in AJ's branch that some functions end up un-inlined. For clang, it's doubleround. For gcc it's arr_read_xor_write and doubleround.

    Both are fixed with:

    diff --git a/src/crypto/chacha20_vec.ipp b/src/crypto/chacha20_vec.ipp
    index 344c68259a5..16d7da45633 100644
    --- a/src/crypto/chacha20_vec.ipp
    +++ b/src/crypto/chacha20_vec.ipp
    @@ -164 +164 @@ public:
    -        Repeat<10>([&]() {
    +        Repeat<10>([&]() __attribute__ ((always_inline)) {
    

    That means, to be safe, every lambda would need that attribute, which is pretty ugly and easy to forget. So I think my aversion to lambdas in this code was somewhat justified.

    Further, clang's inlining docs state that variadic functions aren't inlined. Experimenting now, that's obviously not true as of v21-git. But clearly it was in the recent past.

    So.. I'd really prefer to keep things as simple here as possible. I know the recursive template functions aren't exactly pretty, but I don't think they're THAT bad? Plus, with c++26, it all goes away in favor of template for :)

  23. ajtowns commented at 1:38 AM on December 18, 2025: contributor

    So.. I'd really prefer to keep things as simple here as possible. I know the recursive template functions aren't exactly pretty, but I don't think they're THAT bad? Plus, with c++26, it all goes away in favor of template for :)

    Could we get some comments in the code as to compiler versions (and architecture) that failed to be efficient enough with lambdas, to hopefully avoid future people modernizing the code without checking that these problems have gone away? Presumably will be a long time before we can modernize to template for (which doesn't even seem to be on cppref's compiler support page yet?)...

    (Only thing that I do think is "that bad" about the recursive templates is using ITER and I -- i should be the loop variable, not the size!)

  24. theuni commented at 3:52 PM on December 18, 2025: member

    Could we get some comments in the code as to compiler versions (and architecture) that failed to be efficient enough with lambdas, to hopefully avoid future people modernizing the code without checking that these problems have gone away? Presumably will be a long time before we can modernize to template for (which doesn't even seem to be on cppref's compiler support page yet?)...

    (Only thing that I do think is "that bad" about the recursive templates is using ITER and I -- i should be the loop variable, not the size!)

    Sure, I can definitely add some more context and clean up the wonky variable names.

    Another brain dump after thinking about all this some more last night:

    1. I don't think lambdas specifically are the problem, more specifically, the issue is: can the compiler infer enough about the "callback" function to inline it?

    There are a few considerations there. @sipa's make_index_sequence suggestion is executed immediately, so I imagine that's more likely to be inlined than AJ's Repeat. But still, from a compiler's POV, if it's evaluating: "here's a function call without ALWAYS_INLINE and my function is already huge, should I exclude it from inlining?", imo it'd be reasonable for it to conclude "yes".

    So to be safe, whatever we do, I think we should strive to annotate all functions. And imo, annotating a bunch of lambdas with an inline attribute feels weird.

    (as an aside: There's also the flatten attribute, which could be added to multi_block_crypt as a belt-and-suspenders)

    The index_sequence trick is neat. If that ends up looking cleaner/more obvious than the recursive iteration, that works for me. I'll play around with it.

    1. Not all loops have to be unrolled.

    In my testing, loop unrolling always showed slightly better performance (presumably because calculating multiple blocks is otherwise branch-free), but it's not nearly as performance-critical as the inlining. For example, skipping unrolling of doubleround leads to much smaller code, which I imagine could end up being faster on some architectures.

    Is there any way we could help that would make you reconsider? I don't mind running the benchmarks on a few platforms, as long as we can have simpler code. The current template magic is not something I would like to see more of - modern C++20 should be able to handle the situations we have here.

    My primary goal with this code (and hopefully setting a precedent for other multi-arch simd code... poly1305 is next ;) is to be as explicit to the compiler about what we want as possible. Ideally as portably as possible. So if we want our functions inlined or loops unrolled, we should attempt to communicate those things opposed to leaving them implicit. Unfortunately, modern c++ doesn't have ways to express either of those yet, but we can make our intentions clear enough.

  25. ajtowns commented at 9:38 AM on December 19, 2025: contributor

    And imo, annotating a bunch of lambdas with an inline attribute feels weird.

    You could #define AI __attribute__((always_inline)), then you'd just be adding AI to all the lambdas, which, if nothing else, would be very modern?

    My primary goal with this code (and hopefully setting a precedent for other multi-arch simd code... poly1305 is next ;) is to be as explicit to the compiler about what we want as possible. Ideally as portably as possible.

    The above was kind-of a joke, but, perhaps you could combine it with a clang-tidy plugin that lets CI check that all lambdas in a particular namespace are annotated in that way? That might be both clear to compilers and reasonably friendly to human authors/reviewers?

    (Explicit recursive templates and prohibiting lambdas are fine by me though; that's still a big step up from inline asm)

  26. l0rinc commented at 8:18 AM on December 22, 2025: contributor

    I have measured its effect on IBD and happy to say it produces a measurable speedup.

    <img width="1474" height="846" alt="image" src="https://github.com/user-attachments/assets/a660986e-c752-4670-8dcf-595f6742520d" />

    <details> <summary>IBD | 926619 blocks | dbcache 450 | i7-hdd | x86_64 | Intel(R) Core(TM) i7-7700 CPU @ 3.60GHz | 8 cores | 62Gi RAM | ext4 | HDD</summary>

    COMMITS="938d7aacabd0bb3784bb3e529b1ed06bb2891864 3bddf59cd3f201ecf8d65bb1f6c0cde5c39595e9"; \
    STOP=926619; DBCACHE=450; \
    CC=gcc; CXX=g++; \
    BASE_DIR="/mnt/my_storage"; DATA_DIR="$BASE_DIR/BitcoinData"; LOG_DIR="$BASE_DIR/logs"; \
    (echo ""; for c in $COMMITS; do git fetch -q origin $c && git log -1 --pretty='%h %s' $c || exit 1; done) && \
    (echo "" && echo "IBD | ${STOP} blocks | dbcache ${DBCACHE} | $(hostname) | $(uname -m) | $(lscpu | grep 'Model name' | head -1 | cut -d: -f2 | xargs) | $(nproc) cores | $(free -h | awk '/^Mem:/{print $2}') RAM | $(df -T $BASE_DIR | awk 'NR==2{print $2}') | $(lsblk -no ROTA $(df --output=source $BASE_DIR | tail -1) | grep -q 0 && echo SSD || echo HDD)"; echo "") &&\
    hyperfine \
      --sort command \
      --runs 2 \
      --export-json "$BASE_DIR/ibd-$(sed -E 's/(\w{8})\w+ ?/\1-/g;s/-$//'<<<"$COMMITS")-$STOP-$DBCACHE-$CC.json" \
      --parameter-list COMMIT ${COMMITS// /,} \
      --prepare "killall -9 bitcoind 2>/dev/null; rm -rf $DATA_DIR/*; git checkout {COMMIT}; git clean -fxd; git reset --hard && \
        cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo && ninja -C build bitcoind -j2 && \
        ./build/bin/bitcoind -datadir=$DATA_DIR -stopatheight=1 -printtoconsole=0; sleep 20" \
      --conclude "cp $DATA_DIR/debug.log $LOG_DIR/debug-{COMMIT}-$(date +%s).log && \
                 grep -q 'height=0' $DATA_DIR/debug.log && grep -q 'Disabling script verification at block [#1](/bitcoin-bitcoin/1/)' $DATA_DIR/debug.log && grep -q 'height=$STOP' $DATA_DIR/debug.log" \
      "COMPILER=$CC ./build/bin/bitcoind -datadir=$DATA_DIR -stopatheight=$STOP -dbcache=$DBCACHE -blocksonly -printtoconsole=0"
    

    938d7aacab Merge bitcoin/bitcoin#33657: rest: allow reading partial block data from storage 3bddf59cd3 chacha20: Add generic vectorized chacha20 implementation

    Benchmark 1: COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=926619 -dbcache=450 -blocksonly -printtoconsole=0 (COMMIT = 938d7aacabd0bb3784bb3e529b1ed06bb2891864)
      Time (mean ± σ):     45198.232 s ± 539.231 s    [User: 57185.689 s, System: 4367.881 s]
      Range (min … max):   44816.939 s … 45579.526 s    2 runs
     
    Benchmark 2: COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=926619 -dbcache=450 -blocksonly -printtoconsole=0 (COMMIT = 3bddf59cd3f201ecf8d65bb1f6c0cde5c39595e9)
      Time (mean ± σ):     43699.641 s ± 25.532 s    [User: 57610.736 s, System: 4058.824 s]
      Range (min … max):   43681.587 s … 43717.695 s    2 runs
     
    Relative speed comparison
            1.03 ±  0.01  COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=926619 -dbcache=450 -blocksonly -printtoconsole=0 (COMMIT = 938d7aacabd0bb3784bb3e529b1ed06bb2891864)
            1.00          COMPILER=gcc ./build/bin/bitcoind -datadir=/mnt/my_storage/BitcoinData -stopatheight=926619 -dbcache=450 -blocksonly -printtoconsole=0 (COMMIT = 3bddf59cd3f201ecf8d65bb1f6c0cde5c39595e9)
    

    </details>

  27. ajtowns commented at 7:50 AM on December 24, 2025: contributor

    I have measured its effect on IBD and happy to say it produces a measurable speedup.

    Presumably that indicates this IBD run is compute bound (vs disk or network), and that ChaCha20 is using up maybe 7% of CPU time (or 7% of a core when we're bottlenecked on something single-threaded) prior to this PR. That seems like a lot? Is this due to FastRandomContext, or something else? Or is it just that obfuscating all the block data is a large component of IBD CPU currently? This seems a bit surprising to me.

  28. in src/crypto/chacha20_vec.ipp:288 in 3bddf59cd3 outdated
     283 | +
     284 | +#if defined(CHACHA20_NAMESPACE)
     285 | +namespace CHACHA20_NAMESPACE {
     286 | +#endif
     287 | +
     288 | +void chacha20_crypt_vectorized(std::span<const std::byte>& in_bytes, std::span<std::byte>& out_bytes, const std::array<uint32_t, 12>& input) noexcept
    


    sedited commented at 11:02 PM on December 24, 2025:

    I think this function should get a short description too. Wasn't immediately clear to me that it attempts to peel off larger blocks first before potentially finishing with a few smaller blocks.

  29. theuni commented at 9:18 PM on January 6, 2026: member

    Presumably that indicates this IBD run is compute bound (vs disk or network), and that ChaCha20 is using up maybe 7% of CPU time (or 7% of a core when we're bottlenecked on something single-threaded) prior to this PR. That seems like a lot? Is this due to FastRandomContext, or something else? Or is it just that obfuscating all the block data is a large component of IBD CPU currently? This seems a bit surprising to me.

    Not sure if you missed this in the description, but that's exactly what this is trying to improve:

    Local profiling revealed that chacha20 accounts for a substantial amount of the network thread's time. It's not clear to me if speeding up chacha20 will improve network performance/latency, but it will definitely make it more efficient.

    IBD would indeed be slowed by Chacha20 via single-threaded bip324 handling on the network thread. While profiling my POC multi-process net binary I observed ChaCha20Aligned::Crypt() accounting for 30% of the net thread's cpu time.

    So it's not surprising to me at all that 3x'ing that function (it's only 2x here, avx2/avx512 improve performance even more) speeds up IBD.

  30. theuni commented at 7:34 PM on January 13, 2026: member

    I finally managed to track down the gcc slowdown that @l0rinc mentioned during last week's IRC meeting. The culprit was this gcc bug. Thankfully, it's easily worked around by simply not calling the guilty builtin.

    Also pushed @l0rinc's fix for big-endian.

    Now that gcc/clang are more on par and the impl seems feasible again, I'll address the other feedback.

  31. in src/crypto/chacha20_vec.h:25 in 3bddf59cd3 outdated
      20 | +
      21 | +#ifdef ENABLE_CHACHA20_VEC
      22 | +
      23 | +namespace chacha20_vec_base
      24 | +{
      25 | +    void chacha20_crypt_vectorized(std::span<const std::byte>& in_bytes, std::span<std::byte>& out_bytes, const std::array<uint32_t, 12>& input) noexcept;
    


    l0rinc commented at 5:42 PM on May 4, 2026:

    3bddf59 chacha20: Add generic vectorized chacha20 implementation:

    We could avoid a copy by making it a span instead:

        void chacha20_crypt_vectorized(std::span<const std::byte>& in_bytes, std::span<std::byte>& out_bytes, std::span<const uint32_t, 12> input) noexcept;
    

    l0rinc commented at 9:50 PM on August 29, 2026:

    The vector backend already advances the remaining input and output spans, so the wrapper and extracted scalar helper are unnecessary.

    Could we pass the state as a fixed-extent span, dispatch vector groups at the start of ChaCha20Aligned::Crypt, and let its existing scalar loop process the remainder?

    <details><summary>simplify `ChaCha20Aligned::Crypt`</summary>

    diff --git a/src/crypto/chacha20.cpp b/src/crypto/chacha20.cpp
    index b7500237a5..891a157d57 100644
    --- a/src/crypto/chacha20.cpp
    +++ b/src/crypto/chacha20.cpp
    @@ -170,13 +170,11 @@ static inline void chacha20_crypt(std::span<const std::byte> in_bytes, std::span
         std::byte* c = out_bytes.data();
         size_t blocks = out_bytes.size() / ChaCha20Aligned::BLOCKLEN;
         assert(blocks * ChaCha20Aligned::BLOCKLEN == out_bytes.size());
    -
    +    if (!blocks) return;
     
         uint32_t x0, x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13, x14, x15;
         uint32_t j4, j5, j6, j7, j8, j9, j10, j11, j12, j13, j14, j15;
     
    -    if (!blocks) return;
    -
         j4 = input[0];
         j5 = input[1];
         j6 = input[2];
    @@ -288,20 +286,14 @@ static inline void chacha20_crypt(std::span<const std::byte> in_bytes, std::span
     
     inline void ChaCha20Aligned::Crypt(std::span<const std::byte> in_bytes, std::span<std::byte> out_bytes) noexcept
     {
    -    assert(in_bytes.size() == out_bytes.size());
    -    size_t blocks = out_bytes.size() / ChaCha20Aligned::BLOCKLEN;
    -    assert(blocks * ChaCha20Aligned::BLOCKLEN == out_bytes.size());
     #ifdef ENABLE_CHACHA20_VEC
         // The vectorized implementation cannot increment the first nonce word
    +    const size_t blocks{out_bytes.size() / BLOCKLEN};
         assert(blocks < std::numeric_limits<uint32_t>::max() - input[8]);
    -    const auto state = std::to_array(input);
    -    chacha20_vec::chacha20_crypt_vectorized(in_bytes, out_bytes, state);
    -    const size_t blocks_written = blocks - (out_bytes.size() / ChaCha20Aligned::BLOCKLEN);
    -    input[8] += blocks_written;
    +    chacha20_vec::chacha20_crypt_vectorized(in_bytes, out_bytes, input);
    +    input[8] += blocks - out_bytes.size() / BLOCKLEN;
     #endif
    -    if (in_bytes.size()) {
    -        chacha20_crypt(in_bytes, out_bytes, input);
    -    }
    +    chacha20_crypt(in_bytes, out_bytes, input);
     }
     
     void ChaCha20::Keystream(std::span<std::byte> out) noexcept
    diff --git a/src/crypto/chacha20_vec.cpp b/src/crypto/chacha20_vec.cpp
    index 80f03f99ed..080eaa59e6 100644
    --- a/src/crypto/chacha20_vec.cpp
    +++ b/src/crypto/chacha20_vec.cpp
    @@ -13,7 +13,7 @@ namespace chacha20_vec {
     
     static_assert(BLOCKLEN == chacha20_vec128::BLOCKLEN);
     
    -void chacha20_crypt_vectorized(std::span<const std::byte>& in_bytes, std::span<std::byte>& out_bytes, const std::array<uint32_t, 12>& input) noexcept
    +void chacha20_crypt_vectorized(std::span<const std::byte>& in_bytes, std::span<std::byte>& out_bytes, std::span<const uint32_t, STATE_WORDS> input) noexcept
     {
         assert(in_bytes.size() == out_bytes.size());
         chacha20_vec128::ChaCha20Vectorized crypter(input);
    diff --git a/src/crypto/chacha20_vec.h b/src/crypto/chacha20_vec.h
    index c0c172c0d8..9b58dd6d1c 100644
    --- a/src/crypto/chacha20_vec.h
    +++ b/src/crypto/chacha20_vec.h
    @@ -5,7 +5,6 @@
     #ifndef BITCOIN_CRYPTO_CHACHA20_VEC_H
     #define BITCOIN_CRYPTO_CHACHA20_VEC_H
     
    -#include <array>
     #include <cstdint>
     #include <cstddef>
     #include <span>
    @@ -20,6 +19,7 @@
     
     namespace chacha20_vec {
     static constexpr uint16_t BLOCKLEN{64};
    +static constexpr uint16_t STATE_WORDS{12};
     
     enum class VectorTarget {
         X86_64,
    @@ -32,7 +32,7 @@ constexpr VectorTarget TARGET{VectorTarget::X86_64};
     constexpr VectorTarget TARGET{VectorTarget::AARCH64};
     #endif
     
    -void chacha20_crypt_vectorized(std::span<const std::byte>& in_bytes, std::span<std::byte>& out_bytes, const std::array<uint32_t, 12>& input) noexcept;
    +void chacha20_crypt_vectorized(std::span<const std::byte>& in_bytes, std::span<std::byte>& out_bytes, std::span<const uint32_t, STATE_WORDS> input) noexcept;
     } // namespace chacha20_vec
     
     #endif // ENABLE_CHACHA20_VEC
    diff --git a/src/crypto/chacha20_vec_128impl.h b/src/crypto/chacha20_vec_128impl.h
    index 27f2d41b9f..fd876d3c09 100644
    --- a/src/crypto/chacha20_vec_128impl.h
    +++ b/src/crypto/chacha20_vec_128impl.h
    @@ -249,7 +249,7 @@ class ChaCha20Vectorized
         const vec128 state1;
         vec128 state2;
     public:
    -    ALWAYS_INLINE ChaCha20Vectorized(const std::array<uint32_t, 12>& input) noexcept
    +    ALWAYS_INLINE ChaCha20Vectorized(std::span<const uint32_t, chacha20_vec::STATE_WORDS> input) noexcept
             : state0((vec128){input[0], input[1], input[2], input[3]})
             , state1((vec128){input[4], input[5], input[6], input[7]})
             , state2((vec128){input[8], input[9], input[10], input[11]})
    

    </details>

  32. in src/crypto/chacha20_vec.ipp:301 in 3bddf59cd3
     296 | +    while(in_bytes.size() >= CHACHA20_VEC_BLOCKLEN * 16) {
     297 | +        multi_block_crypt<16>(in_bytes, out_bytes, state0, state1, state2);
     298 | +        state2 += (vec256){16, 0, 0, 0, 16, 0, 0, 0};
     299 | +        in_bytes = in_bytes.subspan(CHACHA20_VEC_BLOCKLEN * 16);
     300 | +        out_bytes = out_bytes.subspan(CHACHA20_VEC_BLOCKLEN * 16);
     301 | +    }
    


    l0rinc commented at 5:45 PM on May 4, 2026:

    3bddf59 chacha20: Add generic vectorized chacha20 implementation:

    We might have mentioned this before but we can reduce duplication by extracting this to something like:

    template <size_t STATES>
    ALWAYS_INLINE void process_blocks(std::span<const std::byte>& in_bytes, std::span<std::byte>& out_bytes, const vec256& state0, const vec256& state1, vec256& state2)
    {
        static constexpr vec256 increment = (vec256){STATES, 0, 0, 0, STATES, 0, 0, 0};
        while (in_bytes.size() >= CHACHA20_VEC_BLOCKLEN * STATES) {
            multi_block_crypt<STATES>(in_bytes, out_bytes, state0, state1, state2);
            state2 += increment;
            in_bytes = in_bytes.subspan(CHACHA20_VEC_BLOCKLEN * STATES);
            out_bytes = out_bytes.subspan(CHACHA20_VEC_BLOCKLEN * STATES);
        }
    }
    

    and use it as e.g.

        process_blocks<16>(in_bytes, out_bytes, state0, state1, state2);
    

    Local benchmarks indicate it retains the performance.

  33. in src/crypto/chacha20_vec.ipp:180 in 3bddf59cd3
     175 | +    std::array<uint32_t, 8> temparr;
     176 | +    memcpy(temparr.data(), in_bytes.data(), in_bytes.size());
     177 | +    vec256 tempvec = vec ^ (vec256){temparr[0], temparr[1], temparr[2], temparr[3], temparr[4], temparr[5], temparr[6], temparr[7]};
     178 | +    vec_byteswap(tempvec);
     179 | +    temparr = {tempvec[0], tempvec[1], tempvec[2], tempvec[3], tempvec[4], tempvec[5], tempvec[6], tempvec[7]};
     180 | +    memcpy(out_bytes.data(), temparr.data(), out_bytes.size());
    


    l0rinc commented at 5:50 PM on May 4, 2026:

    3bddf59 chacha20: Add generic vectorized chacha20 implementation:

    Could we simplify this by using memcpy directly between byte spans and vec256 instead, something like:

        vec256 tempvec;
        memcpy(&tempvec, in_bytes.data(), sizeof(tempvec));
        vec_byteswap(tempvec);
        tempvec ^= vec;
        vec_byteswap(tempvec);
        memcpy(out_bytes.data(), &tempvec, sizeof(tempvec));
    
  34. l0rinc changes_requested
  35. l0rinc commented at 5:52 PM on May 4, 2026: contributor

    I have remeasured it on a few platforms - on RPI5 this is a serious slowdown compared to before:

    Machine Baseline Final Speedup
    Mac M4 Max 1062 MB/s 2172 MB/s 2.05x
    umbrel N150 (GCC 12.2) 116 MB/s 272 MB/s 2.34x
    RPi5 (GCC 15.0.1) 406 MB/s 207 MB/s 0.51x ⚠️
    RPi5 (Clang 22.0.0) 452 MB/s 558 MB/s 1.24x

    RPi5 with GCC shows a regression: the generic vectorized implementation is ~2x slower than scalar on ARM64 with GCC, while Clang handles it fine. The "gcc/clang fix" commit helps x86 (umbrel) but doesn't fix the ARM64/GCC issue. I haven't investigated the source of the problem, just identified it.

    <img width="2084" height="1476" alt="image" src="https://github.com/user-attachments/assets/e5de73f5-d2ea-4ec2-b29c-482da5a5dbac" />


    <details><summary>Mac M4 - AppleClang 21.0.0.21000099</summary>

    for commit in 224120bf1299392deaa59ab71c895a1e6264f205 b9300cc696135d1ff31ffbdf639dc0f99167d49c 96f741441180d14df0519caa3f7c73f818a12dbf 26e9c7b588fcbdc956c4e2b241f4ef7d121d3d79 e6ec033f175031f72e23253bd0821f6fc6d353b2 6db8cf0e28bbaef24cb9e1e3f0c59f118a25619c 63a99f2b31efde3db91349bbf13f3c243c453084 e82c752615badf8a811846122c2c370d645385e9 62a8c487da9721fd1149dc3fefa7fb11292370bf 332fa6e26f293af58fad83ccc896291a68650fae 548791e2b58fb65a52159a18f3765613e11422a3; do \
        git fetch origin $commit >/dev/null 2>&1 && git checkout $commit >/dev/null 2>&1 && echo "" && git log -1 --pretty='%h %s' && \
        rm -rfd build >/dev/null 2>&1 && cmake -B build -DBUILD_BENCH=ON -DCMAKE_BUILD_TYPE=Release >/dev/null 2>&1 && \
        cmake --build build -j$(nproc) >/dev/null 2>&1 && \
        for _ in $(seq 1); do \
          sleep 5; \
          sudo taskpolicy -t 5 -l 5 nice -n -20 ./build/bin/bench_bitcoin -filter='CHACHA20_.*' -min-time=10000; \
        done; \
    done
    
    224120bf12 Merge bitcoin/bitcoin#32394: net: make m_nodes_mutex non-recursive
    
    |             ns/byte |              byte/s |    err% |     total | benchmark
    |--------------------:|--------------------:|--------:|----------:|:----------
    |                0.94 |    1,061,878,902.65 |    0.4% |     11.00 | `CHACHA20_1MB`
    |                0.95 |    1,057,294,303.75 |    0.3% |     10.96 | `CHACHA20_256BYTES`
    |                0.96 |    1,037,370,700.06 |    0.5% |     10.52 | `CHACHA20_64BYTES`
    
    b9300cc696 chacha20: move single-block crypt to inline helper function
    
    |             ns/byte |              byte/s |    err% |     total | benchmark
    |--------------------:|--------------------:|--------:|----------:|:----------
    |                0.88 |    1,131,925,309.10 |    0.3% |     10.95 | `CHACHA20_1MB`
    |                0.89 |    1,123,374,575.47 |    0.4% |     10.89 | `CHACHA20_256BYTES`
    |                0.92 |    1,086,410,862.30 |    0.2% |     11.00 | `CHACHA20_64BYTES`
    
    96f7414411 chacha20: Add generic vectorized chacha20 implementation
    
    |             ns/byte |              byte/s |    err% |     total | benchmark
    |--------------------:|--------------------:|--------:|----------:|:----------
    |                0.46 |    2,190,867,321.66 |    0.0% |     11.01 | `CHACHA20_1MB`
    |                0.60 |    1,664,336,473.57 |    0.1% |     10.99 | `CHACHA20_256BYTES`
    |                0.94 |    1,063,169,379.64 |    0.2% |     10.99 | `CHACHA20_64BYTES`
    
    26e9c7b588 squashme: fix vectorized chacha20 on big endian
    
    |             ns/byte |              byte/s |    err% |     total | benchmark
    |--------------------:|--------------------:|--------:|----------:|:----------
    |                0.46 |    2,175,076,479.08 |    0.1% |     11.00 | `CHACHA20_1MB`
    |                0.61 |    1,644,558,660.17 |    0.1% |     10.94 | `CHACHA20_256BYTES`
    |                0.95 |    1,055,198,073.31 |    0.3% |     10.53 | `CHACHA20_64BYTES`
    
    e6ec033f17 squashme: fix main performance discrepancy between clang and gcc
    
    |             ns/byte |              byte/s |    err% |     total | benchmark
    |--------------------:|--------------------:|--------:|----------:|:----------
    |                0.46 |    2,172,474,228.31 |    0.1% |     11.00 | `CHACHA20_1MB`
    |                0.61 |    1,641,693,934.28 |    0.3% |     10.97 | `CHACHA20_256BYTES`
    |                0.95 |    1,051,868,628.18 |    0.3% |     10.54 | `CHACHA20_64BYTES`
    

    </details>

    <details><summary>Umbrel</summary>

    for compiler in gcc; do \
      if [ "$compiler" = "gcc" ]; then CC=gcc; CXX=g++; COMP_VER=$(gcc -dumpfullversion); \
      else CC=clang; CXX=clang++; COMP_VER=$(clang -dumpversion); fi && \
      echo "> Compiler: $compiler $COMP_VER" && \
      for commit in 224120bf1299392deaa59ab71c895a1e6264f205 b9300cc696135d1ff31ffbdf639dc0f99167d49c 96f741441180d14df0519caa3f7c73f818a12dbf 26e9c7b588fcbdc956c4e2b241f4ef7d121d3d79 e6ec033f175031f72e23253bd0821f6fc6d353b2 6db8cf0e28bbaef24cb9e1e3f0c59f118a25619c 63a99f2b31efde3db91349bbf13f3c243c453084 e82c752615badf8a811846122c2c370d645385e9 62a8c487da9721fd1149dc3fefa7fb11292370bf 332fa6e26f293af58fad83ccc896291a68650fae 548791e2b58fb65a52159a18f3765613e11422a3; do \
        git fetch origin $commit >/dev/null 2>&1 && git checkout $commit >/dev/null 2>&1 && echo "" && git log -1 --pretty='%h %s' && \
        rm -rf build >/dev/null 2>&1 && cmake -B build -DBUILD_BENCH=ON -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=$CC -DCMAKE_CXX_COMPILER=$CXX >/dev/null 2>&1 && \
        cmake --build build -j$(nproc) >/dev/null 2>&1 && \
        for i in 1; do \
          build/bin/bench_bitcoin -filter='CHACHA20_.*' -min-time=10000; \
        done; \
      done; \
    done
    

    </details>

    <details><summary>Umbrel: gcc 12.2.0</summary>

    224120bf12 Merge bitcoin/bitcoin#32394: net: make m_nodes_mutex non-recursive
    
    |             ns/byte |              byte/s |    err% |        ins/byte |        cyc/byte |    IPC |       bra/byte |   miss% |     total | benchmark
    |--------------------:|--------------------:|--------:|----------------:|----------------:|-------:|---------------:|--------:|----------:|:----------
    |                8.60 |      116,311,101.69 |    0.0% |           16.55 |            6.92 |  2.391 |           0.02 |    0.0% |     10.86 | `CHACHA20_1MB`
    |                8.76 |      114,180,117.53 |    0.0% |           17.21 |            7.05 |  2.441 |           0.08 |    0.0% |     10.97 | `CHACHA20_256BYTES`
    |                9.26 |      108,047,834.96 |    0.0% |           19.22 |            7.45 |  2.578 |           0.27 |    0.0% |     11.00 | `CHACHA20_64BYTES`
    
    b9300cc696 chacha20: move single-block crypt to inline helper function
    
    |             ns/byte |              byte/s |    err% |        ins/byte |        cyc/byte |    IPC |       bra/byte |   miss% |     total | benchmark
    |--------------------:|--------------------:|--------:|----------------:|----------------:|-------:|---------------:|--------:|----------:|:----------
    |                8.67 |      115,349,945.71 |    0.0% |           16.75 |            6.98 |  2.401 |           0.02 |    0.0% |     10.87 | `CHACHA20_1MB`
    |                8.81 |      113,493,232.74 |    0.0% |           17.31 |            7.10 |  2.440 |           0.05 |    0.0% |     11.00 | `CHACHA20_256BYTES`
    |                9.22 |      108,508,333.73 |    0.0% |           19.00 |            7.42 |  2.560 |           0.17 |    0.0% |     11.01 | `CHACHA20_64BYTES`
    
    96f7414411 chacha20: Add generic vectorized chacha20 implementation
    
    |             ns/byte |              byte/s |    err% |        ins/byte |        cyc/byte |    IPC |       bra/byte |   miss% |     total | benchmark
    |--------------------:|--------------------:|--------:|----------------:|----------------:|-------:|---------------:|--------:|----------:|:----------
    |                7.27 |      137,465,451.07 |    0.0% |           12.54 |            5.85 |  2.142 |           0.00 |    0.0% |     10.70 | `CHACHA20_1MB`
    |                7.37 |      135,758,621.02 |    0.0% |           13.04 |            5.93 |  2.199 |           0.07 |    0.0% |     11.00 | `CHACHA20_256BYTES`
    |                9.33 |      107,202,828.01 |    0.1% |           19.25 |            7.51 |  2.563 |           0.22 |    0.0% |     11.00 | `CHACHA20_64BYTES`
    
    26e9c7b588 squashme: fix vectorized chacha20 on big endian
    
    |             ns/byte |              byte/s |    err% |        ins/byte |        cyc/byte |    IPC |       bra/byte |   miss% |     total | benchmark
    |--------------------:|--------------------:|--------:|----------------:|----------------:|-------:|---------------:|--------:|----------:|:----------
    |                7.28 |      137,438,778.33 |    0.0% |           12.54 |            5.86 |  2.141 |           0.00 |    0.0% |     10.69 | `CHACHA20_1MB`
    |                7.37 |      135,749,652.15 |    0.0% |           13.04 |            5.93 |  2.199 |           0.07 |    0.0% |     11.00 | `CHACHA20_256BYTES`
    |                9.32 |      107,248,535.46 |    0.0% |           19.25 |            7.51 |  2.563 |           0.22 |    0.0% |     11.00 | `CHACHA20_64BYTES`
    
    e6ec033f17 squashme: fix main performance discrepancy between clang and gcc
    
    |             ns/byte |              byte/s |    err% |        ins/byte |        cyc/byte |    IPC |       bra/byte |   miss% |     total | benchmark
    |--------------------:|--------------------:|--------:|----------------:|----------------:|-------:|---------------:|--------:|----------:|:----------
    |                3.68 |      271,912,991.35 |    0.0% |            9.79 |            2.96 |  3.310 |           0.00 |    0.0% |     11.01 | `CHACHA20_1MB`
    |                3.80 |      263,471,169.28 |    0.0% |           10.29 |            3.06 |  3.366 |           0.07 |    0.0% |     11.00 | `CHACHA20_256BYTES`
    |                9.32 |      107,266,696.30 |    0.0% |           19.25 |            7.51 |  2.564 |           0.22 |    0.0% |     11.00 | `CHACHA20_64BYTES`
    

    </details>

    <details><summary>Rpi5</summary>

    rpi@rpi5-8:/mnt/my_storage/bitcoin$ for compiler in gcc clang; do \
      if [ "$compiler" = "gcc" ]; then CC=gcc; CXX=g++; COMP_VER=$(gcc -dumpfullversion); \
      else CC=clang; CXX=clang++; COMP_VER=$(clang -dumpversion); fi && \
      echo "> Compiler: $compiler $COMP_VER" && \
      for commit in 224120bf1299392deaa59ab71c895a1e6264f205 b9300cc696135d1ff31ffbdf639dc0f99167d49c 96f741441180d14df0519caa3f7c73f818a12dbf 26e9c7b588fcbdc956c4e2b241f4ef7d121d3d79 e6ec033f175031f72e23253bd0821f6fc6d353b2 6db8cf0e28bbaef24cb9e1e3f0c59f118a25619c 63a99f2b31efde3db91349bbf13f3c243c453084 e82c752615badf8a811846122c2c370d645385e9 62a8c487da9721fd1149dc3fefa7fb11292370bf 332fa6e26f293af58fad83ccc896291a68650fae 548791e2b58fb65a52159a18f3765613e11422a3; do \
        git fetch origin $commit >/dev/null 2>&1 && git checkout $commit >/dev/null 2>&1 && echo "" && git log -1 --pretty='%h %s' && \
        rm -rf build >/dev/null 2>&1 && cmake -B build -DBUILD_BENCH=ON -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=$CC -DCMAKE_CXX_COMPILER=$CXX >/dev/null 2>&1 && \
        cmake --build build -j$(nproc) >/dev/null 2>&1 && \
        for i in 1; do \
          build/bin/bench_bitcoin -filter='CHACHA20_.*' -min-time=10000; \
        done; \
      done; \
    done
    

    </details>

    <details><summary>Rpi5: gcc 15.0.1</summary>

    224120bf12 Merge bitcoin/bitcoin#32394: net: make m_nodes_mutex non-recursive
    
    |             ns/byte |              byte/s |    err% |        ins/byte |        cyc/byte |    IPC |       bra/byte |   miss% |     total | benchmark
    |--------------------:|--------------------:|--------:|----------------:|----------------:|-------:|---------------:|--------:|----------:|:----------
    |                2.46 |      406,186,773.08 |    0.0% |           15.56 |            5.89 |  2.640 |           0.02 |    0.0% |     11.00 | `CHACHA20_1MB`
    |                2.50 |      400,125,462.96 |    0.0% |           16.14 |            5.99 |  2.697 |           0.07 |    0.0% |     10.59 | `CHACHA20_256BYTES`
    |                2.70 |      370,016,682.88 |    0.0% |           17.88 |            6.47 |  2.762 |           0.25 |    0.0% |     11.00 | `CHACHA20_64BYTES`
    
    b9300cc696 chacha20: move single-block crypt to inline helper function
    
    |             ns/byte |              byte/s |    err% |        ins/byte |        cyc/byte |    IPC |       bra/byte |   miss% |     total | benchmark
    |--------------------:|--------------------:|--------:|----------------:|----------------:|-------:|---------------:|--------:|----------:|:----------
    |                2.32 |      430,122,901.72 |    0.0% |           15.63 |            5.57 |  2.807 |           0.02 |    0.0% |     11.01 | `CHACHA20_1MB`
    |                2.43 |      411,502,538.79 |    0.2% |           16.11 |            5.82 |  2.767 |           0.06 |    0.0% |     10.55 | `CHACHA20_256BYTES`
    |                2.58 |      387,118,423.10 |    0.1% |           17.55 |            6.19 |  2.836 |           0.19 |    0.0% |     11.01 | `CHACHA20_64BYTES`
    
    96f7414411 chacha20: Add generic vectorized chacha20 implementation
    
    |             ns/byte |              byte/s |    err% |        ins/byte |        cyc/byte |    IPC |       bra/byte |   miss% |     total | benchmark
    |--------------------:|--------------------:|--------:|----------------:|----------------:|-------:|---------------:|--------:|----------:|:----------
    |                4.83 |      206,987,012.46 |    0.0% |           23.35 |           11.56 |  2.021 |           0.00 |    0.1% |     10.98 | `CHACHA20_1MB`
    |                4.81 |      207,928,148.69 |    0.0% |           20.59 |           11.52 |  1.788 |           0.07 |    0.0% |     11.00 | `CHACHA20_256BYTES`
    |                2.71 |      368,891,391.01 |    0.0% |           17.84 |            6.49 |  2.749 |           0.19 |    0.0% |     11.00 | `CHACHA20_64BYTES`
    
    26e9c7b588 squashme: fix vectorized chacha20 on big endian
    
    |             ns/byte |              byte/s |    err% |        ins/byte |        cyc/byte |    IPC |       bra/byte |   miss% |     total | benchmark
    |--------------------:|--------------------:|--------:|----------------:|----------------:|-------:|---------------:|--------:|----------:|:----------
    |                4.84 |      206,725,965.93 |    0.0% |           23.35 |           11.57 |  2.018 |           0.00 |    0.4% |     11.00 | `CHACHA20_1MB`
    |                4.80 |      208,151,217.86 |    0.0% |           20.59 |           11.50 |  1.790 |           0.07 |    0.0% |     11.01 | `CHACHA20_256BYTES`
    |                2.72 |      367,823,395.01 |    0.0% |           17.84 |            6.51 |  2.741 |           0.19 |    0.0% |     11.00 | `CHACHA20_64BYTES`
    
    e6ec033f17 squashme: fix main performance discrepancy between clang and gcc
    
    |             ns/byte |              byte/s |    err% |        ins/byte |        cyc/byte |    IPC |       bra/byte |   miss% |     total | benchmark
    |--------------------:|--------------------:|--------:|----------------:|----------------:|-------:|---------------:|--------:|----------:|:----------
    |                4.83 |      207,011,227.50 |    0.0% |           23.29 |           11.56 |  2.015 |           0.00 |    0.1% |     11.00 | `CHACHA20_1MB`
    |                4.84 |      206,631,508.34 |    0.0% |           20.58 |           11.59 |  1.776 |           0.07 |    0.0% |     11.00 | `CHACHA20_256BYTES`
    |                2.71 |      368,486,078.94 |    0.0% |           17.84 |            6.50 |  2.745 |           0.19 |    0.0% |     11.00 | `CHACHA20_64BYTES`
    
    

    </details>

    <details><summary>Rpi5: clang 22.0.0</summary>

    224120bf12 Merge bitcoin/bitcoin#32394: net: make m_nodes_mutex non-recursive
    
    |             ns/byte |              byte/s |    err% |        ins/byte |        cyc/byte |    IPC |       bra/byte |   miss% |     total | benchmark
    |--------------------:|--------------------:|--------:|----------------:|----------------:|-------:|---------------:|--------:|----------:|:----------
    |                2.21 |      451,646,217.56 |    0.0% |           15.72 |            5.30 |  2.967 |           0.03 |    0.0% |     11.00 | `CHACHA20_1MB`
    |                2.30 |      435,574,764.05 |    0.1% |           16.31 |            5.50 |  2.967 |           0.08 |    0.0% |     11.00 | `CHACHA20_256BYTES`
    |                2.55 |      391,818,167.07 |    0.0% |           18.09 |            6.11 |  2.960 |           0.23 |    0.0% |     11.00 | `CHACHA20_64BYTES`
    
    b9300cc696 chacha20: move single-block crypt to inline helper function
    
    |             ns/byte |              byte/s |    err% |        ins/byte |        cyc/byte |    IPC |       bra/byte |   miss% |     total | benchmark
    |--------------------:|--------------------:|--------:|----------------:|----------------:|-------:|---------------:|--------:|----------:|:----------
    |                2.21 |      451,882,020.43 |    0.0% |           15.72 |            5.30 |  2.967 |           0.03 |    0.0% |     11.00 | `CHACHA20_1MB`
    |                2.30 |      435,590,027.21 |    0.1% |           16.31 |            5.50 |  2.968 |           0.08 |    0.0% |     11.00 | `CHACHA20_256BYTES`
    |                2.55 |      391,773,300.32 |    0.0% |           18.09 |            6.11 |  2.960 |           0.23 |    0.0% |     11.00 | `CHACHA20_64BYTES`
    
    96f7414411 chacha20: Add generic vectorized chacha20 implementation
    
    |             ns/byte |              byte/s |    err% |        ins/byte |        cyc/byte |    IPC |       bra/byte |   miss% |     total | benchmark
    |--------------------:|--------------------:|--------:|----------------:|----------------:|-------:|---------------:|--------:|----------:|:----------
    |                1.79 |      558,716,231.71 |    0.0% |            9.60 |            4.28 |  2.243 |           0.00 |    0.1% |     11.01 | `CHACHA20_1MB`
    |                1.65 |      607,517,921.76 |    0.0% |            6.55 |            3.94 |  1.662 |           0.10 |    0.0% |     11.00 | `CHACHA20_256BYTES`
    |                2.60 |      384,629,496.56 |    0.0% |           18.30 |            6.23 |  2.938 |           0.28 |    0.0% |     11.00 | `CHACHA20_64BYTES`
    
    26e9c7b588 squashme: fix vectorized chacha20 on big endian
    
    |             ns/byte |              byte/s |    err% |        ins/byte |        cyc/byte |    IPC |       bra/byte |   miss% |     total | benchmark
    |--------------------:|--------------------:|--------:|----------------:|----------------:|-------:|---------------:|--------:|----------:|:----------
    |                1.79 |      558,241,984.97 |    0.0% |            9.60 |            4.28 |  2.242 |           0.00 |    0.1% |     11.00 | `CHACHA20_1MB`
    |                1.64 |      609,910,639.38 |    0.0% |            6.55 |            3.93 |  1.668 |           0.10 |    0.0% |     11.00 | `CHACHA20_256BYTES`
    |                2.58 |      386,939,531.73 |    0.0% |           18.30 |            6.19 |  2.956 |           0.28 |    0.0% |     11.00 | `CHACHA20_64BYTES`
    
    e6ec033f17 squashme: fix main performance discrepancy between clang and gcc
    
    |             ns/byte |              byte/s |    err% |        ins/byte |        cyc/byte |    IPC |       bra/byte |   miss% |     total | benchmark
    |--------------------:|--------------------:|--------:|----------------:|----------------:|-------:|---------------:|--------:|----------:|:----------
    |                1.79 |      558,279,486.98 |    0.0% |            9.60 |            4.28 |  2.242 |           0.00 |    0.1% |     11.00 | `CHACHA20_1MB`
    |                1.64 |      609,989,533.96 |    0.0% |            6.55 |            3.93 |  1.669 |           0.10 |    0.0% |     11.00 | `CHACHA20_256BYTES`
    |                2.59 |      386,421,109.02 |    0.0% |           18.30 |            6.20 |  2.952 |           0.28 |    0.0% |     11.00 | `CHACHA20_64BYTES`
    

    </details>

  36. theuni commented at 8:19 PM on May 15, 2026: member

    @l0rinc Thanks for the benchmarks! Those were very helpful.

    I haven't gone through all of the comments here yet, since fixing the gcc regression was a blocker for everything else.

    I tracked down the root cause of the gcc slowdown compared to clang: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=125303

    tl;dr: clang gracefully uses 128bit simd when 256bit isn't available, gcc doesn't (yet).

    To work around that, I refactored to allow either 128bit or 256bit depending on the architecture. This works great. With the new code, 256bit operations only give a sliiight speedup (using avx2) over the 128bit ones. So I think the 128bit simd approach is very reasonable.

    gcc is now in the same ballpark as clang, also showing a 2x-4x speedup on the hardware that I've tested. @l0rinc: If you'd like to bench the new approach, see the branch here: https://github.com/theuni/bitcoin/commits/chacha20-vectorized-128bit/

    I'll work on cleaning that up next week.

  37. fanquake marked this as a draft on Aug 14, 2026
  38. fanquake commented at 3:24 PM on August 14, 2026: member

    I'll work on cleaning that up next week.

    Any chance you still want to clean that new branch up/rebase and push it up here?

  39. l0rinc commented at 9:30 PM on August 17, 2026: contributor

    @theuni, if you're busy with other stuff, I don't mind taking over this change, I can probably push an updated version this week.

  40. theuni commented at 9:34 PM on August 17, 2026: member

    Thanks for the pings. I can get to this this week. ~I'm planning to revert back to the 256bit impl and enable it for platforms which support that. By targeting avx2 and arm64, that should cover most users. Then in the future when gcc has caught up with clang, we can enable it unconditionally.~

    Nevermind that plan. I misremembered the state of gcc+arm64. See the below comment for my updated proposal.

  41. theuni commented at 11:19 PM on August 18, 2026: member

    @l0rinc I pushed up 2 big/messy commits. The first reworks the design to be much less hacky. It does away with nearly all of the include weirdness and ifdef mess.

    The second introduces a pluggable vector interface, and adds a 128bit implementation. This works around gcc's vectorizer's limitations and should fix the performance regressions you pointed out. It should now give a good speedup on x86_64 and arm64 without any slowdowns. This is a sacrifice in speed for clang (which happily breaks up 256bit vector operations per-platform as necessary/expected) and for gcc targets with 256bit operations available (like avx2).

    I chose this solution because, while not optimal, it at least gives a nice speedup (1.5x-2x) for the most widely used platforms. Once we get it in, we can look at adding an additional 256bit implementation and the necessary logic to decide at compile-time which should be used. In the future, once gcc is fixed and available enough, we can drop the 128bit one entirely.

    The code still needs lots of documentation and small cleanups. The commit series really doesn't make sense anymore, it should essentially all be squashed down into a single commit. I pushed early mainly for the sake of concept ACKs and updated benchmarking. I'll continue working on documentation and nit fixups. We're down to the wire for feature-freeze and it may be too late now, but maybe we can get it squeezed in :)

  42. DrahtBot added the label CI failed on Aug 19, 2026
  43. DrahtBot commented at 12:33 AM on August 19, 2026: contributor

    <!--85328a0da195eb286784d51f73fa0af9-->

    🚧 At least one of the CI tasks failed. <sub>Task lint: https://github.com/bitcoin/bitcoin/actions/runs/32195595778/job/95898825507</sub> <sub>LLM reason (✨ experimental): CI failed on lint checks because src/crypto/chacha_vec_impl.h is missing the expected include guard and the change introduced a circular dependency (crypto/chacha20 ↔ crypto/chacha20_vec).</sub>

    <details><summary>Hints</summary>

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

    </details>

  44. l0rinc commented at 7:26 AM on August 19, 2026: contributor

    Thanks @theuni, I also experimented with https://github.com/l0rinc/bitcoin/pull/286 as a slightly simpler alternative to this implementation (used a few AI cycles to strip it down this much), using one 128-bit SIMD path in chacha20.cpp for 4-block groups and 2-block horizontal remainders instead of a pluggable multi-file backend. It was also slightly faster on the tested x86-64 and ARM64 platforms, so it might offer some useful ideas here.

    <img width="3404" height="2863" alt="image" src="https://github.com/user-attachments/assets/3c08a18b-b8ae-4182-b602-ee4d2fa3aa50" />

    <details><summary>Raw GCC and Clang measurements and diffs</summary>

    Intel i9-9900K (x86-64) results

    Benchmark GCC yours ns/B GCC l0rinc ns/B GCC winner Clang yours ns/B Clang l0rinc ns/B Clang winner
    CHACHA20_64BYTES 2.05 1.79 l0rinc by 14.5% 1.85 1.83 l0rinc by 1.1%
    CHACHA20_256BYTES 0.98 0.81 l0rinc by 21.0% 0.89 0.82 l0rinc by 8.5%
    CHACHA20_1MB 0.91 0.81 l0rinc by 12.3% 0.83 0.81 l0rinc by 2.5%
    FSCHACHA20POLY1305_64BYTES 5.41 5.16 l0rinc by 4.8% 5.41 5.38 l0rinc by 0.6%
    FSCHACHA20POLY1305_256BYTES 2.38 2.23 l0rinc by 6.7% 2.28 2.21 l0rinc by 3.2%
    FSCHACHA20POLY1305_1MB 1.69 1.58 l0rinc by 7.0% 1.51 1.49 l0rinc by 1.3%

    4× Cortex-A76 (ARM64) results

    Benchmark GCC yours ns/B GCC l0rinc ns/B GCC winner Clang yours ns/B Clang l0rinc ns/B Clang winner
    CHACHA20_64BYTES 2.726 2.768 yours by 1.5% 2.593 2.631 yours by 1.5%
    CHACHA20_128BYTES 2.854 2.685 l0rinc by 6.3% 2.655 2.613 l0rinc by 1.6%
    CHACHA20_192BYTES 2.771 2.625 l0rinc by 5.5% 2.528 2.548 yours by 0.8%
    CHACHA20_256BYTES 1.696 1.439 l0rinc by 17.9% 1.733 1.671 l0rinc by 3.7%
    CHACHA20_1MB 1.654 1.426 l0rinc by 16.0% 1.629 1.601 l0rinc by 1.8%
    FSCHACHA20POLY1305_64BYTES 7.694 7.683 l0rinc by 0.2% 7.473 7.545 yours by 1.0%
    FSCHACHA20POLY1305_256BYTES 3.544 3.272 l0rinc by 8.3% 3.641 3.606 l0rinc by 1.0%
    FSCHACHA20POLY1305_1MB 2.447 2.224 l0rinc by 10.0% 2.570 2.544 l0rinc by 1.0%
    FSCHACHA20POLY1305_BIP324_318BYTES 4.754 4.697 l0rinc by 1.2% 4.606 4.616 yours by 0.2%
    FSCHACHA20POLY1305_BIP324_319BYTES 3.691 3.508 l0rinc by 5.2% 3.827 3.841 yours by 0.4%
    FSCHACHA20POLY1305_BIP324_320BYTES 4.250 4.080 l0rinc by 4.2% 4.382 4.411 yours by 0.7%

    </details>

  45. theuni commented at 4:13 PM on August 20, 2026: member

    @l0rinc Thanks! I spent all day yesterday trying to grok how that actually works.

    Turns out there are some really interesting algorithmic improvements to the 4block version that I finally understand and have reproduced and experimented with. I'm working on a cleaned up version now.

    tl;dr: It has to do a painful matrix transmutation because of the layout, but uses out-of-order writes to minimize spilling. It's a cool trick :)

  46. jonatack commented at 4:21 PM on August 20, 2026: member

    Concept ACK, good work.

  47. chacha20: move single-block crypt to inline helper function 8fb231205c
  48. chacha20: Add generic vectorized chacha20 implementation
    Exploit modern simd to calculate 2/4/6/8/16 states at a time depending on the
    size of the input.
    
    Demonstrates a 2x speedup on x86-64 and 3x for arm+neon. Platforms which
    require runtime detection (avx2/avx512) improve performance even further, and
    will come as a follow-up.
    
    Rather than hand-writing assembly or using arch-specific intrinsics, this is
    written using compiler built-ins understood by gcc and clang.
    
    In practice (at least on x86_64 and armv8), the compilers are able to produce
    assembly that's not much worse than hand-written.
    
    This means that every architecture can benefit from its own vectorized
    instructions without having to write/maintain an implementation for each one.
    But because each will vary in ability to exploit the parallelism, we allow
    (via ifdefs) each architecture to opt-out of some or all multi-state
    calculation at compile-time..
    
    Here, as a starting point, x86-64 and arm+neon have been defined based on local
    benchmarks.
    
    Co-authored-by: Lőrinc <pap.lorinc@gmail.com>
    e3386b9d0d
  49. theuni force-pushed on Aug 21, 2026
  50. theuni commented at 9:36 PM on August 21, 2026: member

    @l0rinc I spent quite a bit of time this week testing/comparing the horizontal layout approach with the vertical one. Ultimately I've concluded that the horizontal approach is the way to go. It's possible to eek out a tiny bit of extra performance using the 4-state vertical layout, but that comes at the cost of still having to carry implementations for the other state counts. The tricks used in your branch are very cool, but I don't think it's worth the cost of keeping two implementations.

    I just pushed a simpler 128bit implementation that uses some of your suggestions. Dropped the vector abstraction stuff. I am able to match your benchmark numbers for all platforms, while shaving off an additional ~40% from arm64+clang on my m1. Untested on arm64+gcc though, fingers crossed there are no surprises there.

    I went ahead and nuked the history, rebased, and added you as a co-author while I was at it.

    Comments and commit messages haven't been updated yet as this has been in heavy flux. Assuming there are no longer any obvious problems remaining, I can start getting it cleaned up (and passing c-i).

  51. l0rinc commented at 9:41 PM on August 21, 2026: contributor

    Thank @theuni, I'll review this during the weekend! Note that the CI indicates a dependency-cycle regression was introduced here.

  52. squashme: fixup namespaces and circular dependencies 8364a78e14
  53. DrahtBot removed the label CI failed on Aug 25, 2026
  54. in src/crypto/chacha20_vec_128impl.h:192 in 8364a78e14
     187 | +        arr_shuf0(arr3);
     188 | +    }
     189 | +}
     190 | +
     191 | +/* Read 32bytes of input, xor with calculated state, write to output. Assumes
     192 | +   that input and output are unaligned, and makes no assumptions about the
    


    l0rinc commented at 8:29 PM on August 29, 2026:

    We're taking 16 bytes per row, the comment should be updated

    /** XOR 16 input bytes with one state row and write them without assuming alignment or vec128's memory layout */
    
  55. in src/crypto/chacha20_vec_128impl.h:206 in 8364a78e14
     201 | +    tempvec ^= (vec128){temparr[0], temparr[1], temparr[2], temparr[3]};
     202 | +    temparr = {tempvec[0], tempvec[1], tempvec[2], tempvec[3]};
     203 | +    memcpy(out_bytes.data(), temparr.data(), out_bytes.size());
     204 | +}
     205 | +
     206 | +/* Merge the 128 bit lanes from 2 states to the proper order, then pass each vec_read_xor_write */
    


    l0rinc commented at 8:30 PM on August 29, 2026:
    /** Write each 64-byte state in row order */
    
  56. in src/crypto/chacha20_vec_128impl.h:236 in 8364a78e14
     231 | +}
     232 | +
     233 | +template <size_t STATES>
     234 | +ALWAYS_INLINE void multi_block_crypt(std::span<const std::byte> in_bytes, std::span<std::byte> out_bytes, const vec128& state0, const vec128& state1, const vec128& state2)
     235 | +{
     236 | +    static constexpr vec128 nums256 = (vec128){0x61707865, 0x3320646e, 0x79622d32, 0x6b206574};
    


    l0rinc commented at 8:31 PM on August 29, 2026:

    nums256 is easy to mistake for the vector width here.

        static constexpr vec128 constants{0x61707865, 0x3320646e, 0x79622d32, 0x6b206574};
    
  57. in src/crypto/chacha20_vec_128impl.h:24 in 8364a78e14
      19 | +#  endif
      20 | +#endif
      21 | +
      22 | +#if !defined(ALWAYS_INLINE)
      23 | +#  define ALWAYS_INLINE inline
      24 | +#endif
    


    l0rinc commented at 8:42 PM on August 29, 2026:

    Is there a specific reason for redefining ALWAYS_INLINE here? The other crypto backends already use (sha256_*.cpp, siphash.h) the one from attributes.h, could we do that here, too?

  58. in src/crypto/chacha20_vec.h:17 in 8364a78e14
      12 | +
      13 | +#ifdef __has_attribute
      14 | +  #if __has_attribute(vector_size)
      15 | +    #define ENABLE_CHACHA20_VEC 1
      16 | +  #endif
      17 | +#endif
    


    l0rinc commented at 8:57 PM on August 29, 2026:

    Now that we're relying on __builtin_shufflevector after the GCC codegen issues (and since we only enable this on x86_64, amd64, and aarch64), should we guard the feature macros before using them?

    #if defined(__has_attribute) && defined(__has_builtin)
      #if __has_attribute(vector_size) && __has_builtin(__builtin_shufflevector) && (defined(__x86_64__) || defined(__amd64__) || defined(__aarch64__))
        #define ENABLE_CHACHA20_VEC 1
      #endif
    #endif
    

    The dispatcher then needs neither a separate target guard nor an x86-named boolean whose alternative implicitly means AArch64.

    <details><summary>limit ChaCha20 vector targets</summary>

    diff --git a/src/crypto/chacha20_vec.cpp b/src/crypto/chacha20_vec.cpp
    index e79f98b6ad..80f03f99ed 100644
    --- a/src/crypto/chacha20_vec.cpp
    +++ b/src/crypto/chacha20_vec.cpp
    @@ -9,35 +9,18 @@
     
     #include <cassert>
     
    -#if defined(__x86_64__) || defined(__amd64__)
    -static constexpr bool target_x86_64 = true;
    -#else
    -static constexpr bool target_x86_64 = false;
    -#endif
    -
    -#if defined(__aarch64__)
    -static constexpr bool target_arm64 = true;
    -#else
    -static constexpr bool target_arm64 = false;
    -#endif
    -
    -static constexpr bool use_vectorized = target_x86_64 || target_arm64;
    -
     namespace chacha20_vec {
     
     static_assert(BLOCKLEN == chacha20_vec128::BLOCKLEN);
     
     void chacha20_crypt_vectorized(std::span<const std::byte>& in_bytes, std::span<std::byte>& out_bytes, const std::array<uint32_t, 12>& input) noexcept
     {
    -    if constexpr (!use_vectorized) {
    -        return;
    -    }
         assert(in_bytes.size() == out_bytes.size());
         chacha20_vec128::ChaCha20Vectorized crypter(input);
     
         while(in_bytes.size() >= BLOCKLEN) {
             size_t blocks = out_bytes.size() / BLOCKLEN;
    -        if constexpr(target_x86_64) {
    +        if constexpr(TARGET == VectorTarget::X86_64) {
                 // 4 is faster than 3 + 1
                 // 4 + 4 is faster than 3 + 3 + 2
                 if  (blocks == 8 || blocks == 4) {
    @@ -49,7 +32,7 @@ void chacha20_crypt_vectorized(std::span<const std::byte>& in_bytes, std::span<s
                 } else {
                     crypter.CryptStates<3>(in_bytes, out_bytes);
                 }
    -        } else if constexpr (target_arm64) {
    +        } else if constexpr (TARGET == VectorTarget::AARCH64) {
                 // 15 is faster than 8 + 4 + 1 + 1 + 1
                 // 12 is faster than 8 + 4
                 if (blocks == 15 ) {
    diff --git a/src/crypto/chacha20_vec.h b/src/crypto/chacha20_vec.h
    index 456b1864bd..c0c172c0d8 100644
    --- a/src/crypto/chacha20_vec.h
    +++ b/src/crypto/chacha20_vec.h
    @@ -10,19 +10,30 @@
     #include <cstddef>
     #include <span>
     
    -#ifdef __has_attribute
    -  #if __has_attribute(vector_size)
    +#if defined(__has_attribute) && defined(__has_builtin)
    +  #if __has_attribute(vector_size) && __has_builtin(__builtin_shufflevector) && (defined(__x86_64__) || defined(__amd64__) || defined(__aarch64__))
         #define ENABLE_CHACHA20_VEC 1
       #endif
     #endif
     
     #ifdef ENABLE_CHACHA20_VEC
     
    -namespace chacha20_vec
    -{
    -    static constexpr size_t BLOCKLEN = 64;
    -    void chacha20_crypt_vectorized(std::span<const std::byte>& in_bytes, std::span<std::byte>& out_bytes, const std::array<uint32_t, 12>& input) noexcept;
    -}
    +namespace chacha20_vec {
    +static constexpr uint16_t BLOCKLEN{64};
    +
    +enum class VectorTarget {
    +    X86_64,
    +    AARCH64,
    +};
    +
    +#if defined(__x86_64__) || defined(__amd64__)
    +constexpr VectorTarget TARGET{VectorTarget::X86_64};
    +#elif defined(__aarch64__)
    +constexpr VectorTarget TARGET{VectorTarget::AARCH64};
    +#endif
    +
    +void chacha20_crypt_vectorized(std::span<const std::byte>& in_bytes, std::span<std::byte>& out_bytes, const std::array<uint32_t, 12>& input) noexcept;
    +} // namespace chacha20_vec
     
     #endif // ENABLE_CHACHA20_VEC
    

    </details>

  59. in src/crypto/chacha20_vec.cpp:40 in 8364a78e14
      35 | +    assert(in_bytes.size() == out_bytes.size());
      36 | +    chacha20_vec128::ChaCha20Vectorized crypter(input);
      37 | +
      38 | +    while(in_bytes.size() >= BLOCKLEN) {
      39 | +        size_t blocks = out_bytes.size() / BLOCKLEN;
      40 | +        if constexpr(target_x86_64) {
    


    l0rinc commented at 9:54 PM on August 29, 2026:

    I find it hard to understand when the same method has mutually exclusive implementations. Usually we could extract the parts to dedicated methods with well known boundaries and the top method could simply be responsible for dispatching to the appropriate impl.

    Here the x86-64 and AArch64 dispatchers are long if/else if chains inside chacha20_crypt_vectorized, so the common entry point owns both target selection and every grouping rule. Some cases only emerge from branch order, such as 8 blocks reaching the AArch64 fallback because no exact case matches first.

    Could we move each target loop into a named helper, express exact group counts with switch, and leave the entry point only to select one constexpr target?

    <details><summary>split ChaCha20 target dispatch</summary>

    diff --git a/src/crypto/chacha20_vec.cpp b/src/crypto/chacha20_vec.cpp
    index 080eaa59e6..6214c726f7 100644
    --- a/src/crypto/chacha20_vec.cpp
    +++ b/src/crypto/chacha20_vec.cpp
    @@ -13,59 +13,66 @@ namespace chacha20_vec {
     
     static_assert(BLOCKLEN == chacha20_vec128::BLOCKLEN);
     
    -void chacha20_crypt_vectorized(std::span<const std::byte>& in_bytes, std::span<std::byte>& out_bytes, std::span<const uint32_t, STATE_WORDS> input) noexcept
    +namespace {
    +
    +[[maybe_unused]] ALWAYS_INLINE void CryptX86_64(std::span<const std::byte>& in_bytes, std::span<std::byte>& out_bytes, std::span<const uint32_t, STATE_WORDS> input) noexcept
     {
         assert(in_bytes.size() == out_bytes.size());
    -    chacha20_vec128::ChaCha20Vectorized crypter(input);
    +    chacha20_vec128::ChaCha20Vectorized crypter{input};
     
    -    while(in_bytes.size() >= BLOCKLEN) {
    -        size_t blocks = out_bytes.size() / BLOCKLEN;
    -        if constexpr(TARGET == VectorTarget::X86_64) {
    -            // 4 is faster than 3 + 1
    -            // 4 + 4 is faster than 3 + 3 + 2
    -            if  (blocks == 8 || blocks == 4) {
    -                crypter.CryptStates<4>(in_bytes, out_bytes);
    -            } else if (blocks == 2) {
    -                crypter.CryptStates<2>(in_bytes, out_bytes);
    -            } else if (blocks == 1) {
    -                crypter.CryptStates<1>(in_bytes, out_bytes);
    -            } else {
    -                crypter.CryptStates<3>(in_bytes, out_bytes);
    -            }
    -        } else if constexpr (TARGET == VectorTarget::AARCH64) {
    -            // 15 is faster than 8 + 4 + 1 + 1 + 1
    -            // 12 is faster than 8 + 4
    -            if (blocks == 15 ) {
    -                crypter.CryptStates<15>(in_bytes, out_bytes);
    -            } else if (blocks == 14) {
    -                crypter.CryptStates<14>(in_bytes, out_bytes);
    -            } else if (blocks == 13) {
    -                crypter.CryptStates<13>(in_bytes, out_bytes);
    -            } else if (blocks == 12) {
    -                crypter.CryptStates<12>(in_bytes, out_bytes);
    -            } else if (blocks == 11) {
    -                crypter.CryptStates<11>(in_bytes, out_bytes);
    -            } else if (blocks == 10) {
    -                crypter.CryptStates<10>(in_bytes, out_bytes);
    -            } else if (blocks == 9) {
    -                crypter.CryptStates<9>(in_bytes, out_bytes);
    -            } else if (blocks == 7) {
    -                crypter.CryptStates<7>(in_bytes, out_bytes);
    -            } else if (blocks == 6) {
    -                crypter.CryptStates<6>(in_bytes, out_bytes);
    -            } else if (blocks == 5) {
    -                crypter.CryptStates<5>(in_bytes, out_bytes);
    -            } else if (blocks == 4) {
    -                crypter.CryptStates<4>(in_bytes, out_bytes);
    -            } else if  (blocks >= 8 ) {
    -                crypter.CryptStates<8>(in_bytes, out_bytes);
    -            } else {
    -                break;
    -            }
    +    while (in_bytes.size() >= BLOCKLEN) {
    +        const size_t blocks = out_bytes.size() / BLOCKLEN;
    +        // 4 is faster than 3 + 1
    +        // 4 + 4 is faster than 3 + 3 + 2
    +        switch (blocks) {
    +        case 1: crypter.CryptStates<1>(in_bytes, out_bytes); break;
    +        case 2: crypter.CryptStates<2>(in_bytes, out_bytes); break;
    +        default: crypter.CryptStates<3>(in_bytes, out_bytes); break;
    +        case 4:
    +        case 8: crypter.CryptStates<4>(in_bytes, out_bytes); break;
             }
         }
     }
     
    +[[maybe_unused]] ALWAYS_INLINE void CryptAArch64(std::span<const std::byte>& in_bytes, std::span<std::byte>& out_bytes, std::span<const uint32_t, STATE_WORDS> input) noexcept
    +{
    +    assert(in_bytes.size() == out_bytes.size());
    +    chacha20_vec128::ChaCha20Vectorized crypter{input};
    +
    +    // 15 is faster than 8 + 4 + 1 + 1 + 1
    +    // 12 is faster than 8 + 4
    +    while (in_bytes.size() >= BLOCKLEN) {
    +        const size_t blocks = out_bytes.size() / BLOCKLEN;
    +        if (blocks < 4) break;
    +        switch (blocks) {
    +        case 3: crypter.CryptStates<3>(in_bytes, out_bytes); break;
    +        case 4: crypter.CryptStates<4>(in_bytes, out_bytes); break;
    +        case 5: crypter.CryptStates<5>(in_bytes, out_bytes); break;
    +        case 6: crypter.CryptStates<6>(in_bytes, out_bytes); break;
    +        case 7: crypter.CryptStates<7>(in_bytes, out_bytes); break;
    +        default: crypter.CryptStates<8>(in_bytes, out_bytes); break;
    +        case 9: crypter.CryptStates<9>(in_bytes, out_bytes); break;
    +        case 10: crypter.CryptStates<10>(in_bytes, out_bytes); break;
    +        case 11: crypter.CryptStates<11>(in_bytes, out_bytes); break;
    +        case 12: crypter.CryptStates<12>(in_bytes, out_bytes); break;
    +        case 13: crypter.CryptStates<13>(in_bytes, out_bytes); break;
    +        case 14: crypter.CryptStates<14>(in_bytes, out_bytes); break;
    +        case 15: crypter.CryptStates<15>(in_bytes, out_bytes); break;
    +        }
    +    }
    +}
    +
    +} // namespace
    +
    +void chacha20_crypt_vectorized(std::span<const std::byte>& in_bytes, std::span<std::byte>& out_bytes, std::span<const uint32_t, STATE_WORDS> input) noexcept
    +{
    +    if constexpr (TARGET == VectorTarget::X86_64) {
    +        CryptX86_64(in_bytes, out_bytes, input);
    +    } else if constexpr (TARGET == VectorTarget::AARCH64) {
    +        CryptAArch64(in_bytes, out_bytes, input);
    +    }
    +}
    +
     } // namespace chacha20_vec
     
     #endif // ENABLE_CHACHA20_VEC
    diff --git a/src/crypto/chacha20_vec.h b/src/crypto/chacha20_vec.h
    index 9b58dd6d1c..d190f45bbb 100644
    --- a/src/crypto/chacha20_vec.h
    +++ b/src/crypto/chacha20_vec.h
    @@ -31,6 +31,7 @@ constexpr VectorTarget TARGET{VectorTarget::X86_64};
     #elif defined(__aarch64__)
     constexpr VectorTarget TARGET{VectorTarget::AARCH64};
     #endif
    +constexpr uint16_t MIN_BLOCKS{TARGET == VectorTarget::AARCH64 ? 3 : 1}; // Shorter AArch64 inputs are faster in scalar code
     
     void chacha20_crypt_vectorized(std::span<const std::byte>& in_bytes, std::span<std::byte>& out_bytes, std::span<const uint32_t, STATE_WORDS> input) noexcept;
     } // namespace chacha20_vec
    

    </details>

  60. in src/crypto/chacha20_vec_128impl.h:116 in 8364a78e14
     111 | +    const vec128& y = std::get<ITER>(arr1);
     112 | +    vec128& z = std::get<ITER>(arr2);
     113 | +
     114 | +    x += y;
     115 | +    z ^= x;
     116 | +    vec_rotl<BITS>(z);
    


    l0rinc commented at 11:14 PM on August 29, 2026:

    Now that we have vec_add_xor_rot, could we reuse the single-vector helper in the array recursion?

    <details><summary>reuse ChaCha20 vector round helper</summary>

    diff --git a/src/crypto/chacha20_vec_128impl.h b/src/crypto/chacha20_vec_128impl.h
    index 3d207697f0..9342c78b6d 100644
    --- a/src/crypto/chacha20_vec_128impl.h
    +++ b/src/crypto/chacha20_vec_128impl.h
    @@ -100,13 +100,7 @@ ALWAYS_INLINE void vec_add_xor_rot(vec128& x, const vec128& y, vec128& z)
     template <size_t BITS, size_t I, size_t ITER = 0>
     ALWAYS_INLINE void arr_add_xor_rot(std::array<vec128, I>& arr0, const std::array<vec128, I>& arr1, std::array<vec128, I>& arr2)
     {
    -    vec128& x = std::get<ITER>(arr0);
    -    const vec128& y = std::get<ITER>(arr1);
    -    vec128& z = std::get<ITER>(arr2);
    -
    -    x += y;
    -    z ^= x;
    -    vec_rotl<BITS>(z);
    +    vec_add_xor_rot<BITS>(std::get<ITER>(arr0), std::get<ITER>(arr1), std::get<ITER>(arr2));
     
         if constexpr(ITER + 1 < I ) arr_add_xor_rot<BITS, I, ITER + 1>(arr0, arr1, arr2);
     }
     
    

    </details>

  61. in src/crypto/chacha20_vec_128impl.h:134 in 8364a78e14
     129 | +            QUARTERROUND( x0, x5,x10,x15);
     130 | +            QUARTERROUND( x1, x6,x11,x12);
     131 | +            QUARTERROUND( x2, x7, x8,x13);
     132 | +            QUARTERROUND( x3, x4, x9,x14);
     133 | +
     134 | +After the first round, arr_shuf0, arr_shuf1, and arr_shuf2 are used to shuffle
    


    l0rinc commented at 11:20 PM on August 29, 2026:

    arr_shuf0, arr_shuf1, and arr_shuf2 differ only in their lane rotation, while doubleround carries an unused template parameter.

    Could we express each rotation through arr_shuf<ROT> and drop the unused parameter?

    <details><summary>generalize ChaCha20 vector shuffles</summary>

    diff --git a/src/crypto/chacha20_vec_128impl.h b/src/crypto/chacha20_vec_128impl.h
    index 9342c78b6d..92b0148d75 100644
    --- a/src/crypto/chacha20_vec_128impl.h
    +++ b/src/crypto/chacha20_vec_128impl.h
    @@ -118,60 +118,43 @@ The second round:
                 QUARTERROUND( x2, x7, x8,x13);
                 QUARTERROUND( x3, x4, x9,x14);
     
    -After the first round, arr_shuf0, arr_shuf1, and arr_shuf2 are used to shuffle
    -the layout to prepare for the second round.
    +After the first round, arr_shuf<1>, arr_shuf<2> and arr_shuf<3> rotate the
    +lanes to prepare for the second round.
     
    -After the second round, they are used (in reverse) to restore the original
    -layout.
    +After the second round, the same rotations are applied in reverse to restore
    +the original layout.
     
     */
     
    -template <size_t I, size_t ITER = 0>
    -ALWAYS_INLINE void arr_shuf0(std::array<vec128, I>& arr)
    -{
    -    vec128& x = std::get<ITER>(arr);
    -    x = vec128{x[1], x[2], x[3], x[0]};
    -
    -    if constexpr(ITER + 1 < I ) arr_shuf0<I, ITER + 1>(arr);
    -}
    -
    -template <size_t I, size_t ITER = 0>
    -ALWAYS_INLINE void arr_shuf1(std::array<vec128, I>& arr)
    +/** Rotate the lanes of every array element left by ROT positions */
    +template <size_t ROT, size_t I, size_t ITER = 0>
    +ALWAYS_INLINE void arr_shuf(std::array<vec128, I>& arr)
     {
         vec128& x = std::get<ITER>(arr);
    -    x = vec128{x[2], x[3], x[0], x[1]};
    +    x = vec128{x[ROT % 4], x[(ROT + 1) % 4], x[(ROT + 2) % 4], x[(ROT + 3) % 4]};
     
    -    if constexpr(ITER + 1 < I ) arr_shuf1<I, ITER + 1>(arr);
    -}
    -
    -template <size_t I, size_t ITER = 0>
    -ALWAYS_INLINE void arr_shuf2(std::array<vec128, I>& arr)
    -{
    -    vec128& x = std::get<ITER>(arr);
    -    x = vec128{x[3], x[0], x[1], x[2]};
    -
    -    if constexpr(ITER + 1 < I ) arr_shuf2<I, ITER + 1>(arr);
    +    if constexpr (ITER + 1 < I) arr_shuf<ROT, I, ITER + 1>(arr);
     }
     
     /* Main round function. */
    -template <size_t I, size_t ITER = 0>
    -ALWAYS_INLINE void doubleround(std::array<vec128, I>& arr0, std::array<vec128, I>& arr1, std::array<vec128, I>&arr2, std::array<vec128, I>&arr3)
    +template <size_t I>
    +ALWAYS_INLINE void doubleround(std::array<vec128, I>& arr0, std::array<vec128, I>& arr1, std::array<vec128, I>& arr2, std::array<vec128, I>& arr3)
     {
         for(unsigned i = 0; i < 10; i++) {
             arr_add_xor_rot<16>(arr0, arr1, arr3);
             arr_add_xor_rot<12>(arr2, arr3, arr1);
             arr_add_xor_rot<8>(arr0, arr1, arr3);
             arr_add_xor_rot<7>(arr2, arr3, arr1);
    -        arr_shuf0(arr1);
    -        arr_shuf1(arr2);
    -        arr_shuf2(arr3);
    +        arr_shuf<1>(arr1);
    +        arr_shuf<2>(arr2);
    +        arr_shuf<3>(arr3);
             arr_add_xor_rot<16>(arr0, arr1, arr3);
             arr_add_xor_rot<12>(arr2, arr3, arr1);
             arr_add_xor_rot<8>(arr0, arr1, arr3);
             arr_add_xor_rot<7>(arr2, arr3, arr1);
    -        arr_shuf2(arr1);
    -        arr_shuf1(arr2);
    -        arr_shuf0(arr3);
    +        arr_shuf<3>(arr1);
    +        arr_shuf<2>(arr2);
    +        arr_shuf<1>(arr3);
         }
     }
    

    </details>

  62. in src/crypto/chacha20_vec.cpp:18 in 8364a78e14
      13 | +static constexpr bool target_x86_64 = true;
      14 | +#else
      15 | +static constexpr bool target_x86_64 = false;
      16 | +#endif
      17 | +
      18 | +#if defined(__aarch64__)
    


    l0rinc commented at 11:27 PM on August 29, 2026:

    The existing ChaCha20 benchmarks jump from 4 blocks to 16,384 blocks, so they do not isolate the 2-3-block paths or AArch64 exact 12-15-state groups.

    Instead of documenting how each platform behaved (or next to, as @ajtowns suggested), could we cover those four boundary sizes?

    <details><summary>benchmark ChaCha20 dispatch groups</summary>

    diff --git a/src/bench/chacha20.cpp b/src/bench/chacha20.cpp
    index cc2b57ebbe..c1c5aed142 100644
    --- a/src/bench/chacha20.cpp
    +++ b/src/bench/chacha20.cpp
    @@ -48,11 +48,41 @@ static void CHACHA20_64BYTES(benchmark::Bench& bench)
         CHACHA20(bench, BUFFER_SIZE_TINY);
     }
     
    +static void CHACHA20_128BYTES(benchmark::Bench& bench)
    +{
    +    CHACHA20(bench, ChaCha20Aligned::BLOCKLEN * 2);
    +}
    +
    +static void CHACHA20_192BYTES(benchmark::Bench& bench)
    +{
    +    CHACHA20(bench, ChaCha20Aligned::BLOCKLEN * 3);
    +}
    +
     static void CHACHA20_256BYTES(benchmark::Bench& bench)
     {
         CHACHA20(bench, BUFFER_SIZE_SMALL);
     }
     
    +static void CHACHA20_768BYTES(benchmark::Bench& bench)
    +{
    +    CHACHA20(bench, ChaCha20Aligned::BLOCKLEN * 12);
    +}
    +
    +static void CHACHA20_832BYTES(benchmark::Bench& bench)
    +{
    +    CHACHA20(bench, ChaCha20Aligned::BLOCKLEN * 13);
    +}
    +
    +static void CHACHA20_896BYTES(benchmark::Bench& bench)
    +{
    +    CHACHA20(bench, ChaCha20Aligned::BLOCKLEN * 14);
    +}
    +
    +static void CHACHA20_960BYTES(benchmark::Bench& bench)
    +{
    +    CHACHA20(bench, ChaCha20Aligned::BLOCKLEN * 15);
    +}
    +
     static void CHACHA20_1MB(benchmark::Bench& bench)
     {
         CHACHA20(bench, BUFFER_SIZE_LARGE);
    @@ -74,7 +104,13 @@ static void FSCHACHA20POLY1305_1MB(benchmark::Bench& bench)
     }
     
     BENCHMARK(CHACHA20_64BYTES);
    +BENCHMARK(CHACHA20_128BYTES);
    +BENCHMARK(CHACHA20_192BYTES);
     BENCHMARK(CHACHA20_256BYTES);
    +BENCHMARK(CHACHA20_768BYTES);
    +BENCHMARK(CHACHA20_832BYTES);
    +BENCHMARK(CHACHA20_896BYTES);
    +BENCHMARK(CHACHA20_960BYTES);
     BENCHMARK(CHACHA20_1MB);
     BENCHMARK(FSCHACHA20POLY1305_64BYTES);
     BENCHMARK(FSCHACHA20POLY1305_256BYTES);
    

    </details>


    Similarly, the FSChaCha20Poly1305 benchmarks jump from 256 bytes to 1 MiB, so they miss the partial-to-exact fifth ChaCha block transition used in the BIP324 comparison.

    Could we benchmark 318, 319, and 320-byte inputs to keep that P2P-sized transition visible?

    <details><summary>benchmark BIP324 ChaCha sizes</summary>

    diff --git a/src/bench/chacha20.cpp b/src/bench/chacha20.cpp
    index c1c5aed142..264d3fbd94 100644
    --- a/src/bench/chacha20.cpp
    +++ b/src/bench/chacha20.cpp
    @@ -98,6 +98,21 @@ static void FSCHACHA20POLY1305_256BYTES(benchmark::Bench& bench)
         FSCHACHA20POLY1305(bench, BUFFER_SIZE_SMALL);
     }
     
    +static void FSCHACHA20POLY1305_BIP324_318BYTES(benchmark::Bench& bench)
    +{
    +    FSCHACHA20POLY1305(bench, 318);
    +}
    +
    +static void FSCHACHA20POLY1305_BIP324_319BYTES(benchmark::Bench& bench)
    +{
    +    FSCHACHA20POLY1305(bench, 319);
    +}
    +
    +static void FSCHACHA20POLY1305_BIP324_320BYTES(benchmark::Bench& bench)
    +{
    +    FSCHACHA20POLY1305(bench, 320);
    +}
    +
     static void FSCHACHA20POLY1305_1MB(benchmark::Bench& bench)
     {
         FSCHACHA20POLY1305(bench, BUFFER_SIZE_LARGE);
    @@ -114,4 +129,7 @@ BENCHMARK(CHACHA20_960BYTES);
     BENCHMARK(CHACHA20_1MB);
     BENCHMARK(FSCHACHA20POLY1305_64BYTES);
     BENCHMARK(FSCHACHA20POLY1305_256BYTES);
    +BENCHMARK(FSCHACHA20POLY1305_BIP324_318BYTES);
    +BENCHMARK(FSCHACHA20POLY1305_BIP324_319BYTES);
    +BENCHMARK(FSCHACHA20POLY1305_BIP324_320BYTES);
     BENCHMARK(FSCHACHA20POLY1305_1MB);
    

    </details>

  63. in src/crypto/chacha20.cpp:299 in 8364a78e14
     296 | +#ifdef ENABLE_CHACHA20_VEC
     297 | +    // Only use the vectorized implementations if the counter will not overflow.
     298 | +    const bool overflow = static_cast<uint64_t>(input[8]) + blocks > std::numeric_limits<uint32_t>::max();
     299 | +    if (!overflow) {
     300 | +        const auto state = std::to_array(input);
     301 | +        chacha20_vec::chacha20_crypt_vectorized(in_bytes, out_bytes, state);
    


    l0rinc commented at 11:32 PM on August 29, 2026:

    The AArch64 backend handles 4 or more blocks, yet 1-3-block Crypt calls still construct its vector state before returning to scalar code.

    Could we expose the backend's minimum block count and skip shorter calls consistently (diff is against my other changes that I applied locally, may not apply cleanly)?

    <details><summary>skip short ChaCha20 vector calls</summary>

    diff --git a/src/crypto/chacha20.cpp b/src/crypto/chacha20.cpp
    index 891a157d57..9a833702b0 100644
    --- a/src/crypto/chacha20.cpp
    +++ b/src/crypto/chacha20.cpp
    @@ -290,8 +290,10 @@ inline void ChaCha20Aligned::Crypt(std::span<const std::byte> in_bytes, std::spa
         // The vectorized implementation cannot increment the first nonce word
         const size_t blocks{out_bytes.size() / BLOCKLEN};
         assert(blocks < std::numeric_limits<uint32_t>::max() - input[8]);
    -    chacha20_vec::chacha20_crypt_vectorized(in_bytes, out_bytes, input);
    -    input[8] += blocks - out_bytes.size() / BLOCKLEN;
    +    if (blocks >= chacha20_vec::MIN_BLOCKS) {
    +        chacha20_vec::chacha20_crypt_vectorized(in_bytes, out_bytes, input);
    +        input[8] += blocks - out_bytes.size() / BLOCKLEN;
    +    }
     #endif
         chacha20_crypt(in_bytes, out_bytes, input);
     }
    diff --git a/src/crypto/chacha20_vec.cpp b/src/crypto/chacha20_vec.cpp
    index 82ce9bc633..0b2a2e0114 100644
    --- a/src/crypto/chacha20_vec.cpp
    +++ b/src/crypto/chacha20_vec.cpp
    @@ -43,7 +43,7 @@ namespace {
         // 12 is faster than 8 + 4
         while (in_bytes.size() >= BLOCKLEN) {
             const size_t blocks = out_bytes.size() / BLOCKLEN;
    -        if (blocks < 4) break;
    +        if (blocks < MIN_BLOCKS) break;
             switch (blocks) {
             case 4: crypter.CryptStates<4>(in_bytes, out_bytes); break;
             case 5: crypter.CryptStates<5>(in_bytes, out_bytes); break;
    diff --git a/src/crypto/chacha20_vec.h b/src/crypto/chacha20_vec.h
    index 9b58dd6d1c..0c3a335026 100644
    --- a/src/crypto/chacha20_vec.h
    +++ b/src/crypto/chacha20_vec.h
    @@ -31,6 +31,7 @@ constexpr VectorTarget TARGET{VectorTarget::X86_64};
     #elif defined(__aarch64__)
     constexpr VectorTarget TARGET{VectorTarget::AARCH64};
     #endif
    +constexpr uint16_t MIN_BLOCKS{TARGET == VectorTarget::AARCH64 ? 4 : 1}; // Shorter AArch64 inputs are faster in scalar code
     
     void chacha20_crypt_vectorized(std::span<const std::byte>& in_bytes, std::span<std::byte>& out_bytes, std::span<const uint32_t, STATE_WORDS> input) noexcept;
     } // namespace chacha20_vec
    

    </details>

  64. in src/crypto/chacha20.cpp:314 in 8364a78e14


    l0rinc commented at 11:43 PM on August 29, 2026:

    Slightly unrelated, but my IDE flagged these: ChaCha20::Keystream expresses fixed-count buffer copies with matching end iterators, while both buffering paths leave derived counts mutable and Crypt checks a size only for emptiness.

    <details><summary>simplify ChaCha20 buffered paths</summary>

    diff --git a/src/crypto/chacha20.cpp b/src/crypto/chacha20.cpp
    index 9a833702b0..140f2baab9 100644
    --- a/src/crypto/chacha20.cpp
    +++ b/src/crypto/chacha20.cpp
    @@ -303,7 +303,7 @@ void ChaCha20::Keystream(std::span<std::byte> out) noexcept
         if (out.empty()) return;
         if (m_bufleft) {
             unsigned reuse = std::min<size_t>(m_bufleft, out.size());
    -        std::copy(m_buffer.end() - m_bufleft, m_buffer.end() - m_bufleft + reuse, out.begin());
    +        std::copy_n(m_buffer.end() - m_bufleft, reuse, out.begin());
             m_bufleft -= reuse;
             out = out.subspan(reuse);
         }
    @@ -314,7 +314,7 @@ void ChaCha20::Keystream(std::span<std::byte> out) noexcept
         }
         if (!out.empty()) {
             m_aligned.Keystream(m_buffer);
    -        std::copy(m_buffer.begin(), m_buffer.begin() + out.size(), out.begin());
    +        std::copy_n(m_buffer.begin(), out.size(), out.begin());
             m_bufleft = m_aligned.BLOCKLEN - out.size();
         }
     }
    @@ -323,7 +323,7 @@ void ChaCha20::Crypt(std::span<const std::byte> input, std::span<std::byte> outp
     {
         assert(input.size() == output.size());
     
    -    if (!input.size()) return;
    +    if (input.empty()) return;
         if (m_bufleft) {
             unsigned reuse = std::min<size_t>(m_bufleft, input.size());
             for (unsigned i = 0; i < reuse; i++) {
    

    </details>

  65. in src/crypto/chacha20_vec.cpp:47 in 8364a78e14
      42 | +                // 4 + 4 is faster than 3 + 3 + 2
      43 | +                // etc.
      44 | +            if  (blocks == 8 || blocks == 4) {
      45 | +                crypter.CryptStates<4>(in_bytes, out_bytes);
      46 | +            } else if (blocks == 2) {
      47 | +                crypter.CryptStates<2>(in_bytes, out_bytes);
    


    l0rinc commented at 12:23 AM on August 30, 2026:

    My concern with all these branches is that I'm not sure all of them are properly tested.

    The differential fuzzer currently chooses an arbitrary byte count and stops at 64 blocks.

    Could we derive the input size from named blocks in [0, 100] plus tail_bytes in [0, BLOCKLEN - 1]? This preserves empty inputs, makes every vector group directly selectable, and keeps the implementation as the independent oracle.

    <details><summary>fuzz ChaCha20 vector groups</summary>

    diff --git a/src/test/fuzz/crypto_diff_fuzz_chacha20.cpp b/src/test/fuzz/crypto_diff_fuzz_chacha20.cpp
    index 5e2f84d621..34cf8447b4 100644
    --- a/src/test/fuzz/crypto_diff_fuzz_chacha20.cpp
    +++ b/src/test/fuzz/crypto_diff_fuzz_chacha20.cpp
    @@ -322,18 +322,21 @@ FUZZ_TARGET(crypto_diff_fuzz_chacha20)
                     assert(counter == ctx.input[12]);
                 },
                 [&] {
    -                uint32_t integralInRange = fuzzed_data_provider.ConsumeIntegralInRange<size_t>(0, 4096);
    -                std::vector<uint8_t> output(integralInRange);
    +                constexpr uint16_t MAX_BLOCKS{100}; // Leaves room for future 256- and 512-bit vector dispatches
    +                const uint16_t blocks{fuzzed_data_provider.ConsumeIntegralInRange<uint16_t>(0, MAX_BLOCKS)};
    +                const uint8_t tail_bytes{fuzzed_data_provider.ConsumeIntegralInRange<uint8_t>(0, ChaCha20Aligned::BLOCKLEN - 1)};
    +                const uint32_t size{uint32_t{blocks} * ChaCha20Aligned::BLOCKLEN + tail_bytes};
    +                std::vector<uint8_t> output(size);
                     const std::vector<uint8_t> input = ConsumeFixedLengthByteVector(fuzzed_data_provider, output.size());
                     chacha20.Crypt(MakeByteSpan(input), MakeWritableByteSpan(output));
    -                std::vector<uint8_t> djb_output(integralInRange);
    +                std::vector<uint8_t> djb_output(size);
                     ECRYPT_encrypt_bytes(&ctx, input.data(), djb_output.data(), input.size());
                     assert(output == djb_output);
                     // DJB's version seeks forward to a multiple of 64 bytes after every operation. Correct for that.
                     uint32_t old_counter = counter;
    -                counter += (integralInRange + 63) >> 6;
    +                counter += (size + ChaCha20Aligned::BLOCKLEN - 1) / ChaCha20Aligned::BLOCKLEN;
                     if (counter < old_counter) ++nonce.first;
    -                if (integralInRange & 63) {
    +                if (size % ChaCha20Aligned::BLOCKLEN) {
                         chacha20.Seek(nonce, counter);
                     }
                     assert(counter == ctx.input[12]);
    

    </details>

  66. in src/crypto/chacha20_vec.cpp:54 in 8364a78e14
      49 | +                crypter.CryptStates<1>(in_bytes, out_bytes);
      50 | +            } else {
      51 | +                crypter.CryptStates<3>(in_bytes, out_bytes);
      52 | +            }
      53 | +        } else if constexpr (target_arm64) {
      54 | +                // 15 is faster than 8 + 4 + 1 + 1 + 1
    


    l0rinc commented at 12:25 AM on August 30, 2026:

    Is 15 also faster than e.g. 8+7? On my M4 Max, processing 14 and 15 blocks as 8+6 and 8+7 is faster than using exact 14- and 15-state groups.

  67. in src/crypto/chacha20_vec_128impl.h:69 in 8364a78e14
      64 | +template <>
      65 | +ALWAYS_INLINE
      66 | +void vec_rotl<8>(vec128& vec)
      67 | +{
      68 | +    using vec128_u8 = uint8_t __attribute__((__vector_size__(16)));
      69 | +    vec = (vec128)__builtin_shufflevector(reinterpret_cast<vec128_u8>(vec), vec128_u8{}, 3,0,1,2,7,4,5,6,11,8,9,10,15,12,13,14);
    


    l0rinc commented at 1:27 AM on August 30, 2026:

    These shuffles temporarily view the same 128 bits as 16- or 8-bit lanes, then convert the result back to four 32-bit lanes.

    Could we use std::bit_cast for both same-size conversions instead of reinterpret_cast and the C-style cast?

    <details><summary>bit-cast ChaCha20 vector lanes </summary>

    diff --git a/src/crypto/chacha20_vec_128impl.h b/src/crypto/chacha20_vec_128impl.h
    index f8fcbfb2ec..f77c50f5c7 100644
    --- a/src/crypto/chacha20_vec_128impl.h
    +++ b/src/crypto/chacha20_vec_128impl.h
    @@ -10,6 +10,7 @@
     
     #include <algorithm>
     #include <array>
    +#include <bit>
     #include <cassert>
     #include <cstdint>
     #include <span>
    @@ -34,7 +35,8 @@ ALWAYS_INLINE
     void vec_rotl<16>(vec128& vec)
     {
         using vec128_u16 = uint16_t __attribute__((__vector_size__(16)));
    -    vec = (vec128)__builtin_shufflevector(reinterpret_cast<vec128_u16>(vec), vec128_u16{}, 1, 0, 3, 2, 5, 4, 7, 6);
    +    const auto halfwords{std::bit_cast<vec128_u16>(vec)};
    +    vec = std::bit_cast<vec128>(__builtin_shufflevector(halfwords, vec128_u16{}, 1, 0, 3, 2, 5, 4, 7, 6));
     }
     #endif
     
    @@ -44,7 +46,8 @@ ALWAYS_INLINE
     void vec_rotl<8>(vec128& vec)
     {
         using vec128_u8 = uint8_t __attribute__((__vector_size__(16)));
    -    vec = (vec128)__builtin_shufflevector(reinterpret_cast<vec128_u8>(vec), vec128_u8{}, 3,0,1,2,7,4,5,6,11,8,9,10,15,12,13,14);
    +    const auto bytes{std::bit_cast<vec128_u8>(vec)};
    +    vec = std::bit_cast<vec128>(__builtin_shufflevector(bytes, vec128_u8{}, 3, 0, 1, 2, 7, 4, 5, 6, 11, 8, 9, 10, 15, 12, 13, 14));
     }
     #endif
    
    

    </details>

    Nit: formatting

  68. in src/crypto/chacha20_vec.cpp:77 in 8364a78e14
      72 | +                crypter.CryptStates<7>(in_bytes, out_bytes);
      73 | +            } else if (blocks == 6) {
      74 | +                crypter.CryptStates<6>(in_bytes, out_bytes);
      75 | +            } else if (blocks == 5) {
      76 | +                crypter.CryptStates<5>(in_bytes, out_bytes);
      77 | +            } else if (blocks == 4) {
    


    l0rinc commented at 1:36 AM on August 30, 2026:

    Corecheck measures the one-block vector path slower, while CryptStates<3> improved the 192-byte median from 0.98 to 0.86 ns/byte on this Apple M4 Max.

    Could we expose target minimums of two blocks on x86-64 and three on AArch64, and leave shorter calls scalar?

  69. in src/crypto/CMakeLists.txt:8 in 8364a78e14
       4 | @@ -5,6 +5,7 @@
       5 |  add_library(bitcoin_crypto STATIC EXCLUDE_FROM_ALL
       6 |    $<$<NOT:$<STREQUAL:${CMAKE_SYSTEM_NAME},Generic>>:aes.cpp>
       7 |    chacha20.cpp
       8 | +  chacha20_vec.cpp
    


    l0rinc commented at 3:28 AM on August 30, 2026:

    The dependency lint treats matching .h and .cpp basenames as one module, which creates chacha20_vec -> chacha20_vec_128impl -> chacha20_vec if the implementation header includes the shared interface.

    Could we rename this TU to chacha20_vec_dispatch.cpp, include chacha20_vec.h from the implementation header, and keep one definition of BLOCKLEN and STATE_WORDS?

  70. in src/crypto/chacha20_vec_128impl.h:270 in 8364a78e14
     265 | +        , state1((vec128){input[4], input[5], input[6], input[7]})
     266 | +        , state2((vec128){input[8], input[9], input[10], input[11]})
     267 | +    {
     268 | +    }
     269 | +    template <size_t STATES>
     270 | +    constexpr ALWAYS_INLINE void CryptStates(std::span<const std::byte>& in_bytes, std::span<std::byte>& out_bytes)
    


    l0rinc commented at 3:31 AM on August 30, 2026:

    This function always calls the non-constexpr multi_block_crypt, so it cannot be evaluated at compile time, we can probably drop the constexpr.

  71. in src/crypto/chacha20_vec_128impl.h:263 in 8364a78e14
     258 | +{
     259 | +    const vec128 state0;
     260 | +    const vec128 state1;
     261 | +    vec128 state2;
     262 | +public:
     263 | +    ALWAYS_INLINE ChaCha20Vectorized(const std::array<uint32_t, 12>& input) noexcept
    


    l0rinc commented at 3:32 AM on August 30, 2026:

    nit: Could we make this explicit?

  72. l0rinc changes_requested
  73. l0rinc commented at 3:53 AM on August 30, 2026: contributor

    I went through the current version and left inline comments.

    The specialization itself is not particularly complicated, but it is sensitive to compiler and architecture changes. I expect the dispatch and grouping choices to need retuning every few years, so I would prefer them to remain centralized and configurable instead of being spread across manual unrolls or duplicated control flow.

    Before this leaves draft, it would help to restructure the PR into smaller, independently reviewable commits that separate preparatory changes, the vector backend, its adoption, benchmarks, and correctness coverage. The title, description, commit messages, and several comments still describe the earlier 256-bit attempt. The implementation can also reuse existing infrastructure, particularly ALWAYS_INLINE from attributes.h and the endian helpers.

    [PR #286](https://github.com/l0rinc/bitcoin/pull/286) shows the final structure and coverage I have in mind. It includes most comments I added here.


    Corecheck currently has no coverage data for the new code and measures the 64-byte case about 7% slower. The one-block x86-64 path can stay scalar while larger groups keep the vector speedup. The explicit constructor and redundant constexpr warnings are cheap to address. The macro warning does not apply because ENABLE_CHACHA20_VEC controls preprocessing, and std::bit_cast makes the same-size vector lane conversions consistent.

    To be more confident in the correctness, I would like to see the new path covered by:

    • tests around every dispatch block count, including one byte below and above each full-block size and in-place operation (see comment).
    • focused benchmarks that isolate every target-specific grouping decision so they can be compared across platforms (see review comment).
    • an extension of the existing crypto_diff_fuzz_chacha20 target that can select each optimized group and compare it with the existing fuzzer oracle.

    I also benchmarked IBD on an Umbrel using master with V1 transport, master with V2 transport, and this PR with V2 transport. The result was surprising: master V1 and V2 were basically the same, while this PR was considerably slower there. I will remeasure future versions once the PR is out of draft, but this is another reason to keep the tuning choices easy to isolate, benchmark, and change.


github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-09-08 11:51 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me