我一直在使用openmp框架进行一些实验,发现了一些奇怪的结果,我不知道我知道如何解释。
我的目标是创建这个巨大的矩阵,然后用值填充它。为了从多线程环境中获得性能,我将代码的某些部分作为并行循环。我在一台带有2个四核处理器的机器中运行它,所以我可以安全地在那里放置8个并发线程。
一切都按预期工作,但由于某种原因,实际分配矩阵行的for循环在仅运行3个线程时具有奇怪的峰值性能。从那以后,添加更多线程只会让我的循环花费更长时间。有8个线程实际上需要的时间只有一个。
这是我的并行循环:
int width = 11;
int height = 39916800;
vector<vector<int> > matrix;
matrix.resize(height);
#pragma omp parallel shared(matrix,width,height) private(i) num_threads(3)
{
#pragma omp for schedule(dynamic,chunk)
for(i = 0; i < height; i++){
matrix[i].resize(width);
}
} /* End of parallel block */
这让我想知道:在多线程环境中调用malloc(我想是矢量模板类的resize方法实际调用的)时是否存在已知的性能问题?我在一个多线程环境中找到了一些关于释放堆空间性能损失的文章,但没有具体说明在这种情况下分配新空间。
为了给你一个例子,我将下面的图表显示循环完成所需的时间作为分配循环的线程数的函数,以及只读取数据的正常循环这个巨大的矩阵后来。
使用gettimeofday函数测量的两个时间似乎在不同的执行实例中返回非常相似和准确的结果。那么,任何人都有一个很好的解释?
答案 0 :(得分:7)
你对内部调用malloc的vector :: resize()是正确的。实现方式malloc相当复杂。我可以看到malloc在多线程环境中可能导致争用的多个地方。
malloc可能会在用户空间中保留全局数据结构来管理用户的堆地址空间。需要保护此全局数据结构以防止并发访问和修改。一些分配器已经进行了优化,以减少访问此全局数据结构的次数......我不知道Ubuntu出现了多远。
malloc分配地址空间。因此,当您实际开始触摸分配的内存时,您将经历“软页面错误”,这是一个页面错误,允许操作系统内核为分配的地址空间分配后备RAM。这可能是昂贵的,因为内核之旅并且需要内核采取一些全局锁来访问它自己的全局RAM资源数据结构。
用户空间分配器可能会保留一些分配的空间来提供新的分配。但是,一旦这些分配用完,分配器就需要返回内核并从内核中分配更多的地址空间。这也很昂贵,并且需要访问内核并且内核需要一些全局锁来访问其全局地址空间管理相关的数据结构。
Bottomline,这些互动可能相当复杂。如果您遇到这些瓶颈,我建议您只需“预先分配”您的记忆。这将涉及分配它然后触摸所有它(全部来自单个线程),以便您可以在以后从所有线程使用该内存,而不会在用户或内核级别遇到锁争用。
答案 1 :(得分:2)
内存分配器绝对是多线程的可能争用点。
从根本上说,堆是一个共享数据结构,因为可以在一个线程上分配内存,并在另一个线程上取消分配。实际上,您的示例正是如此 - “resize”将释放每个工作线程上的内存,这些线程最初分配在其他位置。
gcc和其他编译器中包含的malloc的典型实现使用共享全局锁,并且如果内存分配压力相对较低,则在线程间工作得相当好。但是,在某个分配级别之上,线程将开始在锁定上进行序列化,您将获得过多的上下文切换和缓存废弃,并且性能将降低。你的程序是一个分配繁重的东西的例子,在内循环中有一个alloc + dealloc。
我很惊讶OpenMP兼容的编译器没有更好的线程malloc实现?它们当然存在 - 请查看this question以获取列表。
答案 2 :(得分:1)
从技术上讲,STL vector
使用最终调用std::allocator
的{{1}}。 new
轮流调用libc的new
(适用于您的Linux系统)。
这个malloc实现作为通用分配器是非常有效的,是线程安全的,但是它不可伸缩(GNU libc的malloc来自Doug Lea的dlmalloc)。有许多分配器和论文可以改进dlmalloc以提供可扩展的分配。
我建议您查看来自Google的Emery Berger博士Hoard,tcmalloc和Intel Threading Building Blocks可扩展分配器。