Доступ к памяти после разветвления очень медленный в Mac OS X
Следующий код выполняется примерно в 200 раз медленнее в Mac OS X, чем в Linux. Я не знаю почему, и проблема не кажется тривиальной. Я подозреваю ошибку в gcc на Mac или в самой Mac OS X или в моем оборудовании.
Код разветвляет процесс, который будет копировать таблицу страниц, но не память в Mac OS 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 ответов
Это не имеет ничего общего с 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
Единственной причиной такого долгого сна была бы эта строка:
sleep(300000);
что приводит к 300 секундам сна (300*1000). Может быть, реализация fork()
на Mac OS X отличается от ожидаемого (и всегда возвращает 0).
Вы выделяете 400 мегабайт один раз и еще раз из fork()
(Так как процесс дублируется, включая распределение памяти).
Причиной медлительности может быть просто fork()
с двумя процессами, вы исчерпали доступную физическую память и используете swap
память с диска.
Это обычно намного медленнее, чем использование физической памяти.
Изменить следующие комментарии
Я предлагаю вам изменить код, чтобы начать измерение времени после записи в первый элемент массива.
array[0] = 1;
Timestamp start;
for (int64_t i = 1; i < size; i++) {
array[i] = 1;
Таким образом, время, использованное для выделения памяти после первой записи, не будет учитываться в метке времени.
Что происходит, когда у вас есть родитель waitpid()
на ребенка и убедитесь, что он вышел (и для безопасной работы SIGCHLD
чтобы гарантировать, что процесс будет повторен.) Возможно, что в Linux дочерний процесс мог завершиться раньше, и теперь обработчик сбоев страниц должен выполнять меньше работы для копирования при записи, поскольку на страницы ссылается только один процесс.
Во-вторых... Есть ли у вас какие-либо идеи, вид работы fork()
должен сделать? В частности, это не следует считать "быстрым". Семантически говоря, он должен дублировать каждую страницу в адресном пространстве процесса. Исторически это то, что делал старый Unix, так они говорят. Это улучшается за счет первоначальной маркировки этих страниц как "копирование при записи" (то есть страницы помечаются как "только для чтения", и обработчик ошибок страниц ядра будет дублировать их при первой записи), но это еще много работы, и это означает, что ваш первый доступ для записи на каждой странице будет медленным.
Я поздравляю разработчиков Linux для получения их fork()
и их реализация копирования при записи очень быстро для вашего шаблона доступа... Но кажется очень странным утверждать, что это огромная проблема, если ядро Mac OS не так хорошо, или если другие части системы генерируют разные шаблоны доступа или что-то еще. Форк и написание страниц после форка не должен быть быстрым.
Я полагаю, что я пытаюсь сказать, если вы перенесете свой код в ядро с другим набором вариантов дизайна и вдруг fork()
Это медленнее, сложнее, это часть перемещения вашего кода в другую ОС.
Я подозреваю, что ваша проблема в том, что порядок выполнения в 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>
Вы убедились, что 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 для разветвления дочерних процессов.