upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorBitflash <btf01@proton.me>2026-07-30 14:07:50 -0300
committerGitHub <noreply@github.com>2026-07-30 14:07:50 -0300
commit225d0210f881acb10a3dfaf9bc5a23446bc12d21 (patch)
treef8f15df707084a64924bed42e526c2e0ef43b1b9 /src
parent9f7141b28e2da9db7d7cb099d1f13f28742ff0fa (diff)
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.
Diffstat (limited to 'src')
-rw-r--r--src/gui.cpp3
-rw-r--r--src/main.cpp51
-rw-r--r--src/main.h8
-rw-r--r--src/main_gui.cpp25
-rw-r--r--src/net.cpp58
-rw-r--r--src/net.h2
6 files changed, 131 insertions, 16 deletions
diff --git a/src/gui.cpp b/src/gui.cpp
index fc6f1e1..825202b 100644
--- a/src/gui.cpp
+++ b/src/gui.cpp
@@ -32,6 +32,7 @@ int64 GetBalance();
32string PubKeyToAddress(const std::vector<unsigned char>& vchPubKey); 32string PubKeyToAddress(const std::vector<unsigned char>& vchPubKey);
33bool AddressToHash160(const std::string& str, uint160& hash160Ret); 33bool AddressToHash160(const std::string& str, uint160& hash160Ret);
34void ThreadBitcoinMiner(void*); 34void ThreadBitcoinMiner(void*);
35int StartMinerThreads();
35void ThreadRPCServer(void*); 36void ThreadRPCServer(void*);
36 37
37// --------------------------------------------------------------------------- 38// ---------------------------------------------------------------------------
@@ -762,7 +763,7 @@ static void DrawOptionsDialog()
762 fGenerateBitcoins = 0; 763 fGenerateBitcoins = 0;
763 } else { 764 } else {
764 fGenerateBitcoins = 1; 765 fGenerateBitcoins = 1;
765 if (!vfThreadRunning[3]) _beginthread(ThreadBitcoinMiner, 0, NULL); 766 if (!vfThreadRunning[3]) StartMinerThreads();
766 } 767 }
767 768
768 if (nMineMode == MINE_OPERATOR) { 769 if (nMineMode == MINE_OPERATOR) {
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
72 72
73// Settings 73// Settings
74int fGenerateBitcoins; 74int fGenerateBitcoins;
75// How many threads to hash with. 0 means "decide from the hardware" -- see
76// MinerThreadCount(). This was effectively 1 forever, because Bitcoin 0.1.0
77// started exactly one miner thread and nothing here ever changed that. It made
78// sense for SHA-256d in 2009; RandomX is built to be fed by every core at once,
79// so a 32-core machine was mining at a thirty-second of its capacity and the
80// only visible symptom was a suspiciously idle CPU.
81int nMinerThreads = 0;
75int64 nTransactionFee = 0; 82int64 nTransactionFee = 0;
76CAddress addrIncoming; 83CAddress addrIncoming;
77int nMineMode = MINE_RELAY; 84int nMineMode = MINE_RELAY;
@@ -119,6 +126,26 @@ void GetParticipantMiningStats(uint64& sharesSent, uint64& sharesAccepted, doubl
119// mapKeys 126// mapKeys
120// 127//
121 128
129// Threads to hash with, resolving the automatic case.
130//
131// Automatic leaves one core to the rest of the node -- accepting blocks,
132// talking to peers, serving the GUI -- because a machine that mines perfectly
133// while falling behind the chain has won nothing. Anyone who disagrees can say
134// so with /genproclimit and get exactly what they ask for.
135//
136// Memory is not the reason to hold back: the ~2 GB RandomX dataset is shared by
137// every thread, and each one adds only its own 2 MB scratchpad. Sixteen threads
138// cost about 32 MB more than one.
139int MinerThreadCount()
140{
141 if (nMinerThreads > 0)
142 return nMinerThreads;
143 unsigned int nCores = std::thread::hardware_concurrency();
144 if (nCores <= 1)
145 return 1;
146 return (int)(nCores - 1);
147}
148
122bool AddKey(const CKey& key) 149bool AddKey(const CKey& key)
123{ 150{
124 CRITICAL_BLOCK(cs_mapKeys) 151 CRITICAL_BLOCK(cs_mapKeys)
@@ -2769,7 +2796,7 @@ static bool PoolParticipantMiner()
2769 return true; 2796 return true;
2770} 2797}
2771 2798
2772bool BitcoinMiner() 2799bool BitcoinMiner(int nThreadId)
2773{ 2800{
2774 // Relay mode: never mines, just relays/syncs. (Shouldn't normally get here 2801 // Relay mode: never mines, just relays/syncs. (Shouldn't normally get here
2775 // since the GUI hides Start Mining for Relay, but guard it regardless.) 2802 // since the GUI hides Start Mining for Relay, but guard it regardless.)
@@ -2809,11 +2836,17 @@ bool BitcoinMiner()
2809 while (fGenerateBitcoins) 2836 while (fGenerateBitcoins)
2810 { 2837 {
2811 Sleep(50); 2838 Sleep(50);
2812 CheckForShutdown(3); 2839 // Return instead of _endthread()ing. With more than one miner thread the
2840 // bookkeeping in ThreadBitcoinMiner has to run on the way out, and
2841 // _endthread() does not unwind on Windows, so a normal return is the
2842 // only exit that works the same on both platforms.
2843 if (fShutdown)
2844 return true;
2813 while (vNodes.empty() && !fSoloMineTest) 2845 while (vNodes.empty() && !fSoloMineTest)
2814 { 2846 {
2815 Sleep(1000); 2847 Sleep(1000);
2816 CheckForShutdown(3); 2848 if (fShutdown)
2849 return true;
2817 } 2850 }
2818 2851
2819 unsigned int nTransactionsUpdatedLast = nTransactionsUpdated; 2852 unsigned int nTransactionsUpdatedLast = nTransactionsUpdated;
@@ -2908,8 +2941,11 @@ bool BitcoinMiner()
2908 Sleep(1000); 2941 Sleep(1000);
2909 continue; 2942 continue;
2910 } 2943 }
2911 LogPrint("net", "BitcoinMiner: mining with RandomX (%s)\n", 2944 // The thread number is in the line on purpose: every miner would
2912 RandomXFastReady() ? "fast 2GB" : "light 256MB"); 2945 // otherwise print the same text, and the dedup filter would fold them
2946 // into one, leaving no way to tell four threads from one.
2947 LogPrint("net", "BitcoinMiner: thread %d hashing with RandomX (%s)\n",
2948 nThreadId, RandomXFastReady() ? "fast 2GB" : "light 256MB");
2913 2949
2914 unsigned int nStart = GetTime(); 2950 unsigned int nStart = GetTime();
2915 uint256 hashTarget = CBigNum().SetCompact(pblock->nBits).getuint256(); 2951 uint256 hashTarget = CBigNum().SetCompact(pblock->nBits).getuint256();
@@ -2950,7 +2986,8 @@ bool BitcoinMiner()
2950 // ~256 hashes is already plenty of time) 2986 // ~256 hashes is already plenty of time)
2951 if ((++pblock->nNonce & 0xff) == 0) 2987 if ((++pblock->nNonce & 0xff) == 0)
2952 { 2988 {
2953 CheckForShutdown(3); 2989 if (fShutdown)
2990 break; // falls through to RandomXDestroyMinerVM below
2954 if (pblock->nNonce == 0) 2991 if (pblock->nNonce == 0)
2955 break; 2992 break;
2956 if (pindexPrev != pindexBest) 2993 if (pindexPrev != pindexBest)
@@ -2963,6 +3000,8 @@ bool BitcoinMiner()
2963 } 3000 }
2964 } 3001 }
2965 RandomXDestroyMinerVM(rxvm); 3002 RandomXDestroyMinerVM(rxvm);
3003 if (fShutdown)
3004 break;
2966 } 3005 }
2967 3006
2968 return true; 3007 return true;
diff --git a/src/main.h b/src/main.h
index 7e5bdf2..750d3f1 100644
--- a/src/main.h
+++ b/src/main.h
@@ -73,6 +73,10 @@ extern string strPoolName; // operator: announced pool name
73extern string strPoolDashboardUrl; // operator: optional dashboard URL 73extern string strPoolDashboardUrl; // operator: optional dashboard URL
74extern double dPoolFeePercent; // operator: announced fee percent 74extern double dPoolFeePercent; // operator: announced fee percent
75extern bool fSoloMineTest; // /solomine: mine without requiring a peer (local test) 75extern bool fSoloMineTest; // /solomine: mine without requiring a peer (local test)
76// Threads to hash with; 0 = decide from the hardware. Set by /genproclimit.
77extern int nMinerThreads;
78// Resolves the automatic case into a real count.
79int MinerThreadCount();
76extern bool gPoolServerRunning; // GUI/startup state 80extern bool gPoolServerRunning; // GUI/startup state
77extern volatile bool gPoolRunning; // runtime pool server loop flag (rpc.cpp) 81extern volatile bool gPoolRunning; // runtime pool server loop flag (rpc.cpp)
78 82
@@ -117,7 +121,9 @@ void PrintBlockTree();
117void AddOrphanTx(const CDataStream& vMsg); 121void AddOrphanTx(const CDataStream& vMsg);
118void EraseOrphanTx(uint256 hash); 122void EraseOrphanTx(uint256 hash);
119void LimitOrphanTx(unsigned int nMaxOrphans); 123void LimitOrphanTx(unsigned int nMaxOrphans);
120bool BitcoinMiner(); 124// nThreadId is 1..N, only for logging: it makes each miner's lines
125// distinguishable, which the dedup filter would otherwise collapse into one.
126bool BitcoinMiner(int nThreadId = 1);
121void ThreadRPCServer(void* parg); // rpc.cpp -- .btf pool server 127void ThreadRPCServer(void* parg); // rpc.cpp -- .btf pool server
122void GetParticipantMiningStats(uint64& sharesSent, uint64& sharesAccepted, double& hashRate); 128void GetParticipantMiningStats(uint64& sharesSent, uint64& sharesAccepted, double& hashRate);
123void SetParticipantMiningStatus(const std::string& status); 129void SetParticipantMiningStatus(const std::string& status);
diff --git a/src/main_gui.cpp b/src/main_gui.cpp
index 2484592..852ad4d 100644
--- a/src/main_gui.cpp
+++ b/src/main_gui.cpp
@@ -1,6 +1,7 @@
1// Bitflash entry point -- starts node threads then runs GUI (or headless). 1// Bitflash entry point -- starts node threads then runs GUI (or headless).
2 2
3#include "headers_core.h" 3#include "headers_core.h"
4#include <thread> // hardware_concurrency, to sanity-check /genproclimit
4#ifndef _WIN32 5#ifndef _WIN32
5#include <csignal> 6#include <csignal>
6#endif 7#endif
@@ -52,6 +53,7 @@ static void PrintUsage()
52 printf(" /operator\n"); 53 printf(" /operator\n");
53 printf(" /participant=POOL_BTF_ADDRESS\n"); 54 printf(" /participant=POOL_BTF_ADDRESS\n");
54 printf(" /solomine\n"); 55 printf(" /solomine\n");
56 printf(" /genproclimit=N (mining threads; 0 or absent = every core but one)\n");
55 printf("\n"); 57 printf("\n");
56 printf("Pool operator announcement:\n"); 58 printf("Pool operator announcement:\n");
57 printf(" /poolname=NAME\n"); 59 printf(" /poolname=NAME\n");
@@ -99,6 +101,26 @@ static void ParseStartupArguments(int argc, char* argv[])
99 if (arg(argc,argv,"/solomine") || arg(argc,argv,"-solomine")) 101 if (arg(argc,argv,"/solomine") || arg(argc,argv,"-solomine"))
100 fSoloMineTest = true; 102 fSoloMineTest = true;
101 103
104 // /genproclimit=N -- threads to hash with. 0 or absent means automatic,
105 // which is every core but one. Named after Bitcoin's own option so it reads
106 // familiarly to anyone who has run one of these before.
107 string strProcLimit = argval2(argc, argv, "/genproclimit", "-genproclimit");
108 if (!strProcLimit.empty())
109 {
110 int n = atoi(strProcLimit.c_str());
111 unsigned int nCores = std::thread::hardware_concurrency();
112 if (n < 0)
113 fprintf(stderr, "Ignoring /genproclimit=%s: not a thread count\n",
114 strProcLimit.c_str());
115 else if (nCores > 0 && n > (int)nCores * 4)
116 // Well past diminishing returns, and every thread still costs a
117 // scratchpad. Refuse rather than quietly thrash the machine.
118 fprintf(stderr, "Ignoring /genproclimit=%d: this machine has %u core(s)\n",
119 n, nCores);
120 else
121 nMinerThreads = n; // 0 stays automatic
122 }
123
102 if (arg(argc,argv,"/operator") || arg(argc,argv,"-operator")) 124 if (arg(argc,argv,"/operator") || arg(argc,argv,"-operator"))
103 nMineMode = MINE_OPERATOR; 125 nMineMode = MINE_OPERATOR;
104 126
@@ -289,8 +311,7 @@ int main(int argc, char* argv[])
289 printf("Error: _beginthread(ThreadRPCServer) failed\n"); 311 printf("Error: _beginthread(ThreadRPCServer) failed\n");
290 } 312 }
291 if (fGenerateBitcoins) 313 if (fGenerateBitcoins)
292 if (_beginthread(ThreadBitcoinMiner, 0, NULL) == (uintptr_t)-1) 314 StartMinerThreads();
293 printf("Error: _beginthread(ThreadBitcoinMiner) failed\n");
294 315
295#ifdef BITFLASH_NO_GUI 316#ifdef BITFLASH_NO_GUI
296 // Nothing else this binary can do; /nogui is accepted and redundant. 317 // Nothing else this binary can do; /nogui is accepted and redundant.
diff --git a/src/net.cpp b/src/net.cpp
index 2581578..9ee056f 100644
--- a/src/net.cpp
+++ b/src/net.cpp
@@ -10,6 +10,7 @@
10#undef snprintf 10#undef snprintf
11#endif 11#endif
12#include "headers.h" 12#include "headers.h"
13#include <thread> // hardware_concurrency, for the miner thread count
13#ifdef _WIN32 14#ifdef _WIN32
14#include <winsock2.h> 15#include <winsock2.h>
15#endif 16#endif
@@ -1224,18 +1225,63 @@ void ThreadMessageHandler2(void* parg)
1224 1225
1225 1226
1226 1227
1227//// todo: start one thread per processor, use getenv("NUMBER_OF_PROCESSORS") 1228// Satoshi's "todo: start one thread per processor" stood here since 2009 and is
1229// what MinerThreadCount() and StartMinerThreads() below finally do -- by way of
1230// std::thread::hardware_concurrency rather than NUMBER_OF_PROCESSORS, which only
1231// ever existed on Windows.
1232//
1233// Miners share slot 3 of vfThreadRunning, and StopNode waits for every slot to
1234// go false before the node tears the database down. With one miner that was the
1235// same thing as "the miner finished"; with several, the first one out would
1236// announce that everybody had finished while the rest were still hashing and
1237// still calling ProcessBlock. So count them, and let only the last one leaving
1238// clear the flag.
1239static CCriticalSection cs_nMinersRunning;
1240static int nMinersRunning = 0;
1241
1228void ThreadBitcoinMiner(void* parg) 1242void ThreadBitcoinMiner(void* parg)
1229{ 1243{
1230 vfThreadRunning[3] = true; 1244 int nThreadId = (int)(intptr_t)parg;
1231 CheckForShutdown(3); 1245 CRITICAL_BLOCK(cs_nMinersRunning)
1246 {
1247 nMinersRunning++;
1248 vfThreadRunning[3] = true;
1249 }
1232 try 1250 try
1233 { 1251 {
1234 bool fRet = BitcoinMiner(); 1252 bool fRet = BitcoinMiner(nThreadId);
1235 printf("BitcoinMiner returned %s\n\n\n", fRet ? "true" : "false"); 1253 printf("BitcoinMiner thread %d returned %s\n", nThreadId, fRet ? "true" : "false");
1236 } 1254 }
1237 CATCH_PRINT_EXCEPTION("BitcoinMiner()") 1255 CATCH_PRINT_EXCEPTION("BitcoinMiner()")
1238 vfThreadRunning[3] = false; 1256 CRITICAL_BLOCK(cs_nMinersRunning)
1257 {
1258 if (--nMinersRunning <= 0)
1259 {
1260 nMinersRunning = 0;
1261 vfThreadRunning[3] = false;
1262 }
1263 }
1264}
1265
1266// Start the miners. Returns how many actually got off the ground.
1267int StartMinerThreads()
1268{
1269 int nThreads = MinerThreadCount();
1270 int nStarted = 0;
1271 for (int i = 0; i < nThreads; i++)
1272 {
1273 if (_beginthread(ThreadBitcoinMiner, 0, (void*)(intptr_t)(i + 1)) == (uintptr_t)-1)
1274 printf("Error: _beginthread(ThreadBitcoinMiner) failed on thread %d\n", i + 1);
1275 else
1276 nStarted++;
1277 }
1278 // The dataset is one allocation shared by all of them; only the 2 MB
1279 // scratchpads multiply. Printed because "why is it using 2 GB" is the first
1280 // question anyone asks.
1281 printf("Mining with %d thread(s) of %u core(s) -- about %d MB "
1282 "(~2080 MB shared RandomX dataset + ~2 MB per thread)\n",
1283 nStarted, std::thread::hardware_concurrency(), 2080 + 2 * nStarted);
1284 return nStarted;
1239} 1285}
1240 1286
1241 1287
diff --git a/src/net.h b/src/net.h
index 4379d04..11c023e 100644
--- a/src/net.h
+++ b/src/net.h
@@ -58,6 +58,8 @@ int BtfPexAccept(const std::vector<std::string>& vDesc);
58void AbandonRequests(void (*fn)(void*, CDataStream&), void* param1); 58void AbandonRequests(void (*fn)(void*, CDataStream&), void* param1);
59bool AnySubscribed(unsigned int nChannel); 59bool AnySubscribed(unsigned int nChannel);
60void ThreadBitcoinMiner(void* parg); 60void ThreadBitcoinMiner(void* parg);
61// Starts MinerThreadCount() miners; returns how many came up.
62int StartMinerThreads();
61bool StartNode(string& strError=REF(string())); 63bool StartNode(string& strError=REF(string()));
62bool StopNode(); 64bool StopNode();
63void CheckForShutdown(int n); 65void CheckForShutdown(int n);