c – 为什么写入缓冲区的速度比写入缓冲区的速度快42?

前端之家收集整理的这篇文章主要介绍了c – 为什么写入缓冲区的速度比写入缓冲区的速度快42?前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。
我希望对char *缓冲区的写入可以采用相同的时间,而不管memory1的现有内容如何.不是吗

然而,在缩小基准的不一致的同时,我遇到了一个显然不真实的情况.包含全零的缓冲区在性能方面表现差异很大,从填充42的缓冲区.

在图形上,这看起来像(下面的细节):

这是我用来生成以上3的代码

#include <stdio.h>
#include <stdlib.h>
#include <inttypes.h>
#include <string.h>
#include <time.h>

volatile char *sink;

void process(char *buf,size_t len) {
  clock_t start = clock();
  for (size_t i = 0; i < len; i += 678)
    buf[i] = 'z';
  printf("Processing took %lu μs\n",1000000UL * (clock() - start) / CLOCKS_PER_SEC);
  sink = buf;
}

int main(int argc,char** argv) {
  int total = 0;
  int memset42 = argc > 1 && !strcmp(argv[1],"42");
  for (int i=0; i < 5; i++) {
    char *buf = (char *)malloc(BUF_SIZE);
    if (memset42)
      memset(buf,42,BUF_SIZE);
    else
      memset(buf,BUF_SIZE);
    process(buf,BUF_SIZE);
  }
  return EXIT_SUCCESS;
}

我在我的Linux机箱上编译如下:

gcc -O2 buffer_weirdness.cpp -o buffer_weirdness

…当我运行一个零缓冲区的版本,我得到:

./buffer_weirdness zero
Processing took   12952 μs
Processing took  403522 μs
Processing took  626859 μs
Processing took  626965 μs
Processing took  627109 μs

注意,第一次迭代是快速的,而剩下的迭代可能需要50倍.

当缓冲区首先填充42时,处理总是很快:

./buffer_weirdness 42
Processing took   12892 μs
Processing took   13500 μs
Processing took   13482 μs
Processing took   12965 μs
Processing took   13121 μs

行为取决于`BUF_SIZE(在上面的例子中为1GB) – 更大的大小更有可能显示问题,并且还取决于当前的主机状态.如果我独自离开主机一段时间,缓慢的迭代可能需要60,000μs,而不是600,000,所以10倍快,但比快速处理时间慢5〜5倍.最终,时代回归到完全缓慢的行为.

行为还至少部分依赖于透明的hugepages – 如果我禁用它们2,缓慢迭代的性能提高约3倍,而快速迭代不变.

最后一个注意事项是,该过程的总运行时间比简单地定时处理程序更接近(实际上,零填充,THP关闭版本比其他版本快两倍,大致相同).

这里发生了什么?

1在一些非常不寻常的优化之外,编译器理解缓冲区已经包含什么值,并且删除相同值的写入,这里没有发生.

2 sudo sh -c“echo never> / sys / kernel / mm / transparent_hugepage / enabled”

3这是原始基准测试的蒸馏版本.是的,我泄漏了分配,解决了这个问题 – 这导致了一个更简洁的例子.原来的例子没有泄漏.事实上,当您不泄露分配时,行为会发生变化:可能是因为malloc只能重新使用该区域进行下一次分配,而不是要求操作系统获得更多内存.

解决方法

这似乎很难重现,所以它可能是编译器/ libc具体的.

我最好的猜测在这里:

当您调用malloc时,会将内存映射到进程空间中,这并不意味着操作系统已经从其可用内存池中获取了必要的页面,而是将其添加到某些表中.

现在,当您尝试访问内存时,您的cpu / MMU会引起故障,并且操作系统可以捕获,并检查该地址是否属于“已经在内存空间中但尚未实际分配给处理”.如果是这种情况,则会发现必要的可用内存并将其映射到进程的内存空间中.

现在,现代操作系统通常有一个内置的选项,可以在(重新)使用之前“清除”页面.如果这样做,memset(,)操作就不必要了.在POSIX系统的情况下,如果使用calloc而不是malloc,内存将被清零.

换句话说,您的编译器可能已经注意到,当您的操作系统支持时,完全省略了memset(,).这意味着当你写入到进程()的页面是他们访问的第一时刻,并触发了操作系统的“即时页面映射”机制.

memset(,42)当然可以不被优化,所以在这种情况下,这些页面实际上是预先分配的,你没有看到在process()函数中花费的时间.

你应该使用/usr/bin/time来实际比较整个执行时间和进程中花费的时间 – 我的怀疑意味着保存在进程中的时间实际上花费在主要的内核上下文中.

更新:用优异的Godbolt Compiler Explorer测试:是的,使用-O2和-O3,现代gcc只是省略零memsetting(或者,简单地将其融入calloc,这是malloc,并且为零):

#include <cstdlib>
#include <cstring>
int main(int argc,char ** argv) {
  char *p = (char*)malloc(10000);
  if(argc>2) {
    memset(p,10000);
  } else {
    memset(p,10000);
  }
  return (int)p[190]; // had to add this for the compiler to **not** completely remove all the function body,since it has no effect at all.
}

成为gcc6.3上的x86_64

main:
        // store frame state
        push    rbx
        mov     esi,1
        // put argc in ebx
        mov     ebx,edi
        // Setting up call to calloc (== malloc with internal zeroing)
        mov     edi,10000
        call    calloc 
        // ebx (==argc) compared to 2 ?
        cmp     ebx,2
        mov     rcx,rax
        // jump on less/equal to .L2
        jle     .L2
        // if(argc > 2):
        // set up call to memset
        mov     edx,10000
        mov     esi,42
        mov     rdi,rax
        call    memset
        mov     rcx,rax
.L2:    //else case
        //notice the distinct lack of memset here!
        // move the value at position rcx (==p)+190 into the "return" register
        movsx   eax,BYTE PTR [rcx+190]
        //restore frame
        pop     rbx
        //return
        ret

顺便说一句,如果你删除返回p [190],

}
  return 0;
}

那么编译器根本没有保留函数体的理由 – 它的返回值在编译时很容易确定,并且没有任何副作用.整个程序然后编译

main:
        xor     eax,eax
        ret

请注意,对于每个A,A xor A为0.

猜你在找的C&C++相关文章