比memset更快的零内存方式?

我了解到, memset(ptr, 0, nbytes)非常快,但有没有更快的方法(至less在x86上)?

我认为,memset使用mov ,但是当调零内存大多数编译器使用xor因为它更快,正确吗? 编辑1:错误,正如GregS指出的那样,只适用于寄存器。 我在想什么?

另外我问了一个比我更了解汇编程序的人去看stdlib,他告诉我在x86上memset没有充分利用32位宽的寄存器。 然而当时我很累,所以我不太确定我是否正确理解。

编辑2 :我重新审视这个问题,并做了一些testing。 这是我testing的:

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

结果:

零 – 1是最慢的,除了-O3。 在-O1,-O2和-O3上zero_sizet是最快的,性能大致相同。 memset总是比zero_sizet慢。 (-O3的两倍)。 有一点值得关注的是在-O3 zero_1与zero_sizet同样快。 但是反汇编函数大约有四倍的指令(我认为是由循环展开引起的)。 另外,我试图进一步优化zero_sizet,但编译器总是超出我的意思,但这里并不令人意外。

对于现在的memset胜利,以前的结果被CPUcaching扭曲。 (所有testing都在Linux上运行)需要进一步的testing。 我会尝试汇编下一步:)

edit3:修正了testing代码中的bug,testing结果不受影响

编辑4:在反编译 VS2010 C运行时,我注意到, memset有一个SSE优化程序为零。 这将是很难打败。

x86是相当广泛的设备。

对于完全通用的x86目标,具有“rep movsd”的汇编块可以在零时刻将零发送到内存32位。 尝试确保这项工作的大部分是DWORDalignment。

对于mmx的芯片,movq的汇编循环一次可以达到64位。

您可能可以通过指向long long或_m64的指针获得C / C ++编译器的64位写入。 目标必须是8字节alignment才能获得最佳性能。

对于sse的芯片,movaps是快速的,但只有当地址是16字节alignment,所以使用movsb直到alignment,然后完成你的清除一个movaps循环

Win32有“ZeroMemory()”,但是我忘记了这是一个macrosmemset,还是一个实际的'好'的实现。

memset通常被devise成非常快速的通用设置/调零代码。 它处理所有具有不同尺寸和路线的案例,这些案例会影响您可以使用的各种说明。 根据你所在的系统(以及你的stdlib来自哪个供应商),底层的实现可能是针对该体系结构的汇编器,以便利用其本机属性。 它也可能有内部特殊情况来处理归零的情况(与设置其他值)。

也就是说,如果你有非常具体的,非常性能的关键内存调零,你可以通过自己做一个特定的memset实现。 memset和它的标准库中的朋友总是一个有技巧的编程的有趣目标。 🙂

现在你的编译器应该为你做所有的工作。 至less我知道gcc在优化对memset调用方面非常高效(尽pipe如此,更好地检查汇编程序)。

那么,如果你不需要:

  • 使用calloc堆内存
  • 对堆栈内存使用正确的初始化( ... = { 0 }

而对于真正的大块使用mmap如果你有它。 这只是从系统“免费”获得零初始化的内存。

如果我没记错的话(从几年前开始),其中一位高级开发人员正在讨论在PowerPC上使用bzero()的快速方法(specs说我们需要将几乎所有内存都加电)。 它可能无法很好地转换(如果有的话)到x86,但它可能是值得探讨的。

这个想法是加载数据caching行,清除数据caching行,然后将清除的数据caching行写回内存。

为了什么是值得的,我希望它有帮助。

除非你有特定的需求或者知道你的编译器/ stdlib很烂,否则坚持使用memset。 这是通用的,应该有一般的体面的performance。 另外,编译器可能会更容易优化/内联memset(),因为它可以有固有的支持。

例如,Visual C ++通常会生成与调用库函数一样小的memcpy / memset的内联版本,从而避免了push / call / ret开销。 在编译时可以对size参数进行评估,还有可能的优化。

也就是说,如果你有特定的需求(大小总是很小 或者 很大 ),你可以通过下降到assembly水平来获得速度提升。 例如,使用直写操作将大量内存置零,而不会污染L2高速caching。

但这一切都取决于 – 和正常的东西,请坚持memset / memcpy 🙂

这是一个有趣的问题。 在VC ++ 2012上编译32位版本时,我做了这个实现稍微快一些(但是难以衡量)。它可能会有很大的改进。 在multithreading环境中,在您自己的类中添加此代码可能会使您获得更多的性能提升,因为在multithreading场景中存在一些memset()瓶颈问题。

 // MemsetSpeedTest.cpp : Defines the entry point for the console application. // #include "stdafx.h" #include <iostream> #include "Windows.h" #include <time.h> #pragma comment(lib, "Winmm.lib") using namespace std; /** a signed 64-bit integer value type */ #define _INT64 __int64 /** a signed 32-bit integer value type */ #define _INT32 __int32 /** a signed 16-bit integer value type */ #define _INT16 __int16 /** a signed 8-bit integer value type */ #define _INT8 __int8 /** an unsigned 64-bit integer value type */ #define _UINT64 unsigned _INT64 /** an unsigned 32-bit integer value type */ #define _UINT32 unsigned _INT32 /** an unsigned 16-bit integer value type */ #define _UINT16 unsigned _INT16 /** an unsigned 8-bit integer value type */ #define _UINT8 unsigned _INT8 /** maximum allo wed value in an unsigned 64-bit integer value type */ #define _UINT64_MAX 18446744073709551615ULL #ifdef _WIN32 /** Use to init the clock */ #define TIMER_INIT LARGE_INTEGER frequency;LARGE_INTEGER t1, t2;double elapsedTime;QueryPerformanceFrequency(&frequency); /** Use to start the performance timer */ #define TIMER_START QueryPerformanceCounter(&t1); /** Use to stop the performance timer and output the result to the standard stream. Less verbose than \c TIMER_STOP_VERBOSE */ #define TIMER_STOP QueryPerformanceCounter(&t2);elapsedTime=(t2.QuadPart-t1.QuadPart)*1000.0/frequency.QuadPart;wcout<<elapsedTime<<L" ms."<<endl; #else /** Use to init the clock */ #define TIMER_INIT clock_t start;double diff; /** Use to start the performance timer */ #define TIMER_START start=clock(); /** Use to stop the performance timer and output the result to the standard stream. Less verbose than \c TIMER_STOP_VERBOSE */ #define TIMER_STOP diff=(clock()-start)/(double)CLOCKS_PER_SEC;wcout<<fixed<<diff<<endl; #endif void *MemSet(void *dest, _UINT8 c, size_t count) { size_t blockIdx; size_t blocks = count >> 3; size_t bytesLeft = count - (blocks << 3); _UINT64 cUll = c | (((_UINT64)c) << 8 ) | (((_UINT64)c) << 16 ) | (((_UINT64)c) << 24 ) | (((_UINT64)c) << 32 ) | (((_UINT64)c) << 40 ) | (((_UINT64)c) << 48 ) | (((_UINT64)c) << 56 ); _UINT64 *destPtr8 = (_UINT64*)dest; for (blockIdx = 0; blockIdx < blocks; blockIdx++) destPtr8[blockIdx] = cUll; if (!bytesLeft) return dest; blocks = bytesLeft >> 2; bytesLeft = bytesLeft - (blocks << 2); _UINT32 *destPtr4 = (_UINT32*)&destPtr8[blockIdx]; for (blockIdx = 0; blockIdx < blocks; blockIdx++) destPtr4[blockIdx] = (_UINT32)cUll; if (!bytesLeft) return dest; blocks = bytesLeft >> 1; bytesLeft = bytesLeft - (blocks << 1); _UINT16 *destPtr2 = (_UINT16*)&destPtr4[blockIdx]; for (blockIdx = 0; blockIdx < blocks; blockIdx++) destPtr2[blockIdx] = (_UINT16)cUll; if (!bytesLeft) return dest; _UINT8 *destPtr1 = (_UINT8*)&destPtr2[blockIdx]; for (blockIdx = 0; blockIdx < bytesLeft; blockIdx++) destPtr1[blockIdx] = (_UINT8)cUll; return dest; } int _tmain(int argc, _TCHAR* argv[]) { TIMER_INIT const size_t n = 10000000; const _UINT64 m = _UINT64_MAX; const _UINT64 o = 1; char test[n]; { cout << "memset()" << endl; TIMER_START; for (int i = 0; i < m ; i++) for (int j = 0; j < o ; j++) memset((void*)test, 0, n); TIMER_STOP; } { cout << "MemSet() took:" << endl; TIMER_START; for (int i = 0; i < m ; i++) for (int j = 0; j < o ; j++) MemSet((void*)test, 0, n); TIMER_STOP; } cout << "Done" << endl; int wait; cin >> wait; return 0; } 

当为32位系统发布编译时,输出如下:

 memset() took: 5.569000 MemSet() took: 5.544000 Done 

当为64位系统发布编译时,输出如下:

 memset() took: 2.781000 MemSet() took: 2.765000 Done 

在这里你可以find源代码伯克利的memset() ,我认为这是最常见的实现。

另请参阅从数组0初始化奇怪的程序集 memset= { 0 }

memset函数的devise灵活简单,即使速度也是如此。 在许多实现中,它是一个简单的while循环,它在指定的字节数上一次将指定的值复制一个字节。 如果你想要一个更快的memset(或memcpy,memmove等),几乎总是可以自己编写一个。

最简单的定制就是做单字节“设置”操作,直到目标地址是32位或64位alignment(与芯片的架构相匹配),然后开始复制一个完整的CPU寄存器。 如果你的范围没有在一个alignment的地址上结束,你可能必须在最后做几个单字节的“设置”操作。

根据您的特定CPU,您可能也有一些streamSIMD指令,可以帮助你。 这些通常在alignment地址上工作得更好,所以上面使用alignment地址的技术在这里也是有用的。

为了清零大部分内存,您也可以通过将范围分割成多个部分并且并行处理每个部分(其中部分的数目与您的数目或内核/硬件线程相同)来看到速度提升。

最重要的是,除非您尝试,否则无法判断这些是否有帮助。 至less,看看你的编译器发出的每种情况。 看看其他编译器为他们的标准“memset”发出了什么(它们的实现可能比你的编译器更有效率)。

在这个非常有用的testing中有一个致命的缺陷:由于memset是第一条指令,所以似乎有一些“内存开销”,这使得它非常缓慢。 将memset的时间移动到第二位,并将其他事物放在第一位,或者简单地将memset计时两次,使memset在所有编译开关中速度最快!