我有以下代码,用两次零一次,一次向前和一次一次向后写入一个全局数组。

#include <string.h>
#include <time.h>
#include <stdio.h>
#define SIZE 100000000

char c[SIZE];
char c2[SIZE];

int main()
{
   int i;
   clock_t t = clock();
   for(i = 0; i < SIZE; i++)
       c[i] = 0;

   t = clock() - t;
   printf("%d\n\n", t);

   t = clock();
   for(i = SIZE - 1; i >= 0; i--)
      c[i] = 0;

   t = clock() - t;
   printf("%d\n\n", t);
}


我已经运行了几次,第二张打印总是显示较小的值...

但是,如果在其中一个循环中将更改c更改为c2,则两次打印之间的时间差可以忽略不计...造成这种差异的原因是什么?

编辑:

我尝试使用-O3进行编译,并查看了程序集:有2个对memset的调用,但是第二个仍在打印较小的值。

最佳答案

当您在C中定义一些全局数据时,它会被零初始化:

char c[SIZE];
char c2[SIZE];


在linux(unix)世界中,这意味着cc2都将被分配在特殊的ELF文件部分.bss中:


  ...包含最初仅由零值位表示的静态分配变量的数据段


创建.bss段的目的是不将所有零存储在二进制文件中,它只是说“该程序希望具有200MB的归零内存”。

加载程序时,ELF加载器(在经典静态二进制文件中为内核,或者为ld.so动态加载器,也称为interp)将为.bss分配内存,通常类似于mmapMAP_ANONYMOUS标志和READ + WRITE权限/保护请求。

但是OS内核中的内存管理器不会为您提供全部200 MB的零内存。而是将进程的虚拟内存的一部分标记为零初始化,并且此内存的每一页都指向物理内存中的特殊零页。该页面有4096个字节的零字节,因此,如果您从cc2进行读取,则将得到零字节。并且这种机制允许内核减少内存需求。

到零页的映射是特殊的。它们被标记(在page table中)为只读。当您第一次写入任何此类虚拟页面时,General protection faultpagefault异常将由硬件生成(我将说是由MMU和TLB)。该错误将由内核处理,在您的情况下,将由minor pagefault处理程序处理。它将分配一个物理页面,将其填充为零字节,然后将刚刚访问的虚拟页面的映射重置为该物理页面。然后它将重新运行错误的指令。

我对您的代码进行了一些转换(两个循环都移到了单独的函数中):

$ cat b.c
#include <string.h>
#include <time.h>
#include <stdio.h>
#define SIZE 100000000

char c[SIZE];
char c2[SIZE];

void FIRST()
{
   int i;
   for(i = 0; i < SIZE; i++)
       c[i] = 0;
}

void SECOND()
{
   int i;
   for(i = 0; i < SIZE; i++)
       c[i] = 0;
}


int main()
{
   int i;
   clock_t t = clock();
   FIRST();
   t = clock() - t;
   printf("%d\n\n", t);

   t = clock();
   SECOND();

   t = clock() - t;
   printf("%d\n\n", t);
}


gcc b.c -fno-inline -O2 -o b编译,然后在linux的perf stat或更通用的/usr/bin/time下运行以获取pagefault计数:

$ perf stat ./b
139599

93283


 Performance counter stats for './b':
 ....
            24 550 page-faults               #    0,100 M/sec


$ /usr/bin/time ./b
234246

92754

Command exited with non-zero status 7
0.18user 0.15system 0:00.34elapsed 99%CPU (0avgtext+0avgdata 98136maxresident)k
0inputs+8outputs (0major+24576minor)pagefaults 0swaps


因此,我们有24.5万次次要的页面错误。如果x86 / x86_64上的标准页面大小为4096,则接近100兆字节。

使用perf record / perf report linux profiler,我们可以找到发生页面错误的位置(生成):

$ perf record -e page-faults ./b
...skip some spam from non-root run of perf...
213322

97841

[ perf record: Woken up 1 times to write data ]
[ perf record: Captured and wrote 0.018 MB perf.data (~801 samples) ]

$ perf report -n |cat
...
# Samples: 467  of event 'page-faults'
# Event count (approx.): 24583
#
# Overhead       Samples  Command      Shared Object                   Symbol
# ........  ............  .......  .................  .......................
#
    98.73%           459        b  b                  [.] FIRST
     0.81%             1        b  libc-2.19.so       [.] __new_exitfn
     0.35%             1        b  ld-2.19.so         [.] _dl_map_object_deps
     0.07%             1        b  ld-2.19.so         [.] brk
     ....


因此,现在我们可以看到,只有FIRST函数会生成pagefaults(在第一次写入bss页面时),而SECOND不会生成任何内容。每个pagefault都对应于OS内核完成的某些工作,并且该工作仅每bss页完成一次(因为bss未被取消映射并重新映射回去)。

关于c - 为什么BSS中静态数组上的第二个循环比第一个循环快?,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/56845032/

10-11 21:49