我了解到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;
}
结果:
<击> 除了-O3之外,zero_1是最慢的。 zero_sizet是最快的,在-O1,-O2和-O3之间具有大致相同的性能。 memset总是比zero_sizet慢。 (-O3慢两倍)。有趣的是,-O3 zero_1与zero_sizet同样快。然而,反汇编函数的指令大约是指令的四倍(我认为是由循环展开引起的)。此外,我尝试进一步优化zero_sizet,但编译器总是超过我,但这并不奇怪。
现在memset获胜,以前的结果被CPU缓存扭曲了。 (所有测试都在Linux上运行)需要进一步测试。我接下来会尝试汇编:)
edit3:修复了测试代码中的错误,测试结果不受影响
edit4:在探讨反汇编的VS2010 C运行时,我注意到memset
有一个SSE优化的例程为零。这很难打败。
答案 0 :(得分:31)
x86是相当广泛的设备。
对于完全通用的x86目标,带有“rep movsd”的汇编块可能会在时间上将零值爆发到32位内存。尽量确保这项工作的大部分都是DWORD对齐的。
对于带有mmx的芯片,带有movq的装配环一次可以达到64位。
您可能能够使用C / C ++编译器来使用带有指向long long或_m64的指针的64位写入。目标必须是8字节对齐才能获得最佳性能。
对于带有sse的芯片,movaps很快,但只有当地址是16字节对齐时,所以使用movsb直到对齐,然后使用movaps循环完成清除
Win32有“ZeroMemory()”,但我忘记了这是一个宏来memset,还是一个真正的“好”实现。
答案 1 :(得分:26)
memset
通常设计为非常快的通用设置/置零代码。它处理所有具有不同尺寸和对齐的情况,这会影响您可以用来执行工作的各种指令。根据您所使用的系统(以及您的stdlib来自哪个供应商),底层实现可能是特定于该体系结构的汇编程序,以利用其本机属性。它可能还有内部特殊情况来处理归零的情况(而不是设置其他值)。
也就是说,如果你有一个非常具体的,非常高性能的内存归零,那你可以通过自己动手来击败特定的memset
实现。 memset
及其在标准库中的朋友总是有趣的目标,可以实现单人胜任的编程。 :)
答案 2 :(得分:23)
现在您的编译器应该为您完成所有工作。至少我所知道的gcc非常有效地优化了对memset
的调用(尽管更好地检查汇编程序)。
然后,如果您不需要,请避免memset
:
... = { 0
}
)对于非常大的块,如果有的话,请使用mmap
。这只是从系统“免费”获得零初始化内存。
答案 3 :(得分:5)
如果我没记错的话(从几年前开始),其中一位资深开发人员正在谈论在PowerPC上使用bzero()的快速方法(规格说我们需要在启动时将几乎所有内存归零)。它可能无法很好地转换(如果有的话)到x86,但它可能值得探索。
想法是加载数据缓存行,清除该数据缓存行,然后将清除的数据缓存行写回内存。
对于它的价值,我希望它有所帮助。
答案 4 :(得分:5)
除非您有特定需求或知道您的编译器/ stdlib很糟糕,否则请坚持使用memset。它是通用的,一般应该具有良好的性能。此外,编译器可能更容易优化/内联memset(),因为它可以对其进行内在支持。
例如,Visual C ++通常会生成内联版本的memcpy / memset,这些版本的与库函数的调用一样小,因此可以避免推送/调用/转发开销。当在编译时可以评估size参数时,还有可能进行优化。
那就是说,如果你有特定的需求(其中大小总是很小 *或* 巨大 ),你可以通过下降到装配水平来提高速度。例如,使用直写操作将大块内存清零而不会污染L2缓存。
但这一切都取决于 - 对于普通的东西,请坚持memset / memcpy:)
答案 5 :(得分:2)
另请参阅问题Strange assembly from array 0-initialization,以便对memset
和= { 0 }
进行比较。
答案 6 :(得分:2)
memset功能设计灵活简单,即使以牺牲速度为代价。在许多实现中,它是一个简单的while循环,它在给定的字节数上一次一个字节地复制指定的值。如果你想要一个更快的memset(或memcpy,memmove等),几乎总是可以自己编写代码。
最简单的定制是进行单字节“设置”操作,直到目标地址为32或64位对齐(无论与芯片的架构匹配),然后一次开始复制一个完整的CPU寄存器。如果您的范围没有以对齐的地址结束,则可能必须在结尾处执行几个单字节“设置”操作。
根据您的特定CPU,您可能还有一些可以帮助您的SIMD流指令。这些通常在对齐的地址上工作得更好,因此上述使用对齐地址的技术在这里也很有用。
为了将大部分内存归零,您还可以通过将范围拆分为多个部分并并行处理每个部分(其中部分的数量与您的数量或核心/硬件线程相同)来看到速度提升。
最重要的是,除非你尝试,否则无法判断这些是否会有所帮助。至少,看一下编译器为每种情况发出的内容。查看其他编译器为其标准“memset”发出的内容(它们的实现可能比编译器更有效)。
答案 7 :(得分:2)
在这个非常有用和有用的测试中有一个致命的缺陷: 由于memset是第一条指令,因此似乎存在一些“内存开销”,这使得它非常慢。 将memset的时间移动到第二位,将其他内容移到第一位或简单地将memset移动两次,使得memset在所有编译开关中都最快!!!“
答案 8 :(得分:2)
这是一个有趣的问题。在VC ++ 2012上进行32位版本编译时,我实现了稍微快一点(但几乎不可测量)的实现。它可能会在很多方面得到改进。在多线程环境中在自己的类中添加它可能会带来更多的性能提升,因为在多线程场景中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
Here you can find源代码Berkley的memset()
,我认为这是最常见的实现。
答案 9 :(得分:-1)
memset内联为一系列有效的操作码,展开几个周期。对于非常大的内存块(例如4000x2000 64位帧缓冲区),您可以尝试跨多个线程对其进行优化(您为单独的任务做准备),每个线程都设置自己的部分。请注意,还有bzero(),但是它比较晦涩难懂,并且不太可能像memset一样优化,编译器肯定会注意到您传递了0。
编译器通常假定您要对大块进行内存设置,因此,如果您初始化大量小对象,则对较小的块执行*(uint64_t*)p = 0
可能会更有效率。
通常,所有x86 CPU都是不同的(除非您针对某个标准化平台进行编译),并且针对Pentium 2优化的某些东西在Core Duo或i486上的行为也会有所不同。因此,如果您真的喜欢它,并且想挤牙膏的最后几分,则可以将自己的exe发行并针对不同流行的CPU模型进行了优化的多个版本寄出。根据个人经验,Clang -march = native将我的游戏的FPS从60提升到65,而没有-march。