Memcpy与memset同时使用

时间:2017-04-02 12:56:20

标签: c linux memory x86 malloc

我想使用memcpy来衡量内存带宽。我修改了此答案的代码:why vectorizing the loop does not have performance improvement,它使用memset来衡量带宽。问题是memcpy只比memset慢得多,因为它的运行时间是内存的两倍,因此我预计它会慢大约两倍。

更具体地说,我通过以下操作运行了1 GB以上的数组ab(已分配calloc)100次。

operation             time(s)
-----------------------------
memset(a,0xff,LEN)    3.7
memcpy(a,b,LEN)       3.9
a[j] += b[j]          9.4
memcpy(a,b,LEN)       3.8

请注意,memcpy仅比memset略慢。操作a[j] += b[j](其中j超过[0,LEN))应该比memcpy长三倍,因为它的运行次数是数据的三倍。然而,它只有memset的2.5左右。

然后我使用bmemset(b,0,LEN)初始化为零并再次测试:

operation             time(s)
-----------------------------
memcpy(a,b,LEN)       8.2
a[j] += b[j]          11.5

现在,我们发现memcpy的速度约为memset的两倍,而a[j] += b[j]的速度与memset的速度相差三倍。

至少我希望在memset(b,0,LEN)之前{100}迭代中的第一个memcpy slower because the of lazy allocation (first touch)

为什么我只能在memset(b,0,LEN)之后得到我期望的时间?

test.c的

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

void tests(char *a, char *b, const int LEN){
    clock_t time0, time1;
    time0 = clock();
    for (int i = 0; i < 100; i++) memset(a,0xff,LEN);
    time1 = clock();
    printf("%f\n", (double)(time1 - time0) / CLOCKS_PER_SEC);

    time0 = clock();
    for (int i = 0; i < 100; i++) memcpy(a,b,LEN);
    time1 = clock();
    printf("%f\n", (double)(time1 - time0) / CLOCKS_PER_SEC);

    time0 = clock();
    for (int i = 0; i < 100; i++) for(int j=0; j<LEN; j++) a[j] += b[j];
    time1 = clock();
    printf("%f\n", (double)(time1 - time0) / CLOCKS_PER_SEC);

    time0 = clock();
    for (int i = 0; i < 100; i++) memcpy(a,b,LEN);
    time1 = clock();
    printf("%f\n", (double)(time1 - time0) / CLOCKS_PER_SEC);

    memset(b,0,LEN);
    time0 = clock();
    for (int i = 0; i < 100; i++) memcpy(a,b,LEN);
    time1 = clock();
    printf("%f\n", (double)(time1 - time0) / CLOCKS_PER_SEC);

    time0 = clock();
    for (int i = 0; i < 100; i++) for(int j=0; j<LEN; j++) a[j] += b[j];
    time1 = clock();
    printf("%f\n", (double)(time1 - time0) / CLOCKS_PER_SEC);
}

的main.c

#include <stdlib.h>

int tests(char *a, char *b, const int LEN);

int main(void) {
    const int LEN = 1 << 30;    //  1GB
    char *a = (char*)calloc(LEN,1);
    char *b = (char*)calloc(LEN,1);
    tests(a, b, LEN);
}

编译(gcc 6.2)gcc -O3 test.c main.c。 Clang 3.8给出的结果基本相同。

测试系统:i7-6700HQ@2.60GHz(Skylake),32 GB DDR4,Ubuntu 16.10。在我的Haswell系统上,带宽在memset(b,0,LEN)之前有意义,即我只在Skylake系统上看到问题。

我首先从a[j] += b[k]操作in this answer发现了这个问题,这个问题高估了带宽。

我想出了一个更简单的测试

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

void __attribute__ ((noinline))  foo(char *a, char *b, const int LEN) {
  for (int i = 0; i < 100; i++) for(int j=0; j<LEN; j++) a[j] += b[j];
}

void tests(char *a, char *b, const int LEN) {
    foo(a, b, LEN);
    memset(b,0,LEN);
    foo(a, b, LEN);
}

此输出。

9.472976
12.728426

但是,如果我在memset(b,1,LEN)之后的calloc(见下文)中12.5 12.5 ,则输出

#include <stdlib.h>

int tests(char *a, char *b, const int LEN);

int main(void) {
    const int LEN = 1 << 30;    //  1GB
    char *a = (char*)calloc(LEN,1);
    char *b = (char*)calloc(LEN,1);
    //GCC optimizes memset(b,0,LEN) away after calloc but Clang does not.
    memset(b,1,LEN);
    tests(a, b, LEN);
}

这使我认为这是一个操作系统分配问题,而不是编译器问题。

#include "stdio.h"
#include "stdafx.h"
#define _CRT_SECURE_NO_WARNINGS
#define getch() _getch()

struct Clicker {
    int toggle;
    int average;
};

int main()
{
    struct Clicker *clicker;
    printf("Enter your toggle key: ");
    (*clicker).toggle = _getch();
    printf("Enter your average cps: ");
    scanf_s("%d", (*clicker).average);
    printf("\nCurrent settings: \nToggle: %i \nAverage:%i\n", clicker->toggle, clicker->average);
    getchar();
    return 1;
}

2 个答案:

答案 0 :(得分:1)

重点是大多数平台上的malloccalloc 不会分配内存;他们分配地址空间

malloc等工作:

  • 如果请求可以由空闲列表完成,则从中创建一个块
    • 如果是calloc:相当于memset(ptr, 0, size)已发出
  • 如果不是:请求操作系统扩展地址空间。

对于具有需求分页(COW)的系统(MMU可以在这里提供帮助),第二个选项风向下:

  • 为请求创建足够的页表条目,并使用({COW)引用填充/dev/zero
  • 将这些PTEs添加到流程的地址空间

这将不会消耗物理内存,除了页表。

  • 一旦新内存被引用进行读取,读取将来自/dev/zero/dev/zero设备是一种非常特殊的设备,在这种情况下映射到新内存的每个页面
  • 但是,如果写入新页面,则COW逻辑会启动(通过页面错误):
    • 分配物理内存
    • / dev / zero页面已复制到新页面
    • 新页面与母版分开
    • 并且调用进程最终可以执行所有此操作的更新

答案 1 :(得分:1)

您的b数组可能不是在mmap之后编写的(使用malloc / calloc的大量分配请求通常会转换为mmap)。并且整个数组被编码为单个只读“零页面”(COW mechanism的一部分)。从单页读取零比从多页读取更快,因为单页将保留在缓存和TLB中。这解释了为什么memset(0)之前的测试更快:

  

此输出。 9.472976 12.728426

     

但是,如果我在memset(b,1,LEN)之后的calloc(见下文)中//GCC optimizes memset(b,0,LEN) away after calloc but Clang does not. ,则输出:12.5 12.5

更多关于gcc的malloc + memset / calloc + memset优化到calloc(从my comment扩展而来)

gcc/tree-ssa-strlen.c

https://gcc.gnu.org/bugzilla/show_bug.cgi?id=57742(树优化PR57742)在2013-06-27由Marc Glissehttps://stackoverflow.com/users/1918193?)按照4.9 / 5.0版GCC的计划提出此优化:< / p>

  

memset(malloc(n),0,n) - &gt;释放calloc(N,1)

     

calloc有时可能比malloc + bzero快得多,因为它具有一些内存已经为零的特殊知识。当其他优化将一些代码简化为malloc + memset(0)时,用calloc替换它会很好。遗憾的是,我认为没有办法在C ++中使用new进行类似的优化,这是最容易出现这种代码的地方(例如创建std :: vector(10000))。而且还有一个复杂的问题,memset的大小会比malloc的小一点(使用calloc仍然会很好,但是如果它是一个改进则更难以了解。)

于2014-06-24实施(https://gcc.gnu.org/bugzilla/show_bug.cgi?id=57742#c15) - https://gcc.gnu.org/viewcvs/gcc?view=revision&revision=211956(也https://patchwork.ozlabs.org/patch/325357/

  
      
  • tree-ssa-strlen.c ...   (handle_builtin_malloc,handle_builtin_memset):新功能。
  •   

memset(0) https://github.com/gcc-mirror/gcc/blob/7a31ada4c400351a35ab65f8dc0357e7c88805d5/gcc/tree-ssa-strlen.c#L1889中的当前代码 - 如果malloc获得callocmalloc的指针,它会将calloc转换为{{ 1}}然后memset(0)将被移除:

/* Handle a call to memset.
   After a call to calloc, memset(,0,) is unnecessary.
   memset(malloc(n),0,n) is calloc(n,1).  */
static bool
handle_builtin_memset (gimple_stmt_iterator *gsi)
 ...
  if (code1 == BUILT_IN_CALLOC)
    /* Not touching stmt1 */ ;
  else if (code1 == BUILT_IN_MALLOC
       && operand_equal_p (gimple_call_arg (stmt1, 0), size, 0))
    {
      gimple_stmt_iterator gsi1 = gsi_for_stmt (stmt1);
      update_gimple_call (&gsi1, builtin_decl_implicit (BUILT_IN_CALLOC), 2,
              size, build_one_cst (size_type_node));
      si1->length = build_int_cst (size_type_node, 0);
      si1->stmt = gsi_stmt (gsi1);
    }

2014年3月1日 - 2014年7月15日在gcc-patches邮件列表中对此进行了讨论,主题为“ calloc = malloc + memset

安迪·克莱恩(http://halobates.de/blog/https://github.com/andikleen)发表了明显的评论:https://gcc.gnu.org/ml/gcc-patches/2014-06/msg01818.html

  

FWIW我相信这种转变将打破各种各样的微观   基准。

     

calloc在内部知道从OS中获取的内存已归零。但   记忆可能还没有出现故障。

     

memset总是在内存中出错。

     

所以,如果你有一些像

这样的测试
   buf = malloc(...)
   memset(buf, ...) 
   start = get_time();
   ... do something with buf
   end = get_time()
     

现在时间将完全关闭,因为测量的时间   包括页面错误。

Marc replied好点。我认为解决编译器优化问题是微基准游戏的一部分,如果编译器没有经常在新的和有趣的游戏中搞乱它们,他们的作者会很失望方式; - )“和Andi asked:我宁愿不这样做。我不确定它有多大好处。如果你想保留它,请确保有一个简单的方法来关闭它。

Marc展示了如何关闭此优化:https://gcc.gnu.org/ml/gcc-patches/2014-06/msg01834.html

  

这些标志中的任何一个都有效:

     
      
  • -fdisable-tree-strlen
  •   
  • -fno-builtin-malloc
  •   
  • -fno-builtin-memset(假设您在代码中明确写了'memset')
  •   
  • -fno-builtin
  •   
  • -ffreestanding
  •   
  • -O1
  •   
  • -Os
  •   
     

在代码中,您可以隐藏传递给memset的指针   malloc通过将其存储在volatile变量中返回的一个,或   我们正在做的任何其他躲避编译器的技巧   memset(malloc(n),0,n)