99#include < string>
1010
1111#ifdef __linux__
12+ #include < algorithm>
1213#include < fstream>
14+ #include < map>
1315#include < pthread.h>
1416#include < sched.h>
15- #include < unordered_set>
1617#endif
1718
1819namespace stellar
1920{
2021
2122namespace
2223{
23- // Returns one logical CPU id per physical core, restricted to the CPUs this
24- // process is actually allowed to run on. Empty on failure or on non-Linux
25- // platforms.
26- std::vector<unsigned >
27- physicalCoreRepresentatives ()
24+ struct CpuPinning
25+ {
26+ // Allowed logical CPUs in pinning-preference order.
27+ std::vector<unsigned > order;
28+ // Number of distinct physical cores found in the allowed logical CPUs.
29+ // First `physicalCoreCount` entries of `order` are guaranteed to be on
30+ // distinct physical cores by the pinning order assignment procedure.
31+ size_t physicalCoreCount{0 };
32+ };
33+
34+ // Returns every logical CPU this process is allowed to run on, ordered by
35+ // pinning preference:
36+ // - One logical CPU per physical core in ascending order, but with the core
37+ // hosting logical CPU 0 shifted to be the last core in the order (as the OS
38+ // tends to concentrate housekeeping work on CPU 0)
39+ // - The remaining hyper-thread siblings follow, one tier of siblings at a
40+ // time in the same order as the physical core order above
41+ // - Within the CPU-0 core, CPU 0 itself is demoted behind its siblings, so it
42+ // ends up as the very last CPU we pin.
43+ // Empty on failure or on non-Linux platforms.
44+ CpuPinning
45+ pinnedCpuOrder ()
2846{
29- std::vector<unsigned > res;
3047#ifdef __linux__
3148 cpu_set_t affinity;
3249 CPU_ZERO (&affinity);
@@ -37,12 +54,7 @@ physicalCoreRepresentatives()
3754 return {};
3855 }
3956
40- // Walk the allowed CPUs in ascending order and keep the first (hence
41- // smallest) allowed sibling of each physical core. thread_siblings_list
42- // begins with the smallest sibling id, which is a stable per-core
43- // identifier independent of the affinity mask, so it lets us collapse the
44- // logical CPUs of one core down to a single representative.
45- std::unordered_set<unsigned > seenCores;
57+ std::map<unsigned , std::vector<unsigned >> cpusByCore;
4658 for (unsigned cpu = 0 ; cpu < CPU_SETSIZE ; ++cpu)
4759 {
4860 if (!CPU_ISSET (cpu, &affinity))
@@ -60,20 +72,58 @@ physicalCoreRepresentatives()
6072 // pinning rather than guess.
6173 return {};
6274 }
63- if (seenCores.insert (coreId).second )
75+ cpusByCore[coreId].push_back (cpu);
76+ }
77+
78+ // Order the cores with the CPU-0-hosting core last.
79+ std::vector<std::vector<unsigned > const *> coreOrder;
80+ coreOrder.reserve (cpusByCore.size ());
81+ size_t maxSiblings = 0 ;
82+ for (auto const & [coreId, cpus] : cpusByCore)
83+ {
84+ if (coreId != 0 )
6485 {
65- res .push_back (cpu );
86+ coreOrder .push_back (&cpus );
6687 }
88+ maxSiblings = std::max (maxSiblings, cpus.size ());
6789 }
90+ auto core0 = cpusByCore.find (0 );
91+ if (core0 != cpusByCore.end ())
92+ {
93+ // Demote CPU 0 behind its own hyper-thread siblings so it becomes the
94+ // very last CPU we pin.
95+ auto & core0Cpus = core0->second ;
96+ releaseAssert (!core0Cpus.empty ());
97+ std::rotate (core0Cpus.begin (), core0Cpus.begin () + 1 , core0Cpus.end ());
98+ coreOrder.push_back (&core0Cpus);
99+ }
100+
101+ // Emit the CPUs tier by tier: the first sibling of every core, then the
102+ // second siblings, and so on.
103+ std::vector<unsigned > res;
104+ for (size_t rank = 0 ; rank < maxSiblings; ++rank)
105+ {
106+ for (auto const * cpus : coreOrder)
107+ {
108+ if (rank < cpus->size ())
109+ {
110+ res.push_back ((*cpus)[rank]);
111+ }
112+ }
113+ }
114+ return {std::move (res), cpusByCore.size ()};
115+ #else
116+ return {};
68117#endif
69- return res;
70118}
71119} // namespace
72120
73121BatchExecutor::BatchExecutor ()
74122{
75- mPinCpus = physicalCoreRepresentatives ();
76- if (mPinCpus .empty ())
123+ auto pinning = pinnedCpuOrder ();
124+ mPinCpuOrder = std::move (pinning.order );
125+ mPhysicalCoreCount = pinning.physicalCoreCount ;
126+ if (mPinCpuOrder .empty ())
77127 {
78128 CLOG_WARNING (
79129 Perf,
@@ -82,10 +132,10 @@ BatchExecutor::BatchExecutor()
82132 }
83133 else
84134 {
85- for (size_t i = 0 ; i < mPinCpus .size (); ++i)
135+ for (size_t i = 0 ; i < mPinCpuOrder .size (); ++i)
86136 {
87- CLOG_DEBUG (Perf, " Batch worker physical core {} maps to CPU {}" , i,
88- mPinCpus [i]);
137+ CLOG_DEBUG (Perf, " Batch worker {} pins to CPU {}" , i,
138+ mPinCpuOrder [i]);
89139 }
90140 }
91141}
@@ -120,20 +170,31 @@ void
120170BatchExecutor::pinWorker (size_t index)
121171{
122172#ifdef __linux__
123- if (mPinCpus .empty ())
173+ if (mPinCpuOrder .empty ())
124174 {
125175 return ;
126176 }
127- if (index >= mPinCpus .size ())
177+ if (index >= mPinCpuOrder .size ())
178+ {
179+ CLOG_WARNING (
180+ Perf,
181+ " Not enough logical CPU cores to pin batch executor worker {} - "
182+ " the network configuration demands more cores than available. "
183+ " This may significantly reduce the performance of the node." ,
184+ index);
185+ return ;
186+ }
187+ if (index >= mPhysicalCoreCount )
128188 {
129189 CLOG_WARNING (
130190 Perf,
131- " Not enough physical cores to pin batch executor worker {} - the "
132- " network configuration demands more physical cores than available. "
133- " This may reduce the performance of the node." ,
191+ " Not enough physical CPU cores to pin batch executor "
192+ " worker {} - the network configuration demands more physical "
193+ " cores than available. Using hyper-thread sibling "
194+ " instead. This may reduce the performance of the node." ,
134195 index);
135196 }
136- unsigned cpu = mPinCpus .at (index % mPinCpus . size () );
197+ unsigned cpu = mPinCpuOrder .at (index);
137198 cpu_set_t set;
138199 CPU_ZERO (&set);
139200 CPU_SET (cpu, &set);
0 commit comments