OS X 中的多线程 C 程序比 Linux 慢得多

Multi-threaded C program much slower in OS X than Linux

我写这篇文章是为了我已经完成并提交的 OS class 作业。我昨天 post 编辑了这个问题,但是由于 "Academic Honesty"规定我把它取下来,直到提交截止日期之后。

目的是学习如何使用临界区。有一个 data 数组,其中包含 100 个单调递增的数字,0...99,以及 40 个线程,每个线程随机交换两个元素 2,000,000 次。每秒一次 Checker 检查并确保每个数字中只有一个(这意味着没有发生并行访问)。

这是 Linux 次:

real    0m5.102s
user    0m5.087s
sys     0m0.000s

和OSX次

real    6m54.139s
user    0m41.873s
sys     6m43.792s

我 运行 一个 vagrant box 和 ubuntu/trusty64 在同一台机器上 运行ning OS X。它是四核 i7 2.3Ghz(最高 3.2Ghz)2012 rMBP。

如果我理解正确,sys 是系统开销,我无法控制,即便如此,41s 的用户时间表明线程可能是 运行ning 串行。

如果需要,我可以 post 所有代码,但我会 post 我认为相关的部分。我正在使用 pthreads 因为那是 Linux 提供的,但我假设它们在 OS X 上工作。

正在为 运行 swapManyTimes 例程创建 swapper 个线程:

for (int i = 0; i < NUM_THREADS; i++) {
    int err = pthread_create(&(threads[i]), NULL, swapManyTimes, NULL);
}

Swapper线程临界区,运行在一个for循环中200万次:

pthread_mutex_lock(&mutex);    // begin critical section
int tmpFirst = data[first];
data[first] = data[second];
data[second] = tmpFirst;
pthread_mutex_unlock(&mutex);  // end critical section

只创建了一个 Checker 线程,与 Swapper 相同。它通过遍历 data 数组并用 true 标记对应于每个值的索引来运行。之后,它检查有多少索引是空的。因此:

pthread_mutex_lock(&mutex);
for (int i = 0; i < DATA_SIZE; i++) {
    int value = data[i];
    consistency[value] = 1;
}
pthread_mutex_unlock(&mutex); 

它 运行 每秒调用 sleep(1) 一次 运行 通过它的 while(1) 循环。在所有 swapper 个线程加入后,此线程被取消并加入。

我很乐意提供更多信息,以帮助弄清楚为什么这在 Mac 上如此糟糕。我并不是真的在寻求代码优化方面的帮助,除非那是 OS X 的绊脚石。我尝试在 OS X 上同时使用 clanggcc-4.9 来构建它.

我已经在很大程度上复制了你的结果(没有清扫器):

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

#include <pthread.h>

pthread_mutex_t Lock;
pthread_t       LastThread;
int             Array[100];

void *foo(void *arg)
{
  pthread_t self  = pthread_self();
  int num_in_row  = 1;
  int num_streaks = 0;
  double avg_strk = 0.0;
  int i;

  for (i = 0; i < 1000000; ++i)
  {
    int p1 = (int) (100.0 * rand() / (RAND_MAX - 1));
    int p2 = (int) (100.0 * rand() / (RAND_MAX - 1));

    pthread_mutex_lock(&Lock);
    {
      int tmp   = Array[p1];
      Array[p1] = Array[p2];
      Array[p2] = tmp;

      if (pthread_equal(LastThread, self))
        ++num_in_row;

      else
      {
        ++num_streaks;
        avg_strk += (num_in_row - avg_strk) / num_streaks;
        num_in_row = 1;
        LastThread = self;
      }
    }
    pthread_mutex_unlock(&Lock);
  }

  fprintf(stdout, "Thread exiting with avg streak length %lf\n", avg_strk);

  return NULL;
}

int main(int argc, char **argv)
{
  int       num_threads = (argc > 1 ? atoi(argv[1]) : 40);
  pthread_t thrs[num_threads];
  void     *ret;
  int       i;

  if (pthread_mutex_init(&Lock, NULL))
  {
    perror("pthread_mutex_init failed!");
    return 1;
  }

  for (i = 0; i < 100; ++i)
    Array[i] = i;

  for (i = 0; i < num_threads; ++i)
    if (pthread_create(&thrs[i], NULL, foo, NULL))
    {
      perror("pthread create failed!");
      return 1;
    }

  for (i = 0; i < num_threads; ++i)
    if (pthread_join(thrs[i], &ret))
    {
      perror("pthread join failed!");
      return 1;
    }

  /*
  for (i = 0; i < 100; ++i)
    printf("%d\n", Array[i]);

  printf("Goodbye!\n");
  */

  return 0;
}

在 Linux (2.6.18-308.24.1.el5) 服务器上 Intel(R) Xeon(R) CPU E3-1230 V2 @ 3.30GHz

[ltn@svg-dc60-t1 ~]$ time ./a.out 1

real    0m0.068s
user    0m0.068s
sys 0m0.001s
[ltn@svg-dc60-t1 ~]$ time ./a.out 2

real    0m0.378s
user    0m0.443s
sys 0m0.135s
[ltn@svg-dc60-t1 ~]$ time ./a.out 3

real    0m0.899s
user    0m0.956s
sys 0m0.941s
[ltn@svg-dc60-t1 ~]$ time ./a.out 4

real    0m1.472s
user    0m1.472s
sys 0m2.686s
[ltn@svg-dc60-t1 ~]$ time ./a.out 5

real    0m1.720s
user    0m1.660s
sys 0m4.591s

[ltn@svg-dc60-t1 ~]$ time ./a.out 40

real    0m11.245s
user    0m13.716s
sys 1m14.896s

我的 MacBook Pro (Yosemite 10.10.2) 2.6 GHz i7, 16 GB 内存

john-schultzs-macbook-pro:~ jschultz$ time ./a.out 1

real    0m0.057s
user    0m0.054s
sys 0m0.002s
john-schultzs-macbook-pro:~ jschultz$ time ./a.out 2

real    0m5.684s
user    0m1.148s
sys 0m5.353s
john-schultzs-macbook-pro:~ jschultz$ time ./a.out 3

real    0m8.946s
user    0m1.967s
sys 0m8.034s
john-schultzs-macbook-pro:~ jschultz$ time ./a.out 4

real    0m11.980s
user    0m2.274s
sys 0m10.801s
john-schultzs-macbook-pro:~ jschultz$ time ./a.out 5

real    0m15.680s
user    0m3.307s
sys 0m14.158s
john-schultzs-macbook-pro:~ jschultz$ time ./a.out 40

real    2m7.377s
user    0m23.926s
sys 2m2.434s

我用了 Mac 大约 12 倍的挂钟时间来完成 40 个线程,这与 Linux + gcc 的非常旧版本相比。

注意:我将代码更改为每个线程执行 1M 交换。

看起来 OSX 比 Linux 多做了 很多 的工作。也许它比 Linux 更精细地交织它们?

编辑 更新代码以记录线程立即重新捕获锁的平均次数:

Linux

[ltn@svg-dc60-t1 ~]$ time ./a.out 10
Thread exiting with avg streak length 2.103567
Thread exiting with avg streak length 2.156641
Thread exiting with avg streak length 2.101194
Thread exiting with avg streak length 2.068383
Thread exiting with avg streak length 2.110132
Thread exiting with avg streak length 2.046878
Thread exiting with avg streak length 2.087338
Thread exiting with avg streak length 2.049701
Thread exiting with avg streak length 2.041052
Thread exiting with avg streak length 2.048456

real    0m2.837s
user    0m3.012s
sys 0m16.040s

Mac OSX

john-schultzs-macbook-pro:~ jschultz$ time ./a.out 10
Thread exiting with avg streak length 1.000000
Thread exiting with avg streak length 1.000000
Thread exiting with avg streak length 1.000000
Thread exiting with avg streak length 1.000000
Thread exiting with avg streak length 1.000000
Thread exiting with avg streak length 1.000000
Thread exiting with avg streak length 1.000000
Thread exiting with avg streak length 1.000000
Thread exiting with avg streak length 1.000000
Thread exiting with avg streak length 1.000000

real    0m34.163s
user    0m5.902s
sys 0m30.329s

因此,OSX 更均匀地共享其锁,因此有更多的线程挂起和恢复。

MacOSX 和 Linux 实现 pthread 的方式不同,导致这种缓慢的行为。特别是 MacOSX 不使用自旋锁(根据 ISO C 标准,它们是可选的)。对于像这样的示例,这可能会导致非常非常慢的代码性能。

The OP does not mention/show any code that indicates the thread(s) sleep, wait, give up execution, etc and all the threads are at the same 'nice' level.  

所以一个单独的线程可能会得到 CPU 并且在它完成所有 200 万次执行之前不会释放它。

这将导致在 linux 上执行上下文切换的时间最短。

然而,在 MAC OS 上,一个执行只允许一个 'time slice' 执行,然后另一个 'ready to execute' thread/process 被允许执行.

这意味着更多的上下文切换。

上下文切换在 'sys' 时间内执行。

结果是 MAC OS 将花费更长的时间来执行。

为了公平起见,您可以通过插入 nanosleep() 或通过

调用释放执行来强制上下文切换
#include <sched.h>

then calling

int sched_yield(void);