【问题标题】:Multi-threaded program not faster than single thread on Mac, but on Linux, unless Clang is used多线程程序在 Mac 上不比单线程快,但在 Linux 上,除非使用 Clang
【发布时间】:2013-10-26 11:43:05
【问题描述】:

我正在编写一个 C 程序,它使用 pthreads 进行一些矩阵乘法 (C=A*B),然后计算结果的最大行和范数,这是唯一需要同步作为矩阵的部分乘法本身是独立分布的。每个线程从矩阵 A 的行中获取自己的切片,然后将其行乘以 B 并将它们存储到 C 的相应行中。因此,所有线程在读取相同的内存位置时写入不同的内存位置,而不会被修改。

现在的问题是,当我在我的 MacBook(2012 年中,双核)上用两个线程编译程序时,线程版本几乎与没有线程的版本一样快,无论我制作多大的矩阵.可以预期,对于小矩阵大小,线程生成的开销等会消除加速,但这不是这里的问题。 奇怪的是,如果我在 Red Hat Linux 服务器上运行相同的程序,加速非常明显,而在我的计算机上,串行版本总是快一点。服务器有 16 个核心,但我只使用了两个线程(否则将毫无意义)。

有没有人对为什么同一个程序在服务器上显示加速但在我的 MacBook 上没有任何建议? 这是一些代码,让你看看我在做什么。

编辑:我尝试在 Mac 上使用 clang 而不是 gcc 进行编译,并且 - 神奇地 - 正如您所期望的那样,加速就在那里。欢迎任何解释,为什么 gcc 不能在不同的 CPU 上分配线程,但 clang 可以。有人会认为我不是第一个遇到这种情况的人。

#include <stdlib.h>
#include <sys/time.h>
#include <stdio.h>
#include "assignment3.h"

int main(int argc, const char *argv[])
{
   int n, p = 1;
   printf("Please enter the number of processors.\n > ");
   scanf("%d", &p);
   do {
      printf("Please enter the matrix factor "
            "(matrix size is a multiple of processor number).\n > ");
         scanf("%d", &n);
   } while(n % p);
   /* printf("n and p have values %d and %d\n", n, p); */

   // make matrices
   double **A, **B, **C;
   A = malloc(n*sizeof(double *));
   B = malloc(n*sizeof(double *));
   C = malloc(n*sizeof(double *));
   if (A == NULL || B == NULL || C == NULL) {
      return 1;
   }
   A[0] = malloc(n*n*sizeof(double));
   B[0] = malloc(n*n*sizeof(double));
   C[0] = calloc(n*n, sizeof(double));
   int i;
   for (i = 1; i < n; i++) {
      A[i] = A[0] + i*n;
      B[i] = B[0] + i*n;
      C[i] = C[0] + i*n;
   }

   // Fill with random values between 0 and 1
   fillMatrix(n, A);
   fillMatrix(n, B);

   pthread_t *workers = malloc(p * sizeof(pthread_t));
   thread_data_t *data = malloc(p * sizeof(thread_data_t));
   pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
   double global_norm;
   void **status = malloc(sizeof(void *)); // leaving out malloc is fine on osx, not on linux

   for (i = 0; i < p; i++) {
      data[i].n = n;
      data[i].p = p;
      data[i].my_row = i * n/p;
      data[i].A = A;
      data[i].B = B;
      data[i].C = C;
      data[i].mutex = &mutex;
      data[i].global_norm = &global_norm;
   }


   struct timeval tv1, tv2;
   struct timezone tz;

   gettimeofday(&tv1, &tz);
   for (i = 0; i < p; i++) // multiply uses ATLAS to multiply this thread's portion of the matrices
      pthread_create(&workers[i], NULL, multiply, &data[i]);

   for (i = 0; i < p; i++) pthread_join(workers[i], status);
   gettimeofday(&tv2, &tz);
   double elapsed = (double) (tv2.tv_sec - tv1.tv_sec) + (double)
      (tv2.tv_usec - tv1.tv_usec) * 1.e-6;

   // do non-parallel computation
   struct timeval tv3, tv4;
   struct timezone tz2;
   gettimeofday(&tv3, &tz2);
   double global_norm_serial = multiply_serial(A, B, C, n); // plain serial multiply with ATLAS
   gettimeofday(&tv4, &tz2);
   double elapsed_serial = (double) (tv4.tv_sec - tv3.tv_sec) + (double)
      (tv4.tv_usec - tv3.tv_usec) * 1.e-6;

   printf("Time elapsed for parallel execution: %lf seconds.\n", elapsed);
   printf("Time elapsed for serial execution: %lf seconds.\n", elapsed_serial);



   /* puts("Matrix A:"); */
   /* printMatrix(n, A); */
   /* puts("Matrix B:"); */
   /* printMatrix(n, B); */
   /* puts("Matrix C:"); */
   /* printMatrix(n, C); */
   printf("The maximum row sum norm is %lf.\n", global_norm);
   printf("The maximum (serial) row sum norm is %lf.\n", global_norm_serial);

   free(A[0]);
   free(B[0]);
   free(C[0]);
   free(A);
   free(B);
   free(C);
   free(workers);
   free(data);
   pthread_mutex_destroy(&mutex);
   return 0;
}

EDIT2: 我不知道这有什么帮助,但这就是multiply 过程的作用:

void *multiply(void *arg){
   thread_data_t *data = arg;
   int n = data->n, 
       p = data->p, 
       my_row = (*data).my_row;
   cblas_dgemm(CblasRowMajor, CblasNoTrans, CblasNoTrans, n/p, n, n, 1.0,
         (data->A)[my_row], n, (data->B)[0], n, 0.0, (data->C)[my_row], n);

   pthread_mutex_lock(data->mutex);
   double max = findMax(data->C + my_row, n, n/p, data->my_row);
   if (*(data->global_norm) < max) 
      *(data->global_norm) = max;
   pthread_mutex_unlock(data->mutex);

   pthread_exit(NULL);
   return NULL;
}

thread_data_t 类型有以下成员:

typedef struct {
   int n, p, my_row;
   double **A;
   double **B;
   double **C;
   double *global_norm;
   pthread_mutex_t *mutex;
} thread_data_t;

【问题讨论】:

  • 看看实际的线程函数multiply()可能会有所帮助。
  • @MichaelBurr:查看已编辑的问题。
  • 你不需要为status(你忘了free)分配内存,要么声明一个void* status;变量并调用pthread_join(workers[i], &amp;status),要么干脆做pthread_join(workers[i], NULL)
  • 很公平。我这样做是因为我在 linux 上遇到了段错误(虽然不是 mac),当将 status 作为指向指针的指针未初始化时,但它就像你建议的那样工作,只声明一个简单的指针并使用 &amp;on它。
  • 您的性能可能会受到内存带宽的限制。

标签: c multithreading macos pthreads clang


【解决方案1】:

好吧,不管怎样,我的导师和我似乎已经发现了问题所在。我没有提供任何关于我如何编译和链接我的程序的信息,所以没有人可能真的猜到了。至少不是没有天才的罢工。

对于没有显示任何加速的案例的编译是这样完成的:

gcc-mp-4.7 $(CFILES) $(MANUALPROG) -lcblas -latlas -Wall -o assignment3

CFILES 是我的源文件,MANUALPROG 只是两个不同版本之间的切换。

嗯,我使用的是 MacPorts 安装的 gcc 4.7。我没有具体包括libblas.a 的位置,因为我假设它在这个编译器的标准搜索路径中,因为没有错误。我认为这个搜索路径包括 MacPorts 目录/opt/local/lib,其中libblas.a 和libatlas.a 驻留。所以我假设 gcc 会链接到这些库。嗯,没有。

如果不是上面的,我使用

gcc-mp-4.7 -L/opt/local/lib $(CFILES) $(MANUALPROG) -lcblas -latlas -Wall -o assignment3

然后加速是可见的。因此,该问题与 clang 或特定编译器无关,Apple 的 gcc 也是如此。这只是使用了哪些库的问题。

所以,由于 OS X 没有ldd 工具,我在我的可执行文件上使用了otool -L,发现在第一种情况下,输出是这样的

$> otool -L assignment3
assignment3:
    /System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libBLAS.dylib (compatibility version 1.0.0, current version 1.0.0)
    /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 159.1.0)
    /opt/local/lib/libgcc/libgcc_s.1.dylib (compatibility version 1.0.0, current version 1.0.0)

相反,对于第二种情况,是这样的

$> otool -L assignment3
assignment3:
    /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 159.1.0)
    /opt/local/lib/libgcc/libgcc_s.1.dylib (compatibility version 1.0.0, current version 1.0.0)

事实证明,如果我没有明确指定库所在的路径,编译器会动态链接到 Apple 提供的 BLAS 版本。通过显式包含 MacPorts 版本,可以防止这种情况发生。

现在,回到为什么一个版本更快:我的导师说 BLAS/ATLAS 有不同的实现,它们本身可以是多线程的,也可以不是。现在看起来 Apple 的版本是多线程的,而 MacPorts 不是。为什么?因为假设我正在调用的 BLAS 例程本身是线程化的,那么我自己创建线程然后在每个线程中调用例程或者我是否在一次调用例程中放入整个矩阵都没有关系——它将是无论如何分成线程。当然,我创建自己的线程会消耗更多时间,这解释了“并行”执行时间总是稍长一点的事实。

另一方面,如果 BLAS 例程没有线程,那么我们在创建自己的线程时应该会看到一些不同 - 这就是发生的情况。静态链接到 MacPorts 库意味着程序中有串行 BLAS 调用。因此,当我将它们划分为线程时,我们应该会看到加速——而且确实如此。 事实上,动态链接到 Apple 框架时的执行时间与静态链接到 MacPorts 库并使用我自己的线程的版本的执行时间大致相同(略少)。

我无法确定一个或另一个 BLAS 实现是否是线程化的,但解释符合数据。

【讨论】:

    【解决方案2】:

    我认为最可能的原因是您不能确定这 2 个线程会在不同的处理器上运行。您需要研究操作系统如何决定分配处理器资源。

    【讨论】:

    • 也许吧,但我只是假设既然有两个处理器,它们也会被使用。我现在在我的 Mac 上使用 Clang 而不是 gcc 进行编译,突然间,is 是人们所期望的加速。为什么会这样?
    • 我的回答错了吗?如果做负 1 的人能说出它有什么问题,那将会有所帮助。我是来学习的,也是来回答的。
    • 虽然我没有投反对票,但我确实认为您的答案过于简单化,因为现代操作系统不会将您的线程放在同一个核心上,当您拥有超过一个我认为。那将是愚蠢的。但我对该主题也没有深入的了解。
    • @Spalteer 您声称这是“最可能的原因”是没有根据的。我承认这在理论上是可能的,但似乎荒谬的是不可能的。如果有一件事可能完全理解如何从 CPU 中获取性能,那就是调度程序。
    猜你喜欢
    • 2019-10-15
    • 1970-01-01
    • 2021-08-07
    • 2016-08-09
    • 1970-01-01
    • 1970-01-01
    • 2013-11-22
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多