为什么第二个循环比第一个循环更快?

时间:2014-06-24 00:46:59

标签: c loops gcc optimization for-loop

我有以下代码

#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但第二次仍然打印较小的值。

3 个答案:

答案 0 :(得分:6)

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

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

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

  

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

创建.bss段是为了不在二进制文件中存储所有零,它只是说&#34;这个程序想要有200MB的归零内存&#34;。

加载程序时,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下运行,以获取网页故障计数:

$ 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函数会生成页面错误(在第一次写入bss页面时),而SECOND不生成任何页面错误。每个页面故障都对应于一些工作,由OS内核完成,而且这项工作每页只能执行一次bss(因为bss没有取消映射并重新映射回来)。

答案 1 :(得分:4)

以下asimes回答它是由于缓存 - 我不相信你可以享受带有~100M阵列的缓存的好处,你可能会在返回之前完全删除任何有用的数据。

但是,根据您的平台(主要是操作系统),还有其他机制可供使用 - 当您分配数组时,您永远不会初始化它们,因此第一个循环可能会导致每个4k页面的第一次访问受到惩罚。这通常会导致系统调用的一些辅助,这会带来很高的开销 在这种情况下,您还可以修改页面,因此大多数系统将被强制执行写时复制流程(只要您从页面中读取就可以进行优化),这甚至更重。

每页添加一个小访问权限(对于实际缓存而言应该可以忽略不计,并且它只从每个4k页面中取出一条64B行),设法使我的系统上的结果更均匀(尽管这种形式的测量不是开始时非常准确)

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

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

int main()
{
   int i;
   for(i = 0; i < SIZE; i+=4096)      ////  access and modify each page once
       c[i] = 0;                      ////

   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);
}

答案 2 :(得分:3)

如果修改第二个循环与第一个循环相同,效果相同,则第二个循环更快:

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 = 0; i < SIZE; i++)
      c[i] = 0;
   t = clock() - t;
   printf("%d\n\n", t);
}

这是因为第一个循环将信息加载到缓存中,并且在第二个循环期间可以轻松访问该信息

上述结果:

317841
277270

修改:Leeor提出了一个好点,c不适合缓存。我有一个英特尔酷睿i7处理器:http://ark.intel.com/products/37147/Intel-Core-i7-920-Processor-8M-Cache-2_66-GHz-4_80-GTs-Intel-QPI

根据链接,这意味着L3缓存仅为8 MB,或8,388,608字节,c为100,000,000字节