我想使用memcpy
测量内存带宽。我修改了这个答案中的代码:为什么矢量化循环没有性能改进,它使用memset
来测量带宽。问题是memcpy
只比memset
慢一点,因为我预计它会慢两倍,因为它在两倍的内存上运行。
更具体地说,我使用以下操作运行超过 1 GB 的数组a
和b
(分配的将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);
}
关键是大多数平台上的malloc
和calloc
不分配内存; 它们分配地址空间。
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.c
https://github.com/gcc-mirror/gcc/blob/7a31ada4c400351a35ab65f8dc0357e7c88805d5/gcc/tree-ssa-strlen.c#L1889 中的当前代码 - 如果memset(0)
从malloc
或calloc
获取指针,它将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)
.