parallel_for创建了太多的类/体副本

TBB::parallel_for creates too many class/body copies?

本文关键字:副本 太多 for 创建 parallel      更新时间:2023-10-16

我遵循了基本的parallel_for示例TBB。文档说明:

模板函数parallel_for要求主体对象有一个复制构造函数,调用该构造函数为每个工作线程创建一个(或多个)单独的副本。

我的算法需要每个并发工作线程运行一些内存。现在我在复制构造函数中分配内存。它可以工作,但这些是我的8线机器上的数字:在0-10000的范围内,我得到了大约2000个工作块(operator()的调用),复制构造函数被调用了大约300次!这就是问题所在:300个内存分配中只需要8个。我检查了只有8个线程在运行,并且肯定不会同时使用超过8个类副本。

我假设副本的数量与线程的数量相关是完全错误的吗?有没有更好的方法来分配内存?

#include "tbb/tbb.h"
using namespace tbb;
class ApplyFoo {
    float *const my_a;
public:
    void operator()( const blocked_range<size_t>& r ) const {
        float *a = my_a;
        for( size_t i=r.begin(); i!=r.end(); ++i ) 
           Foo(a[i]); // Foo uses the allocated memory
    }
    ApplyFoo( float a[] ) :
        my_a(a)
    {}
    // the Copy-Constructor is called work every 
    ApplyFoo( const ApplyFoo& other ) :
        my_a(a)
    {
      // Allocate some memory here...
    }
    ~ApplyFoo() 
    {
      // Free the memory here...
    }
};
void ParallelApplyFoo( float a[], size_t n ) {
    parallel_for(blocked_range<size_t>(0,n), ApplyFoo(a));
}

我假设副本的数量与线程的数量相关是完全错误的吗?

假设使用默认分区器(auto_partitioner)的相关性是正确的,但是乘数足够大,并且取决于运行时条件,因此副本的数量可以与子分区的数量一样大。所以,这并不奇怪。

但是,可以通过指定增益大小来控制子范围的数量:

size_t p = task_scheduler_init::default_num_threads();
size_t grainsize = 2*n/p-1;
parallel_for(blocked_range<size_t>(0,n,grainsize), ApplyFoo(a));

这里的计算2*n/p-1是因为在TBB中,粒度不是可能子范围的最小尺寸,而是用于决定是否分裂的阈值。

此外,对于具有parallel_for主体副本数量的分区程序的完全可预测的行为(独立于运行时条件),使用simple_partitioner代替:

parallel_for(blocked_range<size_t>(0,n), ApplyFoo(a), simple_partitioner());

但是,它可能会导致大范围和小粒度的额外开销,因为它不聚合范围。

是否有更好的方法来分配内存?

是的,粒度大小并不是一个好方法,因为它会阻止TBB调度器更好地实现负载平衡。我建议使用线程本地存储容器。与基于编译器的TLS不同,它可以遍历值,以便在一个地方清理内存,即使原始线程已经消失。