我想使用memcpy
来衡量内存带宽。我修改了此答案的代码:why vectorizing the loop does not have performance improvement,它使用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左右。
然后我使用b
将memset(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;
}
答案 0 :(得分:1)
重点是大多数平台上的malloc
和calloc
不会分配内存;他们分配地址空间。
malloc
等工作:
calloc
:相当于memset(ptr, 0, size)
已发出对于具有需求分页(COW)的系统(MMU可以在这里提供帮助),第二个选项风向下:
/dev/zero
这将不会消耗物理内存,除了页表。
/dev/zero
。 /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 Glisse(https://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
获得calloc
或malloc
的指针,它会将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)
。