C语言 Memcpy 与 memset 花费相同的时间



我想使用memcpy测量内存带宽。我修改了这个答案中的代码:为什么矢量化循环没有性能改进,它使用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左右。

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

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)之前memcpy会变慢,因为 100 次迭代中第一次迭代的延迟分配(第一次接触)。

为什么我只得到我期望的时间memset(b,0,LEN)

测试.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("%fn", (double)(time1 - time0) / CLOCKS_PER_SEC);
time0 = clock();
for (int i = 0; i < 100; i++) memcpy(a,b,LEN);
time1 = clock();
printf("%fn", (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("%fn", (double)(time1 - time0) / CLOCKS_PER_SEC);
time0 = clock();
for (int i = 0; i < 100; i++) memcpy(a,b,LEN);
time1 = clock();
printf("%fn", (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("%fn", (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("%fn", (double)(time1 - time0) / CLOCKS_PER_SEC);
}

主.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]操作中发现了这个问题,它高估了带宽。


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

#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

但是,如果我在calloc后在 main 中memset(b,1,LEN)(见下文),那么它会输出

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

关键是大多数平台上malloccalloc不分配内存; 它们分配地址空间

malloc等工作方式:

  • 如果请求可以通过自由列表完成,请从中切出一块
    • calloc的情况下:签发相当于memset(ptr, 0, size)
  • 如果没有:要求操作系统扩展地址空间。

对于具有需求分页 (COW) 的系统(MMU 可以在此处提供帮助),第二个选项可归结为:

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

这将不消耗物理内存,仅占用页表。

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

你的b数组可能不是在mmap-ing之后写的(malloc/calloc 的巨大分配请求通常会转换为mmap)。整个数组被mmap到单个只读的"零页"(COW机制的一部分)。从单个页面读取零比从多个页面读取更快,因为单个页面将保存在缓存和 TLB 中。这就解释了为什么 memset(0) 之前的测试更快:

此输出。 9.472976 12.728426

但是,如果我在calloc后在 main 中memset(b,1,LEN)(见下文),那么它会输出:12.5 12.5

以及更多关于 gcc 的 malloc+memset/calloc+memset 优化到 calloc 的信息(从我的评论中扩展)

//GCC optimizes memset(b,0,LEN) away after calloc but Clang does not.

此优化由 Marc Glisse (https://stackoverflow.com/users/1918193?) 于 2013-06-27 于 2013 年 6 月 27 日于 https://gcc.gnu.org/bugzilla/show_bug.cgi?id=57742 年(树优化 PR57742)提出,计划用于 GCC 的 4.9/5.0 版本:

memset(malloc(n),0,n) -> calloc(n,1)

Calloc 有时可能比 malloc+bzero 快得多,因为它有特殊的知识,一些内存已经为零。当其他优化将一些代码简化为 malloc+memset(0) 时,最好将其替换为 calloc。可悲的是,我认为没有办法在与new C++进行类似的优化,这是此类代码最容易出现的地方(例如创建std::vector(10000))。而且还有一个复杂的问题,即内存集的大小会比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/)

  • 树-SSA-strlen.c ... (handle_builtin_malloc、handle_builtin_memset):新功能。

gcc/tree-ssa-strlen.chttps://github.com/gcc-mirror/gcc/blob/7a31ada4c400351a35ab65f8dc0357e7c88805d5/gcc/tree-ssa-strlen.c#L1889 中的当前代码 - 如果memset(0)malloccalloc获取指针,它将malloc转换为calloc然后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-patch 邮件列表中进行了讨论,主题为">calloc = malloc + memset"

  • https://gcc.gnu.org/ml/gcc-patches/2014-02/msg01693.html
  • https://gcc.gnu.org/ml/gcc-patches/2014-03/threads.html#00009
  • https://gcc.gnu.org/ml/gcc-patches/2014-04/threads.html#00817
  • https://gcc.gnu.org/ml/gcc-patches/2014-05/msg01392.html
  • https://gcc.gnu.org/ml/gcc-patches/2014-06/threads.html#00234
  • https://gcc.gnu.org/ml/gcc-patches/2014-07/threads.html#01059

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

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

calloc内部知道操作系统中刚出现的内存已清零。但 内存可能尚未出现故障。

memset内存中总是出现故障。

所以如果你有一些测试,比如

buf = malloc(...)
memset(buf, ...) 
start = get_time();
... do something with buf
end = get_time()

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

马克回答说:">好点。我想围绕编译器优化工作是微基准测试游戏的一部分,如果编译器没有以新的和有趣的方式定期搞砸它,他们的作者会感到失望;-)"Andi问:">我宁愿不这样做。我不确定它有很多好处。如果你想保留它,请确保有一种简单的方法可以关闭它。

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).

相关内容

  • 没有找到相关文章

最新更新