Многопоточная программа C в OS X намного медленнее, чем Linux

Я написал это для назначения класса ОС, которое я уже выполнил и передал. Вчера я опубликовал этот вопрос, но из-за правил "Академической честности" я снял его до истечения срока подачи заявки.

Целью было научиться использовать критические разделы. Существует массив data с 100 монотонно увеличивающимися числами, 0... 99 и 40 потоками, которые случайным образом меняют местами два элемента по 20000 раз каждый. Через секунду проходит a 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.2 ГГц) 2012 rMBP.

Если я правильно понимаю, sys - это системные издержки, над которыми я не контролирую, и даже тогда 41 с пользовательского времени предполагает, что потоки выполняются серийно.

Я могу опубликовать весь код, если это необходимо, но я опубликую бит, которые, по моему мнению, актуальны. Я использую 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 2 миллиона раз:

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.

Ответ 1

MacOSX и Linux реализуют pthread по-разному, вызывая это медленное поведение. В частности, MacOSX не использует винтовые блоки (они являются необязательными в соответствии со стандартом ISO C). Это может привести к очень медленной работе кода с примерами, подобными этому.

Ответ 2

Я продублировал ваш результат в значительной степени (без подметания):

#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,30 ГГц

[[email protected] ~]$ time ./a.out 1

real    0m0.068s
user    0m0.068s
sys 0m0.001s
[[email protected] ~]$ time ./a.out 2

real    0m0.378s
user    0m0.443s
sys 0m0.135s
[[email protected] ~]$ time ./a.out 3

real    0m0.899s
user    0m0.956s
sys 0m0.941s
[[email protected] ~]$ time ./a.out 4

real    0m1.472s
user    0m1.472s
sys 0m2.686s
[[email protected] ~]$ time ./a.out 5

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

[[email protected] ~]$ time ./a.out 40

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

На моем MacBook Pro (Yosemite 10.10.2) 2.6 ГГц i7, 16 ГБ памяти

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

Потребовалось примерно в 12 раз больше времени на настенные часы, чтобы выполнить 40 потоков, и это по сравнению с очень старой версией Linux + gcc.

ПРИМЕЧАНИЕ. Я изменил свой код, чтобы сделать 1M свопов в потоке.

Похоже, что в конфликте OSX делает много больше работы, чем Linux. Может быть, это чередуется с ними намного лучше, чем Linux?

EDIT Обновленный код для записи avg количества раз, когда поток немедленно захватывает блокировку:

Linux

[[email protected] ~]$ 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 делится своими блокировками гораздо более равномерно и, следовательно, имеет еще множество приостановок и повторений потоков.

Ответ 3

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.  

поэтому отдельный поток может получить процессор и не выпускать его до тех пор, пока он не завершит выполнение всех 2mil.

Это приведет к минимальному времени выполнения контекстных переключателей в linux.

Тем не менее, в ОС MAC для выполнения предоставляется только "временной срез" для выполнения, прежде чем разрешено выполнение "готового к исполнению" потока/процесса.

Это означает гораздо больше контекстных переключателей.

Контекстные коммутаторы выполняются в режиме "sys".

В результате MAC-OS займет гораздо больше времени.

Для игрового поля вы можете принудительно переключать контекстные переключатели, вставляя нанослое() или вызов, чтобы освободить выполнение через

#include <sched.h>

then calling

int sched_yield(void);