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


我为OS课程作业写了这篇文章,我已经完成并递交了。我昨天发布了这个问题,但由于“学术诚实”规定,我把它推迟到提交截止日期之后。

目的是学习如何使用关键部分。有一个 data 数组有100个单调递增的数字,0 ... 99和40个线程,每个线程随机交换两个元素2,000,000次。一秒钟一次 Checker通过并确保每个数字只有一个(这意味着没有发生并行访问)。

以下是Linux时代:

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

和OS X次

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

我带着一个流浪盒 ubuntu/trusty64 在运行OS X的同一台机器上。它是一款四核i7 2.3Ghz(高达3.2Ghz)2012 rMBP。

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

如果需要,我可以发布所有代码,但我会发布我认为相关的位。我在用 pthreads 因为这是Linux提供的,但我认为它们适用于OS X.

创建 swapper 线程运行 swapManyTimes 常规:

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的绊脚石。我试过用两者来构建它 clang 和 gcc-4.9 在OS X上。


10643
2018-03-05 22:07


起源

与问题无关: p线程意味着POSIX线程,所以如果OS X与POSIX兼容“我认为它是!“, 然后 pthreads 应该在OS X中工作。 题:编译代码时是否使用了优化? - Iharob Al Asimi
我尝试过 -O0, -O1 和 -O2 没有区别 - Alex Popov
@ JS1检查器每次运行时都会打印一个星号。我大概每秒钟看一个星号,所以我怀疑这是个问题。检查员不必与交换者竞争。如果它永远不会运行它无关紧要。 Swappers运行1 ... 2,000,000 for循环,然后退出。 - Alex Popov
也许OS X有一个糟糕的pthreads实现? - jschultz410
@ jschultz410这当然是可能的,但我认为很难把它搞砸几个这样的数量级。这就是我认为错误可能是我的错误的原因。 - Alex Popov


答案:


MacOSX和Linux以不同方式实现pthread,导致这种缓慢的行为。具体来说,MacOSX不使用自旋锁(根据ISO C标准,它们是可选的)。这可能导致非常非常慢的代码性能,例如这个例子。


6
2018-03-05 23:16



为什么会 不 使用自旋锁导致行为缓慢?我认为自旋锁是浪费CPU,而互斥锁只会导致进程重新执行其执行状态,除非它设法过去。如果你能解释一下为什么不使用自旋锁 比较慢 我很乐意将此标记为已回答:) - Alex Popov
@Alex我找到了 这个答案 这解释了使用自旋锁与否的权衡。此外,它下面的答案添加了更多信息。 - JS1
@AlexPopov - 自旋锁很慢 什么时候有争用  - 在典型的MT代码中很少出现这种情况。如果持有太长时间,自旋锁也可能会变成更重负荷(但CPU密集度更低)的锁。另见: futexes的 - Brett Hale
当我外出吃卷饼时,@ JS1和布雷特打败了我。我生命中的故事。 - Log_n


我已经很好地复制了你的结果(没有扫地机):

#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)服务器上英特尔(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

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

注意:我更改了我的代码,每个线程执行1M交换。

看起来OSX正在进行竞争 批量 比Linux更多的工作。也许它比Linux更精细地交织它们?

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

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更均匀地共享其锁,因此具有更多的线程挂起和恢复。


5
2018-03-05 22:50



快速搜索“osx pthread mutex实现”表明其他人已经看到类似的数字,并且OSX pthread互斥锁可能不会使用用户空间自旋锁。如果每次锁定调用都是一个系统调用,那么可能会解释结果。 - Useless


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,并且在完成所有2mil执行之前不会释放它。

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

但是,在MAC OS上,执行只允许执行“时间片”,然后才允许执行另一个“准备执行”线程/进程。

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

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

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

即使是游戏领域,您也可以通过插入nanosleep()或调用来释放执行来强制进行上下文切换

#include <sched.h>

then calling

int sched_yield(void);

1
2018-03-06 01:09



我的交换器永远不会睡觉,但是由于系统计时器,他们不应该超时吗?我发现很难理解他们没有被抢先一步。 - Alex Popov
通常,非实时版本的Linux操作系统不会执行抢占式上下文切换 - user3629249
要么我缺少某些东西,@ user3629249,要么你,但是线程确实在Linux上被抢占了,否则你就不会在程序中没有自己的并行性。或者我误解了你的评论?哦,这与RT功能无关。 - Ulrich Eckhardt