Доступ к памяти после разветвления очень медленный в Mac OS X - PullRequest
1 голос
/ 10 декабря 2010

Следующий код выполняется в 200 раз медленнее в Mac OS X, чем в Linux.Я не знаю почему, и проблема не кажется тривиальной.Я подозреваю ошибку в gcc на Mac или в Mac OS X или в моем оборудовании.

Код разветвляется процесс, который копирует таблицу страниц, но не память на MacOS X. Память копируется при записи, что происходит в цикле for в конце метода run.Там, для первых 4 вызовов run, все страницы должны быть скопированы, потому что каждая страница затронута.Для выполнения вторых 4 вызовов, когда пропуск составляет 512, необходимо копировать каждую вторую страницу, так как каждая вторая страница затрагивается.Интуитивно понятно, что первые 4 звонка должны занимать примерно вдвое больше времени, чем вторые 4 звонка, что совершенно не так.Для меня вывод программы следующий:

169.655ms
670.559ms
2784.18ms
16007.1ms
16.207ms
25.018ms
42.712ms
79.676ms

В Linux это

5.306ms
10.69ms
20.91ms
41.042ms
6.115ms
12.203ms
23.939ms
40.663ms

Общее время выполнения в Mac OS X составляет примерно 20 секунд, в Linux - около 0,5 секунды.для одной и той же программы оба раза компилируется с помощью gcc.Я пытался скомпилировать версию для Mac OS с gcc4, 4.2 и 4.4 - без изменений.

Есть идеи?

Код:

#include <stdint.h>
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <cstring>
#include <cstdlib>
#include <sys/time.h>

using namespace std;

class Timestamp
{
   private:
   timeval time;

   public:
   Timestamp() { gettimeofday(&time,0); }

   double operator-(const Timestamp& other) const { return static_cast<double>((static_cast<long long>(time.tv_sec)*1000000+(time.tv_usec))-(static_cast<long long>(other.time.tv_sec)*1000000+(other.time.tv_usec)))/1000.0; }
};

class ForkCoW
{
public:
   void run(uint64_t size, uint64_t skip) {
      // allocate and initialize array
      void* arrayVoid;
      posix_memalign(&arrayVoid, 4096, sizeof(uint64_t)*size);
      uint64_t* array = static_cast<uint64_t*>(arrayVoid);
      for (uint64_t i = 0; i < size; ++i)
         array[i] = 0;

      pid_t p = fork();
      if (p == 0)
         sleep(99999999);

      if (p < 0) {
         cerr << "ERRROR: Fork failed." << endl;
         exit(-1);
      }

      {
         Timestamp start;
         for (uint64_t i = 0; i < size; i += skip) {
            array[i] = 1;
         }
         Timestamp stop;
         cout << (stop-start) << "ms" << endl;
      }
      kill(p,SIGTERM);
   }
};

int main(int argc, char* argv[]) {
   ForkCoW f;
   f.run(1ull*1000*1000, 512);
   f.run(2ull*1000*1000, 512);
   f.run(4ull*1000*1000, 512);
   f.run(8ull*1000*1000, 512);

   f.run(1ull*1000*1000, 513);
   f.run(2ull*1000*1000, 513);
   f.run(4ull*1000*1000, 513);
   f.run(8ull*1000*1000, 513);
}

Ответы [ 6 ]

1 голос
/ 16 ноября 2014

Это не имеет ничего общего с C ++.Я переписал ваш пример на C и использую waitpid (2) вместо sleep / SIGCHLD и не могу воспроизвести проблему:

#include <errno.h>
#include <inttypes.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/types.h>

void ForkCoWRun(uint64_t size, uint64_t skip) {
      // allocate and initialize array
      uint64_t* array;
      posix_memalign((void **)&array, 4096, sizeof(uint64_t)*size);
      for (uint64_t i = 0; i < size; ++i)
             array[i] = 0;

      pid_t p = fork();
      switch(p) {
          case -1:
              fprintf(stderr, "ERRROR: Fork failed: %s\n", strerror(errno));
              exit(EXIT_FAILURE);
          case 0:
          {
              struct timeval start, stop;
              gettimeofday(&start, 0);
              for (uint64_t i = 0; i < size; i += skip) {
                 array[i] = 1;
              }
              gettimeofday(&stop, 0);

              long microsecs = (long)(stop.tv_sec - start.tv_sec) *1000000 + (long)(stop.tv_usec - start.tv_usec);
              printf("%ld.%03ld ms\n", microsecs / 1000, microsecs % 1000);
              exit(EXIT_SUCCESS);
          }
          default:
          {
              int exit_status;
              waitpid(p, &exit_status, 0);
              break;
          }
    }
}

int main(int argc, char* argv[]) {
    ForkCoWRun(1ull*1000*1000, 512);
    ForkCoWRun(2ull*1000*1000, 512);
    ForkCoWRun(4ull*1000*1000, 512);
    ForkCoWRun(8ull*1000*1000, 512);

    ForkCoWRun(1ull*1000*1000, 513);
    ForkCoWRun(2ull*1000*1000, 513);
    ForkCoWRun(4ull*1000*1000, 513);
    ForkCoWRun(8ull*1000*1000, 513);
}

, а в OS X 10.8, 10.9 и 10.10 я получаю такие результаты:

6.163 ms
12.239 ms
24.529 ms
49.223 ms
6.027 ms
12.081 ms
24.270 ms
49.498 ms
1 голос
/ 10 декабря 2010

Единственная причина такого долгого сна - это строка:

sleep(300000);

, что приводит к 300 секундам сна (300 * 1000).Возможно, реализация fork() в Mac OS X отличается от ожидаемой (и всегда возвращает 0).

0 голосов
/ 10 декабря 2010

Что происходит, когда у вас есть родитель waitpid() на дочернем элементе и вы гарантируете, что он завершен (и, чтобы быть уверенным, SIGCHLD гарантирует, что процесс будет запущен.) Возможно, что в Linux ребенок мог иметь выход завершился раньше, и теперь обработчик сбоев страниц должен выполнять меньше работы по копированию при записи, поскольку на страницы ссылается только один процесс.

Второе ... У вас есть идеи, какую работу fork() должен делать? В частности, это не следует считать «быстрым». Семантически говоря, он должен дублировать каждую страницу в адресном пространстве процесса. Исторически это то, что делал старый Unix, так они говорят. Это улучшается за счет первоначальной маркировки этих страниц как «копирование при записи» (то есть страницы помечаются только для чтения, и обработчик ошибок страниц ядра будет дублировать их при первой записи), но это все же много работы, а это значит, что ваш первый доступ к записи на каждой странице будет медленным.

Я поздравляю разработчиков Linux с тем, что они очень быстро получили fork() и их функцию копирования при записи для вашего шаблона доступа ... Но кажется очень странным утверждать, что это огромная проблема, если ядро ​​Mac OS не так хорошо, или если другие части системы генерируют другие шаблоны доступа, или что-то еще. Форк и запись страниц после форка не предполагается быстрым.

Полагаю, я пытаюсь сказать, что если вы перенесете свой код в ядро ​​с другим набором вариантов проектирования, и вдруг ваши 1015 * станут медленнее, сложнее, это часть перемещения вашего кода в другая ОС.

0 голосов
/ 10 декабря 2010

Проверено ли, что fork () работает:

int main() 
{
    pid_t pid = fork();

    if( pid > 0 ) {
        std::cout << "Parent\n";
    } else if( pid == 0 ) {
        std::cout << "Child\n";
    } else {
        std::cout << "Failed to fork!\n";
    }
}

Возможно, в MAC OS-X есть некоторые ограничения на разветвление дочерних процессов.

0 голосов
/ 10 декабря 2010

Вы выделяете 400 мегабайт один раз и еще раз из fork() (поскольку процесс дублируется, включая выделение памяти).

Причиной замедления может быть просто причина из fork()с двумя процессами у вас заканчивается физическая память, и вы используете swap память с диска.

Это обычно намного медленнее, чем при использовании физической памяти.

Редактировать следующие комментарии

Я предлагаю вам изменить код, чтобы начать измерение времени после записи в первый элемент массива.

  array[0] = 1;
  Timestamp start;
    for (int64_t i = 1; i < size; i++) {
       array[i] = 1;

Таким образом, время, используемоераспределение памяти после первой записи не будет учитываться в отметке времени .

0 голосов
/ 10 декабря 2010

Я подозреваю, что ваша проблема в том, что порядок выполнения в Linux заключается в том, что сначала запускается родитель, а затем выполняется родитель, а дочерний процесс завершается, потому что его родитель отсутствует, но в Mac OS сначала запускается дочерний, что включает 300 секунд сна.

В любом стандарте Unix абсолютно нет никакой гарантии, что два процесса после разветвления будут работать параллельно. Ваши утверждения о способности ОС делать это, несмотря на это.

Просто чтобы доказать, что это время ожидания, я заменил ваш код "30000" на "SLEEPTIME", скомпилировал и запустил его с g++ -DSLEEPTIME=?? foo.c && ./a.out:

SLEEPTIME   output
20          20442.1
30          30468.5
40          40431.4
10          10449  <just to prove it wasn't getting longer each run>
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...