perf: multi-threading experiments (--disable-gil?)#369
Merged
Conversation
We don't care about the order of the txs in the blocks: bitcoind should have already validated consensus rules, we just trust bitcoind. Having two loops will allow parallelising processing the txs.
spend_utxo() does disk reads, so previously any single disk read would block the whole of advance_txs().
processing tx-outputs is fully CPU-bottlenecked, so this will only be faster if running python without the GIL
towards parallelising advance_blocks
:( splitting the workload across threads makes things significantly slower if there is a GIL
this will be friendlier for multi-threaded writes into it, than appending to bytearrays
SomberNight
force-pushed
the
202606_bench1
branch
2 times, most recently
from
June 25, 2026 17:50
1cf3f3e to
7baf42f
Compare
blindly adapt to recent changes of advance_txs
SomberNight
marked this pull request as ready for review
June 25, 2026 19:00
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Split block processing work across a thread-pool.
There are two kinds of performance we should consider:
grep BlockProcessor, e.g.:I have been experimenting running ElectrumX with a free-threaded cpython (compiled with
--disable-gil), and running withPYTHON_GIL=0at runtime. Normally Python has a Global Interpreter Lock, which means in practice only one thread can do work on the CPU, i.e. single core CPU performance is often the bottleneck for many workloads.Since Python 3.13, it is possible to run cpython without the GIL.
On master, the BlockProcessor is completely single-threaded. It processes blocks in order.
For each block, it processes txs in order. For each tx:
spend_utxo)Part of the reason for the huge variance in time needed for (2), is
spend_utxohitting the disk.spend_utxodoes a blocking read in RocksDB (or LevelDB), which might hit the RocksDB cache, or might hit the OS cache, or finally might have to go and read the disk. For a single block with 5k txs, each with 5 inputs,spend_utxomight hit the disk 25k times, sequentially. (though such an extreme example is unlikely: the caches will get warmed)This PR changes the block processing logic to process txs out-of-order within a single block:
As the
spend_utxocodepath is disk-read-bottlenecked, this should improve the performance regardless of the GIL.In case the GIL is disabled, this PR also parallelises a lot of the CPU-bottlenecked calculations.
However, I have found that doing this unconditionally makes performance of (1) significantly slower in the GIL-enabled traditional scenario (
tx/sec since genesis: 7,923 (height: 785,897), see later). Hence, we maintain two distinct code paths and branch at runtime depending on whether the GIL is enabled.Measured performance of this PR compared to master (using RocksDB 9):
tx/sec since genesis: 10,282 (height 955,147)tx/sec since genesis: 9,766 (height 783,693)tx/sec since genesis: 22,413 (height 955,333)(min=0.2 sec, avg=1.275 sec, max=3.4 sec, num_blocks=222)(min=0.1 sec, avg=0.633 sec, max=2.4 sec, num_blocks=221)(min=0.2 sec, avg=0.514 sec, max=1.9 sec, num_blocks=44)Memory usage: during sync from genesis, with the GIL disabled, due to the massive parallelization significantly more RAM is needed. I was testing with the default
CACHE_MB=1200: with the GIL, memory usage hovers around 2 GB, while without it is around 8 GB.For now, running ElectrumX with the GIL disabled is to be considered experimental. However, it seemed to work for me :)
generic perf baseline from master for my own reference (using SomberNight@b3ff9f9):