The recently merged XOR obfuscation introduced a minor serialization cost (seen during IBD benchmarking). This PR is meant to alleviate that.
Changes in testing, benchmarking and implementation
- Added new tests comparing randomized inputs against a trivial implementation and performing roundtrip checks with random chunks.
- Updated the benchmark to reflect realistic scenarios by capturing every call of
util::Xor
for the first 860k blocks, creating a model with the same data distribution. An additional benchmark checks the effect of short-circuiting XOR when the key is zero, ensuring no speed regression occurs when the obfuscation feature is disabled. - Optimized the Xor function to process in batches (64/32/16/8 bits instead of per-byte).
- Migrated remaining
std::vector<std::byte>(8)
values touint64_t
.
Reproducer and assembly
Memory alignment is handled via std::memcpy
, optimized out on tested platforms (see https://godbolt.org/z/dcxvh6abq):
- Clang (x86-64) - 32 bytes/iter using SSE vector operations
- GCC (x86-64) - 16 bytes/iter using unrolled 64-bit XORs
- RISC-V (32-bit) - 8 bytes/iter using load/XOR/store sequence
- s390x (big-endian) - 64 bytes/iter with unrolled 8-byte XORs
Endianness
The only endianness issue was with bit rotation, intended to realign the key if obfuscation halted before full key consumption. Elsewhere, memory is read, processed, and written back in the same endianness, preserving byte order. Since CI lacks a big-endian machine, testing was done locally via Docker.
0brew install podman pigz
1softwareupdate --install-rosetta
2podman machine init
3podman machine start
4docker run --platform linux/s390x -it ubuntu:latest /bin/bash
5 apt update && apt install -y git build-essential cmake ccache pkg-config libevent-dev libboost-dev libssl-dev libsqlite3-dev && \
6 cd /mnt && git clone https://github.com/bitcoin/bitcoin.git && cd bitcoin && git remote add l0rinc https://github.com/l0rinc/bitcoin.git && git fetch --all && git checkout l0rinc/optimize-xor && \
7 cmake -B build && cmake --build build --target test_bitcoin -j$(nproc) && \
8 ./build/src/test/test_bitcoin --run_test=streams_tests
Performance
0 cmake -B build -DBUILD_BENCH=ON -DCMAKE_BUILD_TYPE=Release \
1&& cmake --build build -j$(nproc) \
2&& build/src/bench/bench_bitcoin -filter='XorHistogram|AutoFileXor' -min-time=10000
C++ compiler …………………….. AppleClang 16.0.0.16000026
Before:
ns/byte | byte/s | err% | total | benchmark |
---|---|---|---|---|
1.46 | 684,148,318.55 | 0.9% | 10.17 | AutoFileXor |
21.57 | 46,357,868.35 | 0.3% | 10.93 | XorHistogram |
After:
ns/byte | byte/s | err% | total | benchmark |
---|---|---|---|---|
0.14 | 7,071,717,185.75 | 0.9% | 11.25 | AutoFileXor |
9.25 | 108,126,826.70 | 0.4% | 11.00 | XorHistogram |
C++ compiler …………………….. GNU 13.2.0
Before:
ns/byte | byte/s | err% | ins/byte | cyc/byte | IPC | bra/byte | miss% | total | benchmark |
---|---|---|---|---|---|---|---|---|---|
1.84 | 544,924,597.89 | 0.1% | 9.20 | 3.49 | 2.639 | 1.03 | 0.1% | 11.03 | AutoFileXor |
35.74 | 27,980,106.15 | 0.1% | 110.78 | 121.32 | 0.913 | 12.70 | 5.6% | 11.70 | XorHistogram |
After:
ns/byte | byte/s | err% | ins/byte | cyc/byte | IPC | bra/byte | miss% | total | benchmark |
---|---|---|---|---|---|---|---|---|---|
0.54 | 1,863,647,178.39 | 0.0% | 0.00 | 0.00 | 0.743 | 0.00 | 2.1% | 11.06 | AutoFileXor |
13.73 | 72,823,051.75 | 0.0% | 24.45 | 46.61 | 0.525 | 5.96 | 13.4% | 11.04 | XorHistogram |
i.e. roughly 2-2.5x faster for a representative sample.
The 860k block profile contains a lot of very big arrays (96'233 separate sizes, biggest was 3'992'470 bytes long) - a big departure from the previous 400k and 700k blocks (having 1500 sizes, biggest was 9319 bytes long).
The performance characteristics are also quite different, now that we have more and bigger byte arrays:
C++ compiler …………………….. AppleClang 16.0.0.16000026
Before:
ns/byte | byte/s | err% | total | benchmark |
---|---|---|---|---|
1.04 | 961,221,881.72 | 0.2% | 93.30 | XorHistogram |
After:
ns/byte | byte/s | err% | total | benchmark |
---|---|---|---|---|
0.03 | 28,649,147,111.58 | 0.2% | 6.78 | XorHistogram |
i.e. ~30x faster with Clang at processing the data with representative histograms.
C++ compiler …………………….. GNU 13.2.0
Before:
ns/byte | byte/s | err% | ins/byte | cyc/byte | IPC | bra/byte | miss% | total | benchmark |
---|---|---|---|---|---|---|---|---|---|
0.97 | 1,032,916,679.87 | 0.0% | 9.01 | 3.29 | 2.738 | 1.00 | 0.0% | 86.58 | XorHistogram |
After:
ns/byte | byte/s | err% | ins/byte | cyc/byte | IPC | bra/byte | miss% | total | benchmark |
---|---|---|---|---|---|---|---|---|---|
0.10 | 10,369,097,976.62 | 0.0% | 0.32 | 0.33 | 0.985 | 0.06 | 0.6% | 8.63 | XorHistogram |
i.e. ~10x faster with GCC at processing the data with representative histograms.
Before:
ns/op | op/s | err% | total | benchmark |
---|---|---|---|---|
3,146,553.30 | 317.81 | 0.1% | 11.02 | ReadBlockFromDiskTest |
1,014,134.52 | 986.06 | 0.2% | 11.00 | ReadRawBlockFromDiskTest |
After:
ns/op | op/s | err% | total | benchmark |
---|---|---|---|---|
2,522,199.92 | 396.48 | 0.1% | 11.03 | ReadBlockFromDiskTest |
64,484.30 | 15,507.65 | 0.2% | 10.58 | ReadRawBlockFromDiskTest |
Also visible on https://corecheck.dev/bitcoin/bitcoin/pulls/31144
The obfuscation’s effect on IBD seems’ to be about 3%, see: #31144 (comment)