From 225d0210f881acb10a3dfaf9bc5a23446bc12d21 Mon Sep 17 00:00:00 2001 From: Bitflash Date: Thu, 30 Jul 2026 14:07:50 -0300 Subject: mine: hash on every core instead of one (#52) Satoshi left a note above ThreadBitcoinMiner in 2009: //// todo: start one thread per processor, //// use getenv("NUMBER_OF_PROCESSORS") Nobody did, and it stopped being a nicety when the proof of work became RandomX. SHA-256d on a 2009 desktop lost little to a single thread. RandomX is built to be fed by every core at once, so one thread on a 32-core machine mines at a thirty-second of what the box can do -- and the only outward sign is a CPU that looks suspiciously idle. A user reported exactly that: 30 minutes of "Mining", no blocks, Bitflash sitting at 3.3% CPU while the 2 GB dataset was allocated and fast mode was active. Nothing was broken. It was hashing on one core. bitflash -gen # every core but one bitflash -gen -genproclimit=8 # exactly eight Automatic leaves one core to the rest of the node -- accepting blocks, talking to peers, drawing the window -- because a node that mines beautifully while falling behind the chain has won nothing. /genproclimit overrides it, and a count several times the core count is refused rather than quietly thrashing. The wrapper was already built for this: RandomXCreateMinerVM says "own VM, not shared", and the ~2 GB dataset is one allocation every thread reads. Only the 2 MB scratchpad multiplies, so sixteen threads cost about 32 MB more than one. The startup line says so, because "why is it using 2 GB" is the first question anyone asks. Two things had to be right for this to be safe: - Shutdown. Miners share slot 3 of vfThreadRunning and StopNode waits for every slot before DBFlush tears the database down. With several, the first one out announced that everyone had finished while the rest were still calling ProcessBlock, so they are counted now and only the last one clears the flag. The three CheckForShutdown(3) calls inside the miner became plain returns: _endthread() does not unwind on Windows, so the bookkeeping would have been skipped there. - Duplicated work. Each thread makes its own key, and the coinbase pays it, so every thread has a different merkle root and searches a different space. Block assembly was already under cs_main and cs_mapTransactions; AddKey and ProcessBlock take their own locks. Verified on both platforms: four distinct thread ids reach the hashing loop and all report back on shutdown, which exits 0 with DBFlush running. Auto picks cores-1 (1 on a 2-core box, correctly reported). Speedup itself needs a many-core machine to show and has not been measured here -- the claim in this message is about threads doing work, not a benchmark. Not touched: RandomX's memory. RANDOMX_DATASET_BASE_SIZE and SCRATCHPAD_L3 are part of the hash function, so a knob for "use more RAM" would either do nothing or fork the chain. Threads are the only real dial. --- src/main.cpp | 51 +++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 45 insertions(+), 6 deletions(-) (limited to 'src/main.cpp') diff --git a/src/main.cpp b/src/main.cpp index c882b0b..d4fc6d0 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -72,6 +72,13 @@ bool fSoloMineTest = false; // /solomine: mine without requiring a peer (local t // Settings int fGenerateBitcoins; +// How many threads to hash with. 0 means "decide from the hardware" -- see +// MinerThreadCount(). This was effectively 1 forever, because Bitcoin 0.1.0 +// started exactly one miner thread and nothing here ever changed that. It made +// sense for SHA-256d in 2009; RandomX is built to be fed by every core at once, +// so a 32-core machine was mining at a thirty-second of its capacity and the +// only visible symptom was a suspiciously idle CPU. +int nMinerThreads = 0; int64 nTransactionFee = 0; CAddress addrIncoming; int nMineMode = MINE_RELAY; @@ -119,6 +126,26 @@ void GetParticipantMiningStats(uint64& sharesSent, uint64& sharesAccepted, doubl // mapKeys // +// Threads to hash with, resolving the automatic case. +// +// Automatic leaves one core to the rest of the node -- accepting blocks, +// talking to peers, serving the GUI -- because a machine that mines perfectly +// while falling behind the chain has won nothing. Anyone who disagrees can say +// so with /genproclimit and get exactly what they ask for. +// +// Memory is not the reason to hold back: the ~2 GB RandomX dataset is shared by +// every thread, and each one adds only its own 2 MB scratchpad. Sixteen threads +// cost about 32 MB more than one. +int MinerThreadCount() +{ + if (nMinerThreads > 0) + return nMinerThreads; + unsigned int nCores = std::thread::hardware_concurrency(); + if (nCores <= 1) + return 1; + return (int)(nCores - 1); +} + bool AddKey(const CKey& key) { CRITICAL_BLOCK(cs_mapKeys) @@ -2769,7 +2796,7 @@ static bool PoolParticipantMiner() return true; } -bool BitcoinMiner() +bool BitcoinMiner(int nThreadId) { // Relay mode: never mines, just relays/syncs. (Shouldn't normally get here // since the GUI hides Start Mining for Relay, but guard it regardless.) @@ -2809,11 +2836,17 @@ bool BitcoinMiner() while (fGenerateBitcoins) { Sleep(50); - CheckForShutdown(3); + // Return instead of _endthread()ing. With more than one miner thread the + // bookkeeping in ThreadBitcoinMiner has to run on the way out, and + // _endthread() does not unwind on Windows, so a normal return is the + // only exit that works the same on both platforms. + if (fShutdown) + return true; while (vNodes.empty() && !fSoloMineTest) { Sleep(1000); - CheckForShutdown(3); + if (fShutdown) + return true; } unsigned int nTransactionsUpdatedLast = nTransactionsUpdated; @@ -2908,8 +2941,11 @@ bool BitcoinMiner() Sleep(1000); continue; } - LogPrint("net", "BitcoinMiner: mining with RandomX (%s)\n", - RandomXFastReady() ? "fast 2GB" : "light 256MB"); + // The thread number is in the line on purpose: every miner would + // otherwise print the same text, and the dedup filter would fold them + // into one, leaving no way to tell four threads from one. + LogPrint("net", "BitcoinMiner: thread %d hashing with RandomX (%s)\n", + nThreadId, RandomXFastReady() ? "fast 2GB" : "light 256MB"); unsigned int nStart = GetTime(); uint256 hashTarget = CBigNum().SetCompact(pblock->nBits).getuint256(); @@ -2950,7 +2986,8 @@ bool BitcoinMiner() // ~256 hashes is already plenty of time) if ((++pblock->nNonce & 0xff) == 0) { - CheckForShutdown(3); + if (fShutdown) + break; // falls through to RandomXDestroyMinerVM below if (pblock->nNonce == 0) break; if (pindexPrev != pindexBest) @@ -2963,6 +3000,8 @@ bool BitcoinMiner() } } RandomXDestroyMinerVM(rxvm); + if (fShutdown) + break; } return true; -- cgit v1.2.3