【问题标题】:Are read-only of global variables and writing to private class members "false sharing" in openmp全局变量是只读的并且在openmp中写入私有类成员“错误共享”
【发布时间】:2014-05-19 06:33:50
【问题描述】:

我正在编写一个程序,其中一堆不同的类都存储在一个向量中,使用公共数据结构对私有成员执行并行操作。我想使用 OpenMP 为多个处理器并行化它,但我对代码中的两个操作有两个问题,这两个操作都在下面的示例中用 cmets 表示,显示了程序逻辑的简化形式。

#include <omp.h>
#include <iostream>
#include <sys/timeb.h>
#include <vector>

class A {
  private :
    long _i;
  public :
    void add_i(long &i) { _i += i; }
    long get_i() const { return _i; }
};

int main()
{
  timeb then;
  ftime(&then);
  unsigned int BIG = 1000000;
  int N = 4;
  std::vector<long> foo(BIG, 1);
  std::vector<A *> bar;
  for (unsigned int i = 0; i < N; i++)
  {
    bar.push_back(new A());
  }
  #pragma omp parallel num_threads(4)
  {
    for(long i = 0; i < BIG; i++)
    {
      int thread_n = omp_get_thread_num();
      // read a global variable
      long *to_add = &foo[i];
      // write to a private variable
      bar[thread_n]->add_i(*to_add);
    }
  }
  timeb now;
  ftime(&now);
  for (int i = 0; i < N; i++)
  {
    std::cout << bar[i]->get_i() << std::endl;
  }
  std::cout << now.millitm - then.millitm << std::endl;
}

第一个注释解决了从全局 foo 读取的问题。这是“虚假共享”(或数据晃动)吗?我阅读的大多数资源都在写操作方面谈论虚假共享,但我不知道这是否适用于读操作。

第二条注释解决了对 bar 中类的写入操作。同样的问题:这是虚假分享吗?它们写入相同全局数据结构中的元素(根据我的阅读,晃动),但只作用于元素内的私有数据。

当我用 for 循环替换 OpenMP 宏时,程序速度提高了大约 25%,所以我猜我做错了什么......

【问题讨论】:

  • 你的例子很奇怪。你说的是一堆不同的类,但我认为你的意思是同一个类的一堆不同的对象?你说的一堆是指四个。那不是很多。但它也被设置为似乎任意的线程数。您为什么不向我们展示您的非并行代码,然后我们可以向您展示如何最好地并行化它。

标签: c++ openmp


【解决方案1】:

现代内存分配器是线程感知的。为了防止在修改bar 的元素所指向的class A 的每个实例时出现错误共享,您应该将内存分配移动到并行区域内,例如:

const int N = 4;
std::vector<long> foo(BIG, 1);
std::vector<A *> bar(N);
#pragma omp parallel num_threads(N)
{
  int thread_n = omp_get_thread_num();
  bar[thread_n] = new A();
  for(long i = 0; i < BIG; i++)
  {
    // read a global variable
    long *to_add = &foo[i];
    // write to a private variable
    bar[thread_n]->add_i(*to_add);
  }
}

另请注意,在这种情况下,omp_get_thread_num() 仅被调用一次,而不是在您的代码中调用BIG 次。调用函数的开销相对较低,但多次调用时就会累加。

【讨论】:

  • 我不认为i 是这种情况,在并行区域内声明的变量是自动私有的。
  • 已修复。我有点错过了 i 在循环构造中声明的事实。
【解决方案2】:

您最大的共享问题是bar[thread_n]foo[i] 的阅读不是问题。

编辑:由于bar[thread_n] 持有指针,并且指针是更新的内容,因此很少或没有共享。您仍然可以从将“一次块”加载到每个 CPU 核心中受益,而不是从每个高速缓存行中为每个 CPU 核心读取一个或两个项目。所以下面的代码可能仍然受益。与往常一样,当涉及到性能问题时,进行大量基准测试(启用优化),因为不同的系统会表现不同(取决于编译器、CPU 架构、内存子系统等)

在每个线程中一次“集中”几个项目会更好。可能是这样的:

const int STEP=16;

for(long i = 0; i < BIG; i+=STEP)
{
  int thread_n = omp_get_thread_num();
  int stop = std::min(BIG-i, STEP);   // Don't go over edge. 
  for(j = 0; j < stop; j++)
  {
     // read a global variable    
     long *to_add = &foo[i+j];
     // write to a private variable
     bar[thread_n*STEP + j]->add_i(*to_add);
  }
}

您可能需要稍微调整“STEP”以使其达到正确的水平。

【讨论】:

  • 他的 bar 对象在堆上是 new。谁知道他们最终会在哪里。在我的笔记本电脑上,分配粒度是 32 字节,缓存行是 64。可能两个对象最终会在同一个缓存行中。
  • 但是向量bar是一块连续的内存。
  • 这是指针的向量,vector&lt;A*&gt; 而不是vector&lt;A&gt;
  • @Anycorn:指针存储在哪里?在向量中,它是一块连续的内存。与您写的 A* bar[1000000] 相同(嗯,有点)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-02-07
  • 1970-01-01
  • 2011-07-22
  • 2019-07-24
相关资源
最近更新 更多