比用 memset 更快地消除内存?

我了解到 memset(ptr, 0, nbytes)确实很快,但是有没有更快的方法(至少在 x86上) ?

我假设 memset 使用 mov,但是当归零内存时,大多数编译器使用 xor,因为它更快,对吗?编者:错误,正如 GregS 指出的那样,只有寄存器才起作用。我到底在想什么?

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

编辑2 : 我重新讨论了这个问题,并做了一些测试:

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

结果:

0 _ 1是最慢的,除了-O3。Zero _ sizet 是最快的,在-O1、-O2和-O3之间的性能大致相同。Memset 总是比 zero _ sizet 慢。(对于 -O3来说慢了一倍)。有趣的是,at-O30 _ 1的速度和0 _ siz 的速度一样快。然而,反汇编函数的指令数量大约是这个函数的四倍(我认为这是由循环展开引起的)。此外,我尝试进一步优化 zero _ sizet,但编译器总是超过我,但这里并不奇怪。罢工

现在 memset 获胜,以前的结果被 CPU 缓存扭曲。(所有测试都在 Linux 上运行)需要进一步的测试。我接下来试试汇编程序:)

编辑3: 测试代码中的固定错误,测试结果不受影响

编辑4: 在浏览已拆解的 VS2010C 运行时时,我注意到 memset有一个针对零的 SSE 优化例程。很难战胜它。

139101 次浏览

memset通常被设计为非常非常快的 一般用途设置/归零代码。它处理不同大小和对齐方式的所有情况,这会影响您可以使用来完成工作的各种指令。根据您所在的系统(以及您的 stdlib 来自哪个供应商) ,底层实现可能是特定于该体系结构的汇编程序,以利用其本机属性。它还可能有内部特殊用例来处理归零(与设置其他值相比)。

也就是说,如果您需要进行非常具体、非常关键的性能内存归零,那么您当然可以通过自己完成这项工作来击败特定的 memset实现。memset及其标准库中的朋友总是胜人一筹的编程的有趣目标。:)

如果我没记错的话(几年前) ,一位资深开发人员谈到了一种在 PowerPC 上快速实现 bzero ()的方法(规范说我们需要在开机时几乎将所有内存都实现 bzero)。它可能不能很好地转换到 x86(如果可以的话) ,但是它值得探索。

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

不管怎样,我希望能有所帮助。

X86是相当广泛的设备。

对于完全通用的 x86目标,一个带有“ rep ovsd”的汇编块可以同时向内存中输出零到32位。尝试确保这项工作的大部分是 DWORD 对齐。

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

您可以让 C/C + + 编译器使用64位写操作,并指向 long long 或 _ m64。为了获得最佳性能,目标必须是8字节对齐。

对于带有 sse 的芯片,movaps 是快速的,但只有当地址是16字节对齐时,才能使用 movsb 直到对齐,然后用一个 movaps 循环完成清除

Win32有“ ZeroMemory ()”,但我忘了这是一个给 memset 的宏,还是一个实际的“好”实现。

现在你的编译器应该为你做所有的工作。至少我所知道的 gcc 在优化对 memset的调用方面是非常有效的(不过最好检查一下汇编程序)。

然后,如果没有必要,也要避免 memset:

  • 使用 calloc 作为堆内存
  • 使用适当的初始化(< code > ... = {0 } )用于堆栈内存

对于非常大的块使用 mmap,如果你有的话。这只是从系统“免费”获得零初始化内存。

Memset 函数的设计是灵活和简单的,甚至以牺牲速度为代价。在许多实现中,它是一个简单的 while 循环,每次在给定的字节数上复制指定的值一个字节。如果您想要一个更快的 memset (或 memcpy、 memmove 等) ,几乎总是可以自己编写一个。

最简单的定制方式是执行单字节“ set”操作,直到目标地址为32位或64位对齐(与芯片架构匹配的任何对齐方式) ,然后开始一次复制完整的 CPU 寄存器。如果范围没有以对齐的地址结束,那么可能需要在最后执行几个单字节的“ set”操作。

根据您的特定 CPU,您可能还有一些可以帮助您解决这个问题的流 SIMD 指令。对于对齐的地址,这些方法通常效果更好,因此上述使用对齐地址的技术在这里也很有用。

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

最重要的是,除非你尝试,否则没有办法知道这些方法是否有用。至少,看看编译器为每种情况发出了什么。查看其他编译器为它们的标准“ memset”发出了什么(它们的实现可能比编译器的更有效)。

除非您有特定的需求或者知道您的编译器/stdlib 很糟糕,否则坚持使用 memset。它是通用的,并且应该有良好的性能。此外,编译器可能更容易优化/内联 memset () ,因为它可以得到内在的支持。

例如,Visual C + + 通常会为库函数生成内联版本的 memcpy/memset,它们是 像电话一样小,从而避免了推送/调用/重置开销。当 size 参数可以在编译时求值时,还有进一步的可能优化。

也就是说,如果您有 具体点的需要(其中大小将始终是 很小 * or * 巨大) ,您可以获得下降到组装水平的速度提高。例如,使用 write-through 操作在不污染 L2缓存的情况下对大块内存进行归零。

但这都取决于具体情况——对于普通的东西,请使用 memset/memcpy:)

在这个伟大而有益的测试中,有一个致命的缺陷: 由于 memset 是第一条指令,因此似乎存在一些“内存开销”,这使得它非常慢。 将 memset 的计时移动到第二位,将其他东西移动到第一位,或者只是将 memset 计时两次,就可以使 memset 在所有编译开关中都是最快的! ! !

这是个有趣的问题。在 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

在这里您可以找到源代码 Berkley 的 memset(),我认为它是最常见的实现。

Memset 可以被编译器作为一系列有效的操作码内联,展开几个周期。对于非常大的内存块,比如4000x200064位 framebuffer,您可以尝试跨多个线程对其进行优化(这是为完成该任务而准备的) ,每个线程都设置自己的部分。注意,还有 bzero () ,但是它更加模糊,不太可能像 memset 那样优化,编译器肯定会注意到您传递了0。

编译器通常假设,您在 memset 中设置了较大的块,因此对于较小的块,如果初始化大量的小对象,仅执行 *(uint64_t*)p = 0可能更有效。

一般来说,所有 x86 CPU 都是不同的(除非您为某个标准化平台编译) ,为 Pentium 2优化的某些内容在 Core Duo 或 i486上的表现也不同。因此,如果你真的喜欢它,并想挤出最后几位牙膏,这是有意义的船舶几个版本,你的 exe 编译和优化,不同的流行 CPU 模式。从个人经验来看,与没有进行曲相比,“叮当进行曲”= 原生进行曲将我游戏的 FPS 从60提高到65。