使用 C/OpenMP 时出现性能问题

Performance issue while using C/OpenMP

我写了一些代码来测试使用 C 和 OpenMP 的小程序的执行时间,我遇到了一些应用程序执行时间问题。 这是一段代码,负责添加 2 个向量:

  float *x_f = (float *)malloc(sizeof(float) * DATA_SIZE);
  float *y_f = (float *)malloc(sizeof(float) * DATA_SIZE);

  for (int i = 0; i < DATA_SIZE; i++) {

    x_f[i] = 1.0f;
    y_f[i] = 2.0f;
  }

  start_time = omp_get_wtime();

#pragma omp parallel num_threads(N_THREADS)
  {
      const int thread_id = omp_get_thread_num();

      int begin = range * thread_id;
      int end = begin + range;
      if (thread_id + 1 == N_THREADS)
          end = DATA_SIZE;

      for (int i = begin; i < end; i++) {
          x_f[i] += y_f[i];
      }
  }
  end_time = omp_get_wtime();
  duration = end_time - start_time;

在同一个应用程序中,我使用相同的代码添加具有双精度类型元素的向量。程序似乎运行良好,但结果对我来说有点奇怪,因为 float 计算时间比 double 大几倍。

Float      1       2       3       4      5        6      7        8       9      10      11      12
1024    0,0036  0,4535  0,6875  0,9443  1,1653  1,5068  1,6951  2,0447  2,3546  2,6611  3,1319  3,1468
double     1       2       3       4       5       6       7       8       9       10      11     12
1024    0,0004  0,0014  0,0016  0,0019  0,0018  0,0018  0,002   0,0021  0,0024  0,0028  0,0045  0,0036

1-12 是 OmpeMP 线程数,1024 是元素向量大小。时间以毫秒为单位。有人可以向我解释,为什么会发生或我做错了什么吗?我是 C 和 OpenMP 的新手,我不知道为什么结果如上。

编辑:完整源代码如下:

#include <omp.h>
#include <stdio.h>
#include <stdlib.h>


int main(int argc, char const *argv[]) {
  double start_time, end_time, duration;

  if (argv[1] == NULL || argv[2] == NULL) {
    printf("Error parsing data from input. Program will now close");
    return 2;
  }
  int DATA_SIZE = atoi(argv[1]);
  int N_THREADS = atoi(argv[2]);

  int range = DATA_SIZE / N_THREADS;

  // ===================== FLOAT ========================
  float *x_f = (float *)malloc(sizeof(float) * DATA_SIZE);
  float *y_f = (float *)malloc(sizeof(float) * DATA_SIZE);

  for (int i = 0; i < DATA_SIZE; i++) {

    x_f[i] = 1.0f;
    y_f[i] = 2.0f;
  }

  start_time = omp_get_wtime();
#pragma omp parallel num_threads(N_THREADS)
  {
    const int thread_id = omp_get_thread_num();

    int begin = range * thread_id;
    int end = begin + range;
    if (thread_id + 1 == N_THREADS)
      end = DATA_SIZE;

    for (int i = begin; i < end; i++) {
      x_f[i] += y_f[i];
      /*printf("x_f[%d]=%f\ty_f[%d]=%f\tThreadNum=%d\n", i, x_f[i], i,
      y_f[i],
       omp_get_thread_num());*/
    }
  }

  end_time = omp_get_wtime();
  duration = end_time - start_time;

  // Error checking
  for (int i = 0; i < DATA_SIZE; i++) {
    if (!(3.0 - x_f[i]) == 0) {
      printf("ERROR: %f\n", x_f[i]);
      break;
    }
  }
  free(x_f);
  free(y_f);

  printf("==========[FLOAT]==========\n");
  printf("Number of threads: %d\n", N_THREADS);
  printf("Data size: %d bytes\n", DATA_SIZE * sizeof(float));
  printf("ExecTime: %lf ms\n", duration * 1000);

  // ===================== DOUBLE ========================
  double *x_lf = (double *)malloc(sizeof(double) * DATA_SIZE);
  double *y_lf = (double *)malloc(sizeof(double) * DATA_SIZE);

  for (int i = 0; i < DATA_SIZE; i++) {

    x_lf[i] = 1.0f;
    y_lf[i] = 2.0f;
  }

  start_time = omp_get_wtime();

#pragma omp parallel num_threads(N_THREADS)
  {
    const int thread_id = omp_get_thread_num();

    int begin = range * thread_id;
    int end = begin + range;
    if (thread_id + 1 == N_THREADS)
      end = DATA_SIZE;

    for (int i = begin; i < end; i++) {
      x_lf[i] += y_lf[i];
      /*printf("x_f[%d]=%f\ty_f[%d]=%f\tThreadNum=%d\n", i, x_lf[i], i,
      y_lf[i], omp_get_thread_num());*/
    }
  }

  end_time = omp_get_wtime();
  duration = end_time - start_time;

  // Error checking
  for (int i = 0; i < DATA_SIZE; i++) {
    if (!(3.0 - x_lf[i]) == 0) {
      printf("ERROR: %f\n", x_lf[i]);
      break;
    }
  }
  free(x_lf);
  free(y_lf);

  printf("\n==========[DOUBLE]==========\n");
  printf("Number of threads: %d\n", N_THREADS);
  printf("Data size: %d bytes\n", DATA_SIZE * sizeof(double));
  printf("ExecTime: %lf ms\n", duration * 1000);
  
  return 0;
}

EDIT2: 全部 table 结果 Results

老实说,你显示的结果很奇怪。您如何将 for 循环更改为:

for (int i = thread_id; i < DATA_SIZE; i += N_THREADS) { /*  */ }

使用这个,我得到了 floatdouble 的可比时间。

不过我无法重现您的结果,因为不清楚您是如何定义 range 变量的。

我已经在 Compiler Explorer 上复制了您的代码。如果问题相当大,例如DATA_SIZE=10000000; 一切都按预期工作(浮点数更快,并发减少了 运行 时间)。对于现代处理器,添加 2 个大小为 1024 的数组非常简单,您只是在测量开销或人工制品...

编辑:我刚刚更改了代码的顺序(首先测量双精度,然后测量浮点数),这是我得到的:

==========[DOUBLE]==========
Number of threads: 2
Data size: 8192 bytes
ExecTime: 0.091095 ms
==========[FLOAT]==========
Number of threads: 2
Data size: 4096 bytes
ExecTime: 0.001610 ms

这证明您 运行 首先进行的任何测试都比第二次测试慢得多。我认为这是因为缓存:OpenMP 函数、数据、分配的内存等已经在缓存中,因此您的第二次测试 运行 快得多。

Here is the code refactored。也许有意义。使用 sheredom/ubench。 请找出编译器参数:-fopenmp -s -lm -O3。玩 -O 并找出不同之处。

(作为旁注,请阅读:32 OpenMP Traps for C++ Developers

代码如下:


#include "https://raw.githubusercontent.com/sheredom/ubench.h/master/ubench.h"

#include <assert.h>
#include <omp.h>
#include <stdio.h>
#include <stdlib.h>

    #define DATA_SIZE 100000
    #define N_THREADS 2
    #define range DATA_SIZE/N_THREADS

static struct {
    float x_f[DATA_SIZE] ;
    float y_f[DATA_SIZE] ;
    double x_d[DATA_SIZE] ;
    double y_d[DATA_SIZE] ;
} * app_data = 0 ;

static void app_start (void) 
{
    app_data = calloc(1, sizeof(*app_data) );
    assert(app_data) ;

  for (int i = 0; i < DATA_SIZE; ++i) {
    app_data->x_f[i] = 1.0f;
    app_data->y_f[i] = 2.0f;
    app_data->x_d[i] = 1.0;
    app_data->y_d[i] = 2.0;
  }
}

UBENCH( omp_measuring, adding_two_arrays_of_floats )
{
    #pragma omp parallel num_threads(N_THREADS)
  {
      const int thread_id = omp_get_thread_num();

      const int begin = range * thread_id;
      int end = begin + range;
      if (thread_id + 1 == N_THREADS)
          end = DATA_SIZE;

      for (int i = begin; i < end; i++) {
          app_data->x_f[i] += app_data->y_f[i];
      }
  }
}

UBENCH( omp_measuring, adding_two_arrays_of_doubles )
{
    #pragma omp parallel num_threads(N_THREADS)
  {
      const int thread_id = omp_get_thread_num();

      const int begin = range * thread_id;
      int end = begin + range;
      if (thread_id + 1 == N_THREADS)
          end = DATA_SIZE;

      for (int i = begin; i < end; i++) {
          app_data->x_d[i] += app_data->y_d[i];
      }
  }
}

static void app_end (void) { free(app_data); }


UBENCH_STATE();

int main(int argc, const char *const argv[])
{
    app_start();

    ubench_main(argc, argv);

    app_end();

}

买者自负

OMP 的全部意义(非常)没有实际意义,因为 OMP 显然只有在不使用优化的情况下才有效。很难击败 gnuc -O3.

Godbolt 的问题是它在处理大型数据集时会卡住。在 VLA(又名超大阵列)上使用 OMP 应该 显得有意义。

实际上,很少需要处理兆字节或千兆字节的数据集。然后人们可能会想象正在使用 GPU。没有 OMP。

奖金

app_start不是UBENCH规定的。可以将应用程序命令行参数传递给不同的 app_start :

static void app_start(const unsigned argc, char ** argv )
{
   // use app arguments here
}

int main (int argc, char ** argv) 
{
      app_start(argc,argv);
      ubench_main(argc,argv);
      app_end();
}