【问题标题】:First call to malloc after launching thread takes very long启动线程后首次调用 malloc 需要很长时间
【发布时间】: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


【解决方案1】:

这可能是也可能不是同步问题。 malloc 的语言实现可能是管理每个线程或每个处理器的空闲列表,或者可能有一个可能需要同步的内存池。因此,不了解剩余代码(实际上是整个应用程序和运行时)的作用会导致猜测。

同时,用户模式内存管理器做了两件事:一是确保操作系统分配(也称为“使有效”)一部分虚拟地址空间,二是细分该部分以响应到 malloc() 调用。

因此,可以想象(推测)第一次调用 malloc 从操作系统请求虚拟地址空间。不是便宜的电话。第二次调用可能会简单地将指针返回到操作系统返回的空间中,从而使其更快。

再次,猜测。

【讨论】:

  • 所以,经过更多调查,我发现罪魁祸首实际上是 malloc 中的竞技场分配器(所以它似乎至少与同步化间接相关)。当我通过 mallopt(M_ARENA_MAX, 1); 将 arenas 的数量减少到 1 时,我在仅启动一个线程时没有看到所有输入的 malloc 性能问题,但是当我将 arenas 的数量增加到 2 或更多时,原始问题返回(5-10ms malloc 延迟)。你知道为什么增加竞技场的数量会导致这样的开销吗?
  • 没有实际查看代码,我猜。对不起!
  • 是的,我知道这是一个很难回答的问题,非常感谢您的意见。该评论适用于希望解决自己问题的未来观众
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-01-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-05-05
相关资源
最近更新 更多