零内存的速度比使用memset更快?

时间:2010-09-06 23:49:05

标签: c std

我了解到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优化的例程为零。这很难打败。

10 个答案:

答案 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

  • 将calloc用于堆内存
  • 对堆栈内存使用正确的初始化(... = { 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。