有人可以解释以下内存分配C程序的性能行为吗?

时间:2012-04-10 20:52:30

标签: c linux performance unix compiler-construction

在我的机器上时间A和时间B交换取决于A是否 是否定义(改变了调用两个calloc的顺序)。

我最初将此归因于分页系统。奇怪的是,什么时候 使用mmap代替calloc,情况更加眩晕 - 两个循环都需要相同的时间,如预期的那样。如 可以看到stracecalloc最终导致两个 mmap s,所以没有返回已经分配的记忆魔法。

我在Intel i7上运行Debian测试。

#include <stdlib.h>
#include <stdio.h>
#include <sys/mman.h>

#include <time.h>

#define SIZE 500002816

#ifndef USE_MMAP
#define ALLOC calloc
#else
#define ALLOC(a, b) (mmap(NULL, a * b, PROT_READ | PROT_WRITE,  \
                          MAP_PRIVATE | MAP_ANONYMOUS, -1, 0))
#endif

int main() {
  clock_t start, finish;
#ifdef A
  int *arr1 = ALLOC(sizeof(int), SIZE);
  int *arr2 = ALLOC(sizeof(int), SIZE);
#else
  int *arr2 = ALLOC(sizeof(int), SIZE);
  int *arr1 = ALLOC(sizeof(int), SIZE);
#endif
  int i;

  start = clock();
  {
    for (i = 0; i < SIZE; i++)
      arr1[i] = (i + 13) * 5;
  }
  finish = clock();

  printf("Time A: %.2f\n", ((double)(finish - start))/CLOCKS_PER_SEC);

  start = clock();
  {
    for (i = 0; i < SIZE; i++)
      arr2[i] = (i + 13) * 5;
  }
  finish = clock();

  printf("Time B: %.2f\n", ((double)(finish - start))/CLOCKS_PER_SEC);

  return 0;
}

我得到的输出:

 ~/directory $ cc -Wall -O3 bench-loop.c -o bench-loop
 ~/directory $ ./bench-loop 
Time A: 0.94
Time B: 0.34
 ~/directory $ cc -DA -Wall -O3 bench-loop.c -o bench-loop
 ~/directory $ ./bench-loop                               
Time A: 0.34
Time B: 0.90
 ~/directory $ cc -DUSE_MMAP -DA -Wall -O3 bench-loop.c -o bench-loop
 ~/directory $ ./bench-loop                                          
Time A: 0.89
Time B: 0.90
 ~/directory $ cc -DUSE_MMAP -Wall -O3 bench-loop.c -o bench-loop 
 ~/directory $ ./bench-loop                                      
Time A: 0.91
Time B: 0.92

5 个答案:

答案 0 :(得分:10)

您还应该使用malloc代替calloc进行测试。 calloc做的一件事就是用零填充分配的内存。

我相信你的情况是当你calloc arr1最后然后分配给它时,它已经被故障存入高速缓冲存储器,因为它是最后一个被分配和零填充的。首先calloc arr1而arr2秒,然后arr2的零填充将arr1推出缓存。

答案 1 :(得分:6)

猜猜我可以写更多或更少,特别是少即可。

原因可能因系统而异。然而;对于clib:

每次操作所用的总时间是相反的;如果你有时间 calloc +迭代。

即:

Calloc arr1 : 0.494992654
Calloc arr2 : 0.000021250
Itr arr1    : 0.430646035
Itr arr2    : 0.790992411
Sum arr1    : 0.925638689
Sum arr2    : 0.791013661

Calloc arr1 : 0.503130736
Calloc arr2 : 0.000025906
Itr arr1    : 0.427719162
Itr arr2    : 0.809686047
Sum arr1    : 0.930849898
Sum arr2    : 0.809711953

第一个calloc后来malloc的执行时间会更长 第二。在任何malloc(0)等之前的calloc之间的呼叫会使时间变得均匀 用于同一进程中的malloc类似调用(下面的说明)。一个可以 但是,如果一个人排成几行,这些电话的时间会略有下降。

然而,迭代时间将趋于平缓。

总之;使用的总系统时间最高,首先得到alloc。 然而,这是一个无法在过程限制中逃脱的开销。

有很多维护正在进行中。快速了解一些案例:


在页面上短缺

当进程请求内存时,它被提供虚拟地址范围。这个范围 通过页表转换为物理内存。如果页面翻译了字节 字节我们会很快得到巨大的页表。这是一个原因 内存范围以块或页面形式提供。页面大小是系统 依赖。该架构还可以提供各种页面大小。

如果我们查看上面代码的执行并从/proc/PID/stat添加一些读取 我们看到了这一点(Esp.note RSS):

PID Stat {
  PID          : 4830         Process ID
  MINFLT       : 214          Minor faults, (no page memory read)
  UTIME        : 0            Time user mode
  STIME        : 0            Time kernel mode
  VSIZE        : 2039808      Virtual memory size, bytes
  RSS          : 73           Resident Set Size, Number of pages in real memory
} : Init

PID Stat {
  PID          : 4830         Process ID
  MINFLT       : 51504        Minor faults, (no page memory read)
  UTIME        : 4            Time user mode
  STIME        : 33           Time kernel mode
  VSIZE        : 212135936    Virtual memory size, bytes
  RSS          : 51420        Resident Set Size, Number of pages in real memory
} : Post calloc arr1

PID Stat {
  PID          : 4830         Process ID
  MINFLT       : 51515        Minor faults, (no page memory read)
  UTIME        : 4            Time user mode
  STIME        : 33           Time kernel mode
  VSIZE        : 422092800    Virtual memory size, bytes
  RSS          : 51428        Resident Set Size, Number of pages in real memory
} : Post calloc arr2

PID Stat {
  PID          : 4830         Process ID
  MINFLT       : 51516        Minor faults, (no page memory read)
  UTIME        : 36           Time user mode
  STIME        : 33           Time kernel mode
  VSIZE        : 422092800    Virtual memory size, bytes
  RSS          : 51431        Resident Set Size, Number of pages in real memory
} : Post iteration arr1

PID Stat {
  PID          : 4830         Process ID
  MINFLT       : 102775       Minor faults, (no page memory read)
  UTIME        : 68           Time user mode
  STIME        : 58           Time kernel mode
  VSIZE        : 422092800    Virtual memory size, bytes
  RSS          : 102646       Resident Set Size, Number of pages in real memory
} : Post iteration arr2

PID Stat {
  PID          : 4830         Process ID
  MINFLT       : 102776       Minor faults, (no page memory read)
  UTIME        : 68           Time user mode
  STIME        : 69           Time kernel mode
  VSIZE        : 2179072      Virtual memory size, bytes
  RSS          : 171          Resident Set Size, Number of pages in real memory
} : Post free()

我们可以看到实际分配在内存中的页面被推迟arr2等待 页面请求;持续到迭代开始。如果我们之前添加malloc(0) calloc arr1我们可以注册,这两个数组都没有在物理上分配 迭代前的内存。


由于可能不使用页面,因此根据请求进行映射效率更高。 这就是为什么当进程即calloc足够数量的页面时 保留,但不一定实际分配在实内存中。

当引用地址时,会查询页面表。如果地址是 在未分配的页面中,系统提供页面错误和页面 随后被分配。已分配页面的总和称为 Resident 设置大小(RSS)。

我们可以通过迭代(触摸),即1/4来对我们的数组进行实验。 在此,我还在malloc(0)之前添加了calloc

Pre iteration 1/4:
RSS          : 171              Resident Set Size, Number of pages in real meory

for (i = 0; i < SIZE / 4; ++i)
    arr1[i] = 0;

Post iteration 1/4:
RSS          : 12967            Resident Set Size, Number of pages in real meory

Post iteration 1/1:
RSS          : 51134            Resident Set Size, Number of pages in real meory

为了进一步加快速度,大多数系统最近还会缓存N. 转换后备缓冲区(TLB)中的页表条目。


brk,mmap

当进程(c|m|…)alloc扩展堆的上限时 brk()sbrk()。这些系统调用很昂贵并且可以弥补 这个malloc会收集多个较小的电话给一个较大的brk()。

这也会影响free(),因为负brk()也是资源昂贵的 它们被收集并作为一个更大的操作进行。


对于巨大的要求;就像代码中的代码一样,malloc()使用mmap()The threshold为此,可由mallopt()配置,是受过教育的 值

我们可以通过修改代码中的SIZE来获得乐趣。如果我们利用 malloc.h并使用,

struct mallinfo minf = mallinfo();

(不,不是 milf ),我们可以展示这一点(注意ArenaHblkhd,...):

Initial:

mallinfo {
  Arena   :         0 (Bytes of memory allocated with sbrk by malloc)
  Ordblks :         1 (Number of chunks not in use)
  Hblks   :         0 (Number of chunks allocated with mmap)
  Hblkhd  :         0 (Bytes allocated with mmap)
  Uordblks:         0 (Memory occupied by chunks handed out by malloc)
  Fordblks:         0 (Memory occupied by free chunks)
  Keepcost:         0 (Size of the top-most releasable chunk)
} : Initial

MAX = ((128 * 1024) / sizeof(int)) 

mallinfo {
  Arena   :         0 (Bytes of memory allocated with sbrk by malloc)
  Ordblks :         1 (Number of chunks not in use)
  Hblks   :         1 (Number of chunks allocated with mmap)
  Hblkhd  :    135168 (Bytes allocated with mmap)
  Uordblks:         0 (Memory occupied by chunks handed out by malloc)
  Fordblks:         0 (Memory occupied by free chunks)
  Keepcost:         0 (Size of the top-most releasable chunk)
} : After malloc arr1

mallinfo {
  Arena   :         0 (Bytes of memory allocated with sbrk by malloc)
  Ordblks :         1 (Number of chunks not in use)
  Hblks   :         2 (Number of chunks allocated with mmap)
  Hblkhd  :    270336 (Bytes allocated with mmap)
  Uordblks:         0 (Memory occupied by chunks handed out by malloc)
  Fordblks:         0 (Memory occupied by free chunks)
  Keepcost:         0 (Size of the top-most releasable chunk)
} : After malloc arr2

然后我们从sizeof(int)中减去MAX并获取:

mallinfo {
  Arena   :    266240 (Bytes of memory allocated with sbrk by malloc)
  Ordblks :         1 (Number of chunks not in use)
  Hblks   :         0 (Number of chunks allocated with mmap)
  Hblkhd  :         0 (Bytes allocated with mmap)
  Uordblks:    131064 (Memory occupied by chunks handed out by malloc)
  Fordblks:    135176 (Memory occupied by free chunks)
  Keepcost:    135176 (Size of the top-most releasable chunk)
} : After malloc arr1

mallinfo {
  Arena   :    266240 (Bytes of memory allocated with sbrk by malloc)
  Ordblks :         1 (Number of chunks not in use)
  Hblks   :         0 (Number of chunks allocated with mmap)
  Hblkhd  :         0 (Bytes allocated with mmap)
  Uordblks:    262128 (Memory occupied by chunks handed out by malloc)
  Fordblks:      4112 (Memory occupied by free chunks)
  Keepcost:      4112 (Size of the top-most releasable chunk)
} : After malloc arr2

我们注册系统的工作方式与宣传的一样。如果分配的大小是 低于阈值sbrk使用,内存由malloc内部处理, 使用了其他mmap

这种结构也有助于防止内存碎片等。


指出malloc系列针对一般用途进行了优化。然而 可以修改mmap限制以满足特殊需求。

当/如果修改mmap阈值时,

Note this(并且通过100多行)。  

如果我们填写(触摸)arr1和arr2的每一页,可以进一步观察到这一点 在我们做时间之前:

Touch pages … (Here with page size of 4 kB)

for (i = 0; i < SIZE; i += 4096 / sizeof(int)) {
    arr1[i] = 0;
    arr2[i] = 0;
}

Itr arr1    : 0.312462317
CPU arr1    : 0.32

Itr arr2    : 0.312869158
CPU arr2    : 0.31

另见:


子注:

那么,CPU知道物理地址吗?全息

在记忆世界中,必须解决;)。核心硬件 这是内存管理单元(MMU)。作为一个整体的一部分 CPU或外部芯片。

操作系统在启动时配置MMU并定义各种访问 区域(只读,读写等)因此提供了一定程度的安全性。

我们凡人看到的地址是CPU使用的逻辑地址。该 MMU将其转换为物理地址

CPU的地址由两部分组成:页面地址和偏移量。 [PAGE_ADDRESS.OFFSET]

获取物理地址的过程我们可以有:

.-----.                          .--------------.
| CPU > --- Request page 2 ----> | MMU          |
+-----+                          | Pg 2 == Pg 4 |
      |                          +------v-------+
      +--Request offset 1 -+            |
                           |    (Logical page 2 EQ Physical page 4)
[ ... ]     __             |            |
[ OFFSET 0 ]  |            |            |
[ OFFSET 1 ]  |            |            |
[ OFFSET 2 ]  |            |            |     
[ OFFSET 3 ]  +--- Page 3  |            |
[ OFFSET 4 ]  |            |            |
[ OFFSET 5 ]  |            |            |
[ OFFSET 6 ]__| ___________|____________+
[ OFFSET 0 ]  |            |
[ OFFSET 1 ]  | ...........+
[ OFFSET 2 ]  |
[ OFFSET 3 ]  +--- Page 4
[ OFFSET 4 ]  |
[ OFFSET 5 ]  |
[ OFFSET 6 ]__|
[ ... ]

CPU的逻辑地址空间直接链接到地址长度。一个 32位地址处理器的逻辑地址空间为2 32 字节。 物理地址空间是系统可以承受的内存量。

还有碎片存储器的处理,重新校准等。

这将我们带入了交换文件的世界。如果进程请求更多内存 然后是物理上可用的;其他过程的一页或几页 转移到磁盘/交换及其页面&#34; 被盗&#34;通过请求过程。 MMU跟踪这个;因此CPU不必担心在哪里 记忆实际上是位于。


这进一步将我们带入了肮脏的记忆。

如果我们从/ proc / [pid] / smaps打印一些信息,更具体的范围 对于我们的数组,我们得到类似的结果:

Start:
b76f3000-b76f5000
Private_Dirty:         8 kB

Post calloc arr1:
aaeb8000-b76f5000
Private_Dirty:        12 kB

Post calloc arr2:
9e67c000-b76f5000
Private_Dirty:        20 kB

Post iterate 1/4 arr1
9e67b000-b76f5000
Private_Dirty:     51280 kB

Post iterate arr1:
9e67a000-b76f5000
Private_Dirty:    205060 kB

Post iterate arr2:
9e679000-b76f5000
Private_Dirty:    410096 kB

Post free:
9e679000-9e67d000
Private_Dirty:        16 kB
b76f2000-b76f5000
Private_Dirty:        12 kB

创建虚拟页面时,系统通常会清除虚拟页面中的脏位 页面。
当CPU写入该页面的一部分时,脏位被置位;因此 写了脏页的交换页面,跳过了干净的页面。


答案 2 :(得分:3)

这只是一个过程记忆图像何时按页面扩展的问题。

答案 3 :(得分:3)

简答

第一次调用calloc时,它明确地将内存清零。在下次调用时,它假定从mmap返回的内存已经清零。

<强>详情

以下是我检查过的一些结论,如果您愿意,可以尝试自己:

  1. 在第一次calloc来电之前插入ALLOC来电。在此之后,您将看到时间A和时间B的时间相同。

  2. 使用clock()功能查看每个ALLOC来电的时长。如果他们都使用calloc,您会看到第一次通话比第二次通话要长得多。

  3. 使用time计算calloc版本和USE_MMAP版本的执行时间。当我这样做时,我发现USE_MMAP的执行时间一直略低。

  4. 我使用strace -tt -T运行,显示系统调用的时间和花费的时间。以下是输出的一部分:

  5. Strace输出:

    21:29:06.127536 mmap(NULL, 2000015360, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fff806fd000 <0.000014>
    21:29:07.778442 mmap(NULL, 2000015360, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fff093a0000 <0.000021>
    21:29:07.778563 times({tms_utime=63, tms_stime=102, tms_cutime=0, tms_cstime=0}) = 4324241005 <0.000011>
    

    您可以看到第一次mmap调用花了0.000014秒,但在下一次系统调用之前已经过1.5秒。然后,第二次mmap调用花了0.000021秒,然后在几百微秒后调用了times

    我还使用gdb逐步完成了部分应用程序执行,并看到第一次调用calloc导致多次调用memset,而第二次调用calloc没有打电话给memset。如果您有兴趣,可以查看calloc here的源代码(查找__libc_calloc)。至于为什么calloc在第一次通话时正在进行memset,而不是后续的,我不知道。但我相信这可以解释你所提出的行为。

    至于为什么被归零的数组memset具有改进的性能,我的猜测是因为它是加载到TLB而不是缓存中的值,因为它是一个非常大的数组。无论您询问性能差异的具体原因是两个calloc调用在执行时表现不同。

答案 4 :(得分:2)

摘要 在分析分配数组所需的时间时,会解释时差。最后分配的calloc只需要多一点时间,而另一个(或者当使用mmap时)只需要时间。第一次访问时,内存中的实际分配可能会延迟。

我对Linux上内存分配的内部知之甚少。但是我稍微修改了你的脚本:我为每个数组操作添加了第三个数组和一些额外的迭代。我考虑过Old Pro的说法,没有考虑分配阵列的时间。

结论:使用calloc比使用mmap进行分配需要更长的时间(mmap virtualy在分配内存时没有时间使用,它可能会在以后的第一次访问时推迟),并且使用我的程序在使用之间几乎没有差别用于整个程序执行的mmap或calloc。

无论如何,首先要注意的是,内存分配都发生在内存映射区域而不是堆中。为了验证这一点,我添加了一个快速的“脏”暂停,以便您可以检查进程的内存映射(/ proc // maps)

现在问题,最后一个带有calloc的已分配数组似乎真的在内存中分配(未推迟)。由于arr1和arr2现在表现完全相同(第一次迭代很慢,后续迭代更快)。 Arr3在第一次迭代时更快,因为内存是先前分配的。使用A宏时,arr1会从中受益。我的猜测是内核已经在内存中预先分配了最后一个calloc的数组。为什么?我不知道......我只用一个数组测试过它(所以我删除了所有出现的arr2和arr3),然后我(大致)对所有10次arr1迭代都进行了测试。

malloc和mmap的行为相同(结果未在下面显示),第一次迭代很慢,后续迭代对所有3个数组都更快。

注意:所有结果都是在各种gcc优化标志(-O0到-O3)之间保持一致,所以看起来行为的根源不是来自某种gcc optimsation。

注2:使用GCC 4.6.3在Ubuntu Precise Pangolin(内核3.2)上进行测试

#include <stdlib.h>
#include <stdio.h>
#include <sys/mman.h>

#include <time.h>

#define SIZE 500002816
#define ITERATION 10

#if defined(USE_MMAP)
#  define ALLOC(a, b) (mmap(NULL, a * b, PROT_READ | PROT_WRITE,  \
                          MAP_PRIVATE | MAP_ANONYMOUS, -1, 0))
#elif defined(USE_MALLOC)
#  define ALLOC(a, b) (malloc(b * a))
#elif defined(USE_CALLOC)
#  define ALLOC calloc
#else
#  error "No alloc routine specified"
#endif

int main() {
  clock_t start, finish, gstart, gfinish;
  start = clock();
  gstart = start;
#ifdef A
  unsigned int *arr1 = ALLOC(sizeof(unsigned int), SIZE);
  unsigned int *arr2 = ALLOC(sizeof(unsigned int), SIZE);
  unsigned int *arr3 = ALLOC(sizeof(unsigned int), SIZE);
#else
  unsigned int *arr3 = ALLOC(sizeof(unsigned int), SIZE);
  unsigned int *arr2 = ALLOC(sizeof(unsigned int), SIZE);
  unsigned int *arr1 = ALLOC(sizeof(unsigned int), SIZE);
#endif
  finish = clock();
  unsigned int i, j;
  double intermed, finalres;

  intermed = ((double)(finish - start))/CLOCKS_PER_SEC;
  printf("Time to create: %.2f\n", intermed);

  printf("arr1 addr: %p\narr2 addr: %p\narr3 addr: %p\n", arr1, arr2, arr3);

  finalres = 0;
  for (j = 0; j < ITERATION; j++)
  {
    start = clock();
    {
      for (i = 0; i < SIZE; i++)
        arr1[i] = (i + 13) * 5;
    }
    finish = clock();

    intermed = ((double)(finish - start))/CLOCKS_PER_SEC;
    finalres += intermed;
    printf("Time A: %.2f\n", intermed);
  }

  printf("Time A (average): %.2f\n", finalres/ITERATION);


  finalres = 0;
  for (j = 0; j < ITERATION; j++)
  {
    start = clock();
    {
      for (i = 0; i < SIZE; i++)
        arr2[i] = (i + 13) * 5;
    }
    finish = clock();

    intermed = ((double)(finish - start))/CLOCKS_PER_SEC;
    finalres += intermed;
    printf("Time B: %.2f\n", intermed);
  }

  printf("Time B (average): %.2f\n", finalres/ITERATION);


  finalres = 0;
  for (j = 0; j < ITERATION; j++)
  {
    start = clock();
    {
      for (i = 0; i < SIZE; i++)
        arr3[i] = (i + 13) * 5;
    }
    finish = clock();

    intermed = ((double)(finish - start))/CLOCKS_PER_SEC;
    finalres += intermed;
    printf("Time C: %.2f\n", intermed);
  }

  printf("Time C (average): %.2f\n", finalres/ITERATION);

  gfinish = clock();

  intermed = ((double)(gfinish - gstart))/CLOCKS_PER_SEC;
  printf("Global Time: %.2f\n", intermed);

  return 0;
}

结果:

使用USE_CALLOC

Time to create: 0.13
arr1 addr: 0x7fabcb4a6000
arr2 addr: 0x7fabe917d000
arr3 addr: 0x7fac06e54000
Time A: 0.67
Time A: 0.48
...
Time A: 0.47
Time A (average): 0.48
Time B: 0.63
Time B: 0.47
...
Time B: 0.48
Time B (average): 0.48
Time C: 0.45
...
Time C: 0.46
Time C (average): 0.46

使用USE_CALLOC和A

Time to create: 0.13
arr1 addr: 0x7fc2fa206010
arr2 addr: 0xx7fc2dc52e010
arr3 addr: 0x7fc2be856010
Time A: 0.44
...
Time A: 0.43
Time A (average): 0.45
Time B: 0.65
Time B: 0.47
...
Time B: 0.46
Time B (average): 0.48
Time C: 0.65
Time C: 0.48
...
Time C: 0.45
Time C (average): 0.48

使用USE_MMAP

Time to create: 0.0
arr1 addr: 0x7fe6332b7000
arr2 addr: 0x7fe650f8e000
arr3 addr: 0x7fe66ec65000
Time A: 0.55
Time A: 0.48
...
Time A: 0.45
Time A (average): 0.49
Time B: 0.54
Time B: 0.46
...
Time B: 0.49
Time B (average): 0.50
Time C: 0.57
...
Time C: 0.40
Time C (average): 0.43