OMP Atomic,为什么?

时间:2013-09-16 17:47:48

标签: atomic openmp

我遇到了OpenMP问题。我知道如果你在并行块中增加某些东西,你必须在该表达式之前设置一个原子。但在我的代码中有一部分我不明白。

为什么我必须在这里使用原子?

#pragma omp parallel
{
  double distance, magnitude, factor, r;
  vector_t direction;
  int i, j;
#pragma omp for
  for (i = 0; i < n_body - 1; i++)
    {
      for (j = i + 1; j < n_body; j++)
        {
          r = SQR (bodies[i].position.x - bodies[j].position.x) + SQR (bodies[i].position.y - bodies[j].position.y);
          // avoid numerical instabilities
          if (r < EPSILON)
            {
              // this is not how nature works :-)
              r += EPSILON;
            }
          distance = sqrt (r);
          magnitude = (G * bodies[i].mass * bodies[j].mass) / (distance * distance);

          factor = magnitude / distance;
          direction.x = bodies[j].position.x - bodies[i].position.x;
          direction.y = bodies[j].position.y - bodies[i].position.y;

          // +force for body i
      #pragma omp atomic
          bodies[i].force.x += factor * direction.x;
      #pragma omp atomic
          bodies[i].force.y += factor * direction.y;

          // -force for body j
      #pragma omp atomic
          bodies[j].force.x -= factor * direction.x;
      #pragma omp atomic
          bodies[j].force.y -= factor * direction.y;
        }
    }
    }

为什么我不必在这里使用它:

#pragma omp parallel
{
  vector_t delta_v, delta_p;
  int i;

#pragma omp for
  for (i = 0; i < n_body; i++)
    {
      // calculate delta_v
      delta_v.x = bodies[i].force.x / bodies[i].mass * dt;
      delta_v.y = bodies[i].force.y / bodies[i].mass * dt;

      // calculate delta_p
      delta_p.x = (bodies[i].velocity.x + delta_v.x / 2.0) * dt;
      delta_p.y = (bodies[i].velocity.y + delta_v.y / 2.0) * dt;

       // update body velocity and position
      bodies[i].velocity.x += delta_v.x;
      bodies[i].velocity.y += delta_v.y;
      bodies[i].position.x += delta_p.x;
      bodies[i].position.y += delta_p.y;

      // reset forces
      bodies[i].force.x = bodies[i].force.y = 0.0;


      if (bounce)
    {
      // bounce on boundaries (i.e. it's more like billard)
      if ((bodies[i].position.x < -body_distance_factor) || (bodies[i].position.x > body_distance_factor))
        bodies[i].velocity.x = -bodies[i].velocity.x;
      if ((bodies[i].position.y < -body_distance_factor) || (bodies[i].position.y > body_distance_factor))
        bodies[i].velocity.y = -bodies[i].velocity.y;
    }
    }
}

代码现在正在使用,但我根本不明白为什么。 你能救我吗?

亲切的问候 迈克尔

2 个答案:

答案 0 :(得分:3)

两个代码示例中的第二个,循环的每个并行迭代在数组的元素[i]上工作,并且从不查看任何相邻元素。因此,循环的每次迭代都不会对循环的任何其他迭代产生影响,并且它们都可以同时执行而无需担心。

然而,在第一个代码示例中,循环的每个并行迭代可以使用索引[j]读取和写入在bodies数组中的任何位置。这意味着两个线程可能正在尝试同时更新相同的内存位置,或者一个线程可能正在写入另一个线程正在读取的位置。为避免竞争条件,您需要确保写入是原子的。

答案 1 :(得分:0)

当多个线程写入同一个内存位置时,您需要使用原子操作符或临界区来防止竞争条件。原子操作符更快但有更多限制(例如,它们仅在POD上运行一些基本操作符)但在您的情况下您可以使用它们。

因此,当线程写入相同的内存位置时,您必须问自己。在第一种情况下,您只将外部循环并行化i而不是j上的内部循环,因此您实际上不需要i上的原子运算符{{1}上的原子运算符}}。

让我们考虑第一种情况的例子。我们假设j并且有4个线程。

n_body=101

首先,您会看到每个线程都写入一些相同的内存位置。例如,所有线程都使用Thread one i = 0-24, j = 1-100, j range = 100 Thread two i = 25-49, j = 26-100, j range = 75 Thread three i = 50-74, j = 51-100, j range = 50 Thread four i = 75-99, j = 76-100, j range = 25 写入内存位置。这就是为什么你需要j=76-100的原子运算符。但是,没有线程使用j写入相同的内存位置。这就是为什么你不需要i的原子运算符。

在你的第二种情况下,你只有一个循环并且它是并行化的,所以没有线程写入同一个内存位置,因此你不需要原子操作符。

这回答了您的问题,但这里有一些额外的评论可以改善您的代码效果:

还有另一个独立于原子算子的重要观察。您可以看到线程1运行i 100次,而线程4仅运行j 25次。因此,使用调度(静态)(通常是默认调度程序)不能很好地分配负载。对于较大的j值,情况会变得更糟。

一种解决方案是尝试计划(指导)。我以前没用过这个,但我认为这是正确的解决方案OpenMP: for schedule。 “随着工作的进展,一种特殊的动态调度是指导给每个任务提供越来越小的迭代块。”根据{{​​3}}引导每个连续的块得到“number_of_iterations_remaining / number_of_threads”。所以从我们给出的例子

n_body

现在请注意,线程分布更均匀。使用静态调度时,第四个线程仅运行了j次25次,现在第四个线程运行了Thread one i = 0-24, j = 1-100, j range = 100 Thread two i = 25-44, j = 26-100, j range = 75 Thread three i = 45-69, j = 46-100, j range = 55 Thread four i = 60-69, j = 61-100, j range = 40 Thread one i = 70-76, j = 71-100 ... 40次。

但是让我们更仔细地看看你的算法。在第一种情况下,您正在计算每个物体上的重力。以下是两种方法:

j

但是函数//method1 #pragma omp parallel for for(int i=0; i<n_body; i++) { vector_t force = 0; for(int j=0; j<n_body; j++) { force += gravity_force(i,j); } bodies[i].force = force; } = gravity_force(i,j)所以你不需要计算两次。所以你找到了一个更快的解决方案:

gravity_force(j,i)

第一种方法进行//method2 (see your code and mine below on how to parallelize this) for(int i=0; i<(n_body-1); i++) { for(int j=i+1; j<nbody; j++) { bodies[i].force += gravity_force(i,j); bodies[j].force += gravity_force(i,j); } } 次迭代,第二种方法做(n_bodies-1) nbodies / 2次迭代,首先是n_bodies n_bodies / 2。 但是,第二种情况更难以有效并行化(请参阅下面的代码和我的代码)。它必须使用atmoic操作,负载不平衡。第一种方法进行两次迭代,但负载均匀分布,不需要任何原子操作。你应该测试两种方法,看看哪种方法更快。

要平行第二种方法,你可以做你做的事情:

n_bodies*n_bodies

或者更好的解决方案是使用像这样的私人武力副本:

#pragma omp parallel for schedule(static) // you should try schedule(guided)
for(int i=0; i<(n_body-1); i++) {
    for(int j=i+1; j<nbody; j++) {
        //#pragma omp atomic //not necessary on i
        bodies[i].force += gravity_force(i,j);
        #pragma omp atomic   //but necessary on j
        bodies[j].force += gravity_force(i,j);
    }
}