【发布时间】:2021-08-28 19:06:29
【问题描述】:
首先,我将声明我无法使用示例程序复制该行为。这有点困难,因为我不确定是什么导致了这个问题。
我已经提供了一些代码来给出我想要描述的内容的一种心理地图。本质上,如标题中所述,我们看到线程第一次调用大小为 >= 1KB 的 malloc 需要很长时间。
#include <iostream>
#include <thread>
#include <vector>
#include <ctime>
#include <ratio>
#include <chrono>
using namespace std::chrono;
class myClass {
public:
myClass();
void someFunction();
};
myClass::myClass() {;}
void myClass::someFunction() {
high_resolution_clock::time_point t1 = high_resolution_clock::now();
int *arr1 = (int*)malloc(sizeof(int) * 1000);
high_resolution_clock::time_point t2 = high_resolution_clock::now();
int *arr2 = (int*)malloc(sizeof(int) * 1000);
high_resolution_clock::time_point t3 = high_resolution_clock::now();
duration<double, std::milli> time_span1 = t2 - t1;
duration<double, std::milli> time_span2 = t3 - t2;
std::cout << "first alloc took" << time_span1.count() << "milliseconds" << std::endl;
std::cout << "second alloc took" << time_span2.count() << "milliseconds" << std::endl;
}
int main() {
myClass obj1;
std::vector<std::thread> ThreadManager(1);
ThreadManager[0] = std::thread(&myClass::someFunction, obj1);
ThreadManager[0].join();
}
同样,此示例中不存在此问题。此示例显示已启动线程中的第一个 malloc 比第二个花费的时间更长。但是,在我的机器上,第一个 malloc 的执行大约需要 0.05 毫秒,这对我来说不是问题。第二次调用的“加速”很容易归因于 ILP 之类的东西。
在我正在进行的项目中,第一个 malloc 的执行时间要差得多(5-10ms)。这仅在启动线程后第一次调用 malloc 时发生,前提是请求的内存量不可忽略 (>= ~1KB)。我注意到仅启动一个线程时存在此问题(如示例代码中所示),因此它似乎不是同步问题。该问题可能与碎片有关,但如果我在启动线程之前请求相同数量的内存,我们看不到性能问题。此外,项目中的大多数分配都是通过一次请求大块的分配器完成的,我认为这应该可以减少碎片问题的可能性。此外,我已经测试了主程序的多个输入,并且发生问题的输入集是确定性的——这对我来说意味着它与运行时的复杂性无关。我应该提一下,我正在做的项目规模适中(几千行),std::thread 的可调用对象属于一个比较大的类。
基本上,我不知道是什么导致了这个问题,我想知道是否有人以前见过类似的东西——如果有,他们是如何解决的:)
编辑:
经过进一步调查,性能错误至少与同步间接相关。 malloc 使用多个 arena 来处理多个同步调用。这些竞技场的数量可以通过调用mallopt 来更改。通过mallopt(M_ARENA_MAX, 1)将arenas的最大数量更改为一个后,第一次调用malloc的性能已经恢复正常。也就是说,由于应用程序是多线程的,我想使用更多的 arena,当我将最大值更改为 2 时,开销会返回(第一次调用 malloc 需要 5-10 毫秒)。我想知道如何增加竞技场的数量会导致这样的开销。
【问题讨论】:
-
“所以这似乎不是同步问题。” 您需要重新考虑该假设。
main是一个线程,因此即使是对std::thread的一次调用也会使进程成为多线程,并强制malloc处理同步。由于我看不到main在实际程序中的作用,我只能建议在调用main之后将sleep(2)放在std::thread中,看看这是否会改变行为。您也可以将sleep(2)放在someFunction的开头,让尘埃落定,然后再尝试第一个malloc(只是看看这是否会改变行为)。 -
嘿,谢谢你的想法。应该提一下,与示例程序类似,
main对应的线程只是在启动线程后立即调用 join。因此,我相信在启动的线程运行期间它没有进行任何分配。正如预期的那样,在启动和加入之间发出睡眠语句没有任何影响。另一方面,当要求启动线程立即sleep(2)时,我们看到第一个malloc 不再有很长的执行时间。 -
不幸的是,我无法在程序的这一部分中承受
sleep(2)(因此最初的执行时间问题)。我想知道在这段时间内会沉淀什么灰尘,如果有的话,我可以在启动之前为每个线程预先设置灰尘(或者更好的是,完全避免灰尘沉淀) -
也许你不能在最终产品中使用
sleep,魔法、错误修复睡眠从来都不是一个好主意,但它可能有助于找出问题所在。一旦你知道问题是什么,你就可以想出一个好的解决方案。如果你不知道问题是什么,祝你好运找到解决方案。如果你确实找到了解决方案,你怎么知道? -
不太可能提供有用的答案,因为您发布了您描述为具有代表性但没有重现问题的代码。这表明您未显示的代码受到牵连。一般而言,与创建/启动线程相关的簿记可能(取决于操作系统)在访问其他资源(包括内存)时引入延迟/冲突。此外,C 函数(如
malloc())可能不知道 C++ 线程,因此无法正常运行。一种策略可能是在程序启动期间创建线程池并根据需要回收线程,而不是启动线程以响应(例如)用户操作。
标签: c++ multithreading memory-management pthreads dynamic-memory-allocation