【问题标题】:Intel TBB it's 2 time slower than std - tbb vs std英特尔 TBB 比 std 慢 2 倍 - tbb vs std
【发布时间】:2023-03-06 03:58:01
【问题描述】:

我在两个程序之间做了一些比较,这些程序用来自 mersenne twister 的伪随机整数填充给定向量,关键是 TBB 版本非常慢,std 版本在大约 0.6 秒内执行任务当 TBB 至少需要 1.1s 时。

我还注意到,TBB 并没有真正提供优化的算法来处理容器,但它只提供通用构造(parallel_for、parallel_for_each 和类似的)来处理通用任务,其中 std 提供 std::generate 更好,而且在这种情况下更清洁的解决方案。

您可以在http://www.sendspace.com/file/ew73h8http://www.sendspace.com/file/ew73h8 下载我的带有 2 个小源文件 + 一个用于 gcc 的 Makefile 的小测试

我在这里做错了什么?我越是增加这个向量的大小,TBB 越慢,我在 Ubuntu 13.04 64 位下使用 Intel Q6600。

TBB 版本在某些方面可能会更好?

编辑:两个文件的完整来源


config.hpp

#define N 10000000

std.cpp

#include <random>
#include <iostream>
#include <vector>
#include <algorithm>

#include "config.hpp"

int main() {

    std::vector<u_int32_t> v(N);

    std::mt19937 mt;
    std::uniform_int_distribution<u_int32_t> dist(0,499);

    std::generate(v.begin(),v.end(),[&]{return dist(mt);});

    return(0);
}

tbb.cpp

#include <tbb/concurrent_vector.h>
#include <tbb/parallel_for_each.h>

#include <random>
#include <iostream>

#include "config.hpp"

int main()
{
  tbb::concurrent_vector<u_int32_t> v(N);
  std::mt19937 mt;
  std::uniform_int_distribution<u_int32_t> dist(0, 499);
  tbb::parallel_for_each(v.begin(),v.end(),[&](u_int32_t& e){e = dist(mt); });

  return(0);
}

【问题讨论】:

  • 发布问题中的代码。大多数人都不愿意下载未知文件。
  • 并解释你的构建/运行环境是什么。如果你只有一个核心,也许加速比低于开销......
  • @Massa 它已经在那里了,我有一个四核 CPU(没有 HT,但我认为这并不重要)
  • @ronag 好的,我已经做到了。
  • std::uniform_int_distribution 在多个线程的竞争读取下是否安全?尝试将其设为thread_local,与mt 相同。

标签: c++ c++11 vector concurrency tbb


【解决方案1】:

您的代码实际上并没有做任何计算量那么大的事情,并且还写入了内核之间共享的一块内存,这将导致缓存行频繁失效。内存访问很有可能在整个运行时占主导地位,然后并行访问共享数据结构(潜在的锁定和类似的开销)只会使开销变得更糟。

此外,正如 dsign 所提到的,您通过在线程之间共享随机数生成器来引入额外的开销,这将进一步增加开销。

最后,您在这里并没有真正将苹果与苹果进行比较。使用任何类型的并行运行时库(如 Intel 的 TBB)都是有代价的,即此运行时引入的开销 - 您必须启动它,concurrent_vector 将具有访问机制,例如引入额外开销的锁等。无其中一部分是免费的,您所看到的性能差异很可能仅仅是由于您创建的额外运行时开销。

【讨论】:

  • 你的最后一段是非常重要的一点。但是,在 N=1000000 的情况下,工作量足够高(在我的计算机中接近 3 秒),如果不是因为内存效应,并行执行会带来一些收益。
  • 在什么情况下 TBB 可以显示出超过 std 的增益?你能举一个例子说明这个开销是值得的吗?我在想这种大小的向量足以尝试一些东西
  • 如果您的线程处理不同的数据结构和/或不共享任何状态,您将获得更好的结果。
【解决方案2】:

您正在为 ITBB 将控制的所有工作人员共享随机数生成器 (RNG),正如我从您的问题中看到的那样,这将是四个。撇开从多个线程改变 RNG 状态的线程安全问题不谈,我会指出对缓存的影响:RNG 状态使用的相同内存可用于从四个处理器读取和写入,这很可能使缓存无用。

让我们试试这个:

#include <tbb/concurrent_vector.h>
#include <tbb/parallel_for_each.h>

#include <vector>
#include <functional>

#include <random>
#include <iostream>

#include "config.hpp"

static thread_local std::mt19937 mt;
static thread_local std::uniform_int_distribution<u_int32_t> dist(0, 499);

int main()
{
  std::vector<u_int32_t> v(N);

  auto f = [&v](std::pair<u_int32_t, u_int32_t> const& p) {
     for (size_t i=p.first; i < p.second; i++)
     {
        v[i] = dist( mt );
     }
  };

  std::vector< std::pair< u_int32_t, u_int32_t > > work;
  work.push_back( std::make_pair( 0, N/2) );
  work.push_back( std::make_pair( N/2, N) );

  tbb::parallel_for_each(
    work.begin(),
    work.end(),
    f 
    );

  return(0);
}

现在时间下降到标准版本的近一半(我只有双核)。代码所做的是强制 itbb 在连续的内存块中工作,而不是分发数据,而是分发工作分配。我不认为这是使用 ITBB 的最佳方式,但另一方面,parallel_for_each 不能提供块大小(从我在 docs 中看到的),而使用 *parallel_for* 来做需要一些研究。但这并不难:

#include <tbb/concurrent_vector.h>
#include <tbb/parallel_for.h>

#include <vector>
#include <functional>

#include <random>
#include <iostream>

#include "config.hpp"

static thread_local std::mt19937 mt;
static thread_local std::uniform_int_distribution<u_int32_t> dist(0, 499);

int main()
{
  std::vector<u_int32_t> v(N);

  auto f = [&v](const tbb::blocked_range<u_int32_t>& p) {
     for (auto i=p.begin(); i < p.end(); i++)
     {
        v[i] = dist( mt );
     }
  };

  tbb::parallel_for(
    tbb::blocked_range<u_int32_t>(0,N), 
    f 
    );

  return(0);
}

您可能希望在 OpenMP 中使用一些并行结构,而不是使用 ITBB,反正它已经与 gcc 捆绑了很多年(您仍然可以将 ITBB 与 OpenMP 一起使用,但要小心)。

随机数和并行代码呢?他们很乱。如果您想独立地为 RNG 播种并用于时钟,上面的代码可能就足够了。如果您想获得可重现的结果和不相关的 RNG,那么您必须注意每个生成器都由特定于线程的种子初始化,并且您还需要一种让每个种子通过其线程接触确定性部分的方法工作...

【讨论】:

  • 我在英特尔 TBB 库中看不到 PRNG,我应该使用什么或做什么?
  • 实际上我从这样做中获得了好处,问题是 TBB 它仍然较慢,并且它并没有真正尊重 mersenne 和 uniform_distribution 的设计和实现。
【解决方案3】:

这有什么作用……

void f(u_int32_t& e)
{
    thread_local std::mt19937 mt;
    thread_local std::uniform_int_distribution<u_int32_t> dist(0, 499);
    e = dist(mt);
}


int main()
{
  tbb::concurrent_vector<u_int32_t> v(N);
  tbb::parallel_for_each(v.begin(),v.end(),f);

  return(0);
}

【讨论】:

  • 好的,现在它可以编译和工作了,这个解决方案使它比以前快一点但仍然比std慢,它是0.6s vs 0.7s,这也不尊重真正的统一int分布算法因为这个本地线程,我说得对吗?
  • 我如何知道使用thread_local 存储产生了多少线程?
  • thread_local 说明符给出对象线程本地存储持续时间。这意味着给定线程第一次遇到声明时它将构造对象 - 然后对象将继续存在直到该线程结束。换句话说,每个线程都会有自己的对象实例,所以如果有 4 个线程,就会有 4 个 mt 实例和 4 个 dist 实例。
  • 好的,现在我明白了,有一些反馈吗?如何获取具有标记为 thread_local 的对象实例的线程数?
  • 你可以创建一个对象,在其构造函数中写入 hello world,然后将 hello 对象作为 thread_local 添加到 f,然后运行程序,看看打印了多少个 hello world。
猜你喜欢
  • 2011-10-31
  • 1970-01-01
  • 2023-03-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-06-13
  • 2011-10-10
相关资源
最近更新 更多