【发布时间】:2011-04-08 23:13:43
【问题描述】:
我了解到memset(ptr, 0, nbytes) 确实很快,但是有没有更快的方法(至少在 x86 上)?
我假设 memset 使用mov,但是在清零内存时,大多数编译器使用xor,因为它更快,对吗? edit1: 错误,正如 GregS 指出的那样,它只适用于寄存器。我在想什么?
我还请一个比我更了解汇编程序的人查看 stdlib,他告诉我在 x86 上 memset 没有充分利用 32 位宽的寄存器。不过那时我很累,所以我不太确定我是否理解正确。
edit2: 我重新审视了这个问题并做了一些测试。这是我测试的:
#include <stdio.h>
#include <malloc.h>
#include <string.h>
#include <sys/time.h>
#define TIME(body) do { \
struct timeval t1, t2; double elapsed; \
gettimeofday(&t1, NULL); \
body \
gettimeofday(&t2, NULL); \
elapsed = (t2.tv_sec - t1.tv_sec) * 1000.0 + (t2.tv_usec - t1.tv_usec) / 1000.0; \
printf("%s\n --- %f ---\n", #body, elapsed); } while(0) \
#define SIZE 0x1000000
void zero_1(void* buff, size_t size)
{
size_t i;
char* foo = buff;
for (i = 0; i < size; i++)
foo[i] = 0;
}
/* I foolishly assume size_t has register width */
void zero_sizet(void* buff, size_t size)
{
size_t i;
char* bar;
size_t* foo = buff;
for (i = 0; i < size / sizeof(size_t); i++)
foo[i] = 0;
// fixes bug pointed out by tristopia
bar = (char*)buff + size - size % sizeof(size_t);
for (i = 0; i < size % sizeof(size_t); i++)
bar[i] = 0;
}
int main()
{
char* buffer = malloc(SIZE);
TIME(
memset(buffer, 0, SIZE);
);
TIME(
zero_1(buffer, SIZE);
);
TIME(
zero_sizet(buffer, SIZE);
);
return 0;
}
结果:
zero_1 是最慢的,但 -O3 除外。 zero_sizet 是最快的,在 -O1、-O2 和 -O3 上的性能大致相同。 memset 总是比 zero_sizet 慢。 (-O3 慢两倍)。有趣的一件事是,在 -O3 zero_1 与 zero_sizet 一样快。然而,反汇编的函数大约有四倍的指令(我认为是由循环展开引起的)。另外,我尝试进一步优化 zero_sizet,但编译器总是超过我,但这并不奇怪。
现在 memset 获胜,以前的结果被 CPU 缓存扭曲了。 (所有测试均在 Linux 上运行)需要进一步测试。接下来我会尝试汇编程序:)
edit3:修复了测试代码中的bug,测试结果不受影响
edit4: 在查看反汇编的 VS2010 C 运行时时,我注意到memset 有一个针对零的 SSE 优化例程。很难打败它。
【问题讨论】:
-
与其假设
memset使用mov,不如尝试反汇编编译器的输出?不同的编译器会做不同的事情。如果xor在给定架构上更快,那么如果某些编译器将memset(ptr, 0, nbytes)优化为xor指令也就不足为奇了。 -
我不知道有一个编译器使用 XOR 来归零内存。也许是一个寄存器,但不是内存。为了使用 XOR 到零内存,您必须先读取内存,然后 XOR,然后写入内存。
-
如果合适,
calloc可能实际上是空闲的,因为实现可能会提前零页,而 CPU 则处于空闲状态。这算不算? ;-) -
@aaa carp,GregS 是对的,使用 XOR 清除内存几乎永远不会是正确的答案。突然想到的例外是最小的 Microchip PIC CPU,其中所有可用的 RAM 实际上只是一大块通用寄存器。使用 XOR 清除寄存器在 CISC 架构中很常见。一些 RISC 架构有效地包括一个通用寄存器,该寄存器被硬连线以保持零仅用于此目的。
-
@TravisGockel 同样非常不明智:
madvise()可能只是一个空操作。它可能适用于特定的内核和 libc 版本,但听起来如果您升级任何一个,它都可能很容易损坏。