在Mac OS X上,fork之后的内存访问速度极慢

时间:2010-12-10 17:59:06

标签: macos performance fork

以下代码在Mac OS X上的执行速度比在Linux上慢200倍。我不知道为什么,问题似乎是微不足道的。我怀疑Mac上或Mac OS X本身或我硬件中的gcc存在错误。

代码分叉将复制页面表的进程,但 Mac OS X上的内存。内存在写入时被复制,在运行结束时在for循环中发生方法。在那里,对于前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编译的完全相同的程序。我已经尝试用gcc4,4.2和4.4编译mac os版本 - 没有变化。

有什么想法吗?

代码:

#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 个答案:

答案 0 :(得分:1)

只有这么长时间睡眠的原因才是这一行:

sleep(300000);

导致300秒的睡眠(300 * 1000)。也许{os} x上fork()的实现与你期望的不同(并且它总是返回0)。

答案 1 :(得分:1)

这与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

答案 2 :(得分:0)

我怀疑你的问题是在Linux上执行的顺序是它首先运行父进程,然后父执行并且子进程终止,因为它的父进程已经消失,但是在Mac OS上它首先运行子进程,这涉及到300秒睡觉。

在任何Unix标准中,绝对无法保证fork之后的两个进程将并行运行。关于操作系统能力的断言尽管如此。

为了证明这是睡眠时间,我用“SLEEPTIME”替换了“30000”代码并编译并用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>

答案 3 :(得分:0)

您正在分配400兆字节,并再次从fork()分配(因为该过程是重复的,包括内存分配)。

缓慢的原因可能只是从具有两个进程的fork()开始,您的可用物理内存耗尽,并且正在使用磁盘中的swap内存。

这通常比使用物理内存慢得多。

编辑以下评论

我建议您在写入数组的第一个元素后更改代码以开始计时测量。

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

这样, timestamp 中不会考虑第一次写入后内存分配所用的时间。

答案 4 :(得分:0)

当您在子项上拥有父waitpid()并确保它已退出(以及安全处理SIGCHLD以确保获取该进程时)会发生什么。)在Linux上似乎可能孩子本可以早点退出,现在页面错误处理程序必须减少写入时复制的工作,因为页面只由一个进程引用。

第二......你知道fork()必须做的工作吗?特别是它不应该被假定为“快”。从语义上讲,它必须复制进程地址空间中的每个页面。他们说,从历史上看,这就是旧的Unix所做的。通过最初将这些页面标记为“写时复制”(即,页面被标记为只读,内核的页面错误处理程序将在第一次写入时复制它们)来改进这一点,但这仍然是 / i>很多工作,这意味着你在每一页上的第一次写访问都会很慢。

我祝贺Linux开发人员为您的访问模式快速获取他们的fork()和他们的写时复制实现......但是,如果Mac OS的话,声称这是一个很大的问题似乎是一件非常奇怪的事情。内核不是那么好,或者系统的其他部分碰巧生成不同的访问模式,或者其他什么。分叉,并在分叉后写页面,假设是快速的。

我想我想说的是,如果您将代码移动到具有不同设计选择的内核,并且突然间fork()更慢,更难,这是移动您的一部分代码到不同的操作系统。

答案 5 :(得分:-1)

您是否确认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对分支子进程有一些限制。