【发布时间】:2020-12-22 19:16:25
【问题描述】:
我有一个在 Windows 10 上运行的多线程 c++ 应用程序,系统具有两个 Xeon SP Gold CPU 和 160gb 内存。应用程序启动std::thread::hardware_concurrency() 许多工作线程并处理数据。但是,在处理管道的某个时刻,我在所有内核上的任务管理器中报告的 CPU 负载都会下降(下降到 2-5%)。几个小时后,管道的那部分正常完成,但我正试图找出 CPU 内核未完全利用的原因。
这部分代码没有磁盘(或其他)I/O,只对内存中已经存在的数据进行操作(内存分布在两个 NUMA 节点上,但我已经将工作线程分为两组并分配了亲和掩码对于每个只有一个节点集的 cpu 核心的每个节点,并且只有很少的内存被多个线程访问,但这根本没有提高性能),任务管理器不报告任何硬页面错误并且只有少数几个同步点。
我已经开始检测执行,但到目前为止,我的跟踪没有显示任何可疑之处,所有线程似乎都在执行工作,只是速度很慢而且任务管理器显示内核大部分处于空闲状态。
所以目前我能想到的只有:
- 我不知道的代码中某处的某些互斥锁
- 由于碎片或其他原因导致堆分配非常缓慢(应用程序在具有 160gb 物理内存并具有大约 90gb 工作集内存的机器上运行)
- 软页面错误/TLB 未命中(任务管理器未报告)或某些 NUMA 相关问题
- 我只是忽略了一些非常明显的事情(最有可能的情况)
我的应用程序链接到几个静态库并编译成一个 exe。我正在用 MSVC 编译它(但我没有 Visual Studio 项目,我正在使用 Bazel 构建)。
我可以做些什么来追踪这个问题并找出线程在哪里以及为什么要休眠?
//编辑:由于 cmets 建议在问题中添加一些代码,因此它不会被关闭,这是线程管理(尽管我很确定问题不在这里)
class ThreadedRunner {
public:
ThreadedRunner()
: is_running_(true),
func_(nullptr),
barrier_(nullptr),
count_(0),
thread_(&ThreadedRunner::ThreadFunc, this) {}
~ThreadedRunner() {
{
std::unique_lock<std::mutex> lock(mutex_);
is_running_ = false;
condvar_.notify_all();
}
thread_.join();
}
bool Run(std::function<void(int)>* func, absl::Barrier* barrier,
int thread_index, int thread_count, int count) {
std::unique_lock<std::mutex> lock(mutex_);
if (func_ != nullptr) {
return false;
}
func_ = func;
barrier_ = barrier;
thread_index_ = thread_index;
thread_count_ = thread_count;
count_ = count;
lock.unlock();
condvar_.notify_all();
return true;
}
std::thread& MutableThread() { return thread_; }
private:
void ThreadFunc() {
std::unique_lock<std::mutex> lock(mutex_);
while (true) {
// Wait for an incoming task.
while (is_running_ && func_ == nullptr) {
condvar_.wait(lock);
}
if (!is_running_) {
return;
}
assert(func_ != nullptr);
// Execute!
lock.unlock();
for (int i = thread_index_; i < count_; i += thread_count_) {
(*func_)(i);
}
absl::Barrier* previous_barrier = barrier_;
// Mark this ThreadedRunner as available for another task before blocking on the current barrier.
barrier_ = nullptr;
thread_index_ = 0;
thread_count_ = 0;
count_ = 0;
func_ = nullptr;
if (previous_barrier->Block()) delete previous_barrier;
lock.lock();
}
}
std::mutex mutex_;
std::condition_variable condvar_;
bool is_running_;
std::function<void(int)>* func_;
absl::Barrier* barrier_;
int thread_index_;
int thread_count_;
int count_;
std::thread thread_;
};
class ThreadedRunnerPool {
public:
ThreadedRunnerPool() {
ULONG highest_node_number;
GetNumaHighestNodeNumber(&highest_node_number);
printf("highest NUMA node: %lu\n", highest_node_number);
num_numa_nodes_ = highest_node_number + 1;
runners_.resize(num_numa_nodes_);
}
void Run(int thread_count, int count, std::function<void(int)>* func) {
absl::Barrier* barrier = new absl::Barrier(thread_count + 1);
{
std::unique_lock<std::mutex> lock(mutex_);
int started = 0;
std::vector<int> index_per_numa_node(num_numa_nodes_, 0);
while (started < thread_count) {
int numaIdx = started % num_numa_nodes_;
int& idx = index_per_numa_node[numaIdx];
if (idx >= runners_[numaIdx].size()) {
ThreadedRunner* runner = new ThreadedRunner();
if(num_numa_nodes_ > 1) {
std::thread& thread = runner->MutableThread();
unsigned char node = static_cast<unsigned char>(numaIdx);
unsigned long long processorMask;
GetNumaNodeProcessorMask(node, &processorMask);
printf("node %u processor mask: %llx\n", (unsigned int) node, processorMask);
SetThreadAffinityMask(thread.native_handle(), processorMask);
}
runners_[numaIdx].emplace_back(runner);
}
if (runners_[numaIdx][idx]->Run(func, barrier, started, thread_count, count)) {
++started;
}
++idx;
}
}
if (barrier->Block()) delete barrier;
}
private:
std::mutex mutex_;
size_t num_numa_nodes_;
std::vector<std::vector<std::unique_ptr<ThreadedRunner>>> runners_;
};
void ParallelFor(int thread_count, int count, std::function<void(int)> func) {
static ThreadedRunnerPool* s_Pool = new ThreadedRunnerPool();
s_Pool->Run(thread_count, count, &func);
}
更新:
我现在已经全局替换了 new 和 delete 运算符(以及 new[] 和 delete[])以将这些调用包含在我的分析跟踪中,我现在看到执行时间由一些非常长的new 和 delete 调用,每个几百毫秒。这可能是什么原因?堆碎片?如果是这样,如何打击它?我已经在尝试尽可能预分配内存。
更新#2:
附加@mpoeter 推荐的工具还表明,我的工作线程在ucrtbase.dll!_malloc_base 或ucrtbase.dll!_free_base 内有超过90% 的时间处于同步状态,因此看起来堆分配和释放非常慢。知道这可能是什么原因吗?
【问题讨论】:
-
在什么操作系统上?使用 Linux 可能会更容易,例如使用strace(1)。还可以考虑在您的问题中显示一些minimal reproducible example。没有更多细节,不清楚。
-
这个问题很有趣,但您可能会包含一些示例代码,以确保没有人会以“寻找工具推荐”或类似原因关闭它。
-
@BasileStarynkevitch 在 Windows 10 上运行
-
@pptaszni 我想这样做,但我什至不知道从哪里开始或粘贴哪些代码部分。这是一个庞大的软件项目。我可以粘贴线程运行器的代码,但我很确定问题不存在
-
这么复杂的逻辑有什么目的。为什么不直接使用 OpenMP。跑步者完成后你会清理他们吗?
标签: c++ multithreading performance