72

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

我假设 memset 使用mov,但是在将内存归零时,大多数编译器都使用xor它,因为它更快,对吗?编辑 1:错误,正如 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,测试结果不受影响

编辑 4:在查看反汇编的 VS2010 C 运行时时,我注意到它memset的 SSE 优化例程为零。这将很难被击败。

4

9 回答 9

37

x86 是相当广泛的设备。

对于完全通用的 x86 目标,带有“rep movsd”的程序集块可能会在 32 位内存中一次爆出零。尽量确保这项工作的大部分是 DWORD 对齐的。

对于带有 mmx 的芯片,带有 movq 的组装循环一次可以达到 64 位。

您也许可以让 C/C++ 编译器使用 64 位写入和指向 long long 或 _m64 的指针。目标必须是 8 字节对齐以获得最佳性能。

对于带有 sse 的芯片,movaps 很快,但前提是地址是 16 字节对齐的,所以使用 movsb 直到对齐,然后使用 movaps 循环完成清除

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

于 2010-09-07T00:25:16.800 回答
30

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

也就是说,如果您有非常具体的、对性能非常关键的内存归零要做,那么您当然有可能memset通过自己完成特定的实现来击败它。memset及其在标准库中的朋友始终是单人编程的有趣目标。:)

于 2010-09-06T23:51:58.487 回答
24

如今,您的编译器应该为您完成所有工作。至少我所知道的 gcc 在优化对memsetaway 的调用方面非常有效(不过最好检查汇编程序)。

然后,memset如果您不必这样做,请避免:

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

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

于 2010-09-07T12:39:31.273 回答
6

如果我没记错的话(从几年前开始),一位高级开发人员正在讨论 PowerPC 上 bzero() 的快速方法(规范说我们需要在上电时将几乎所有内存归零)。它可能无法很好地(如果有的话)转换为 x86,但它可能值得探索。

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

对于它的价值,我希望它有所帮助。

于 2010-09-07T00:21:59.937 回答
6

除非您有特定需求或知道您的编译器/stdlib 很糟糕,否则请坚持使用 memset。它是通用的,并且总体上应该具有不错的性能。此外,编译器可能更容易优化/内联 memset(),因为它可以对其提供内在支持。

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

也就是说,如果您有特定需求(尺寸总是很小 * 或 * 巨大),您可以通过下降到装配级别来提高速度。例如,使用直写操作将大块内存归零而不会污染 L2 缓存。

但这一切都取决于 - 对于普通的东西,请坚持使用 memset/memcpy :)

于 2010-09-07T13:58:46.203 回答
3

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

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

根据您的特定 CPU,您可能还有一些流式 SIMD 指令可以帮助您。这些通常在对齐地址上会更好地工作,因此上述使用对齐地址的技术在这里也很有用。

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

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

于 2010-09-07T13:17:58.683 回答
3

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

于 2011-08-19T13:03:34.993 回答
3

这是一个有趣的问题。当在 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's 的源代码memset(),我认为这是最常见的实现。

于 2013-03-08T10:09:17.533 回答
-1

memset 可以被编译器内联为一系列有效的操作码,展开几个周期。对于非常大的内存块,例如 4000x2000 64 位帧缓冲区,您可以尝试跨多个线程(您为该唯一任务准备)对其进行优化,每个线程都设置自己的部分。注意也有bzero(),但是比较晦涩,不太可能像memset那样优化,编译器肯定会注意到你传了0。

编译器通常假设的是你 memset 大块,所以对于较小的块*(uint64_t*)p = 0,如果你初始化大量的小对象,这样做可能会更有效。

通常,所有 x86 CPU 都是不同的(除非您针对某些标准化平台进行编译),并且针对 Pentium 2 优化的某些内容在 Core Duo 或 i486 上的表现也会有所不同。因此,如果您真的喜欢它并想挤出最后几口牙膏,那么发布几个针对不同流行 CPU 型号编译和优化的 exe 版本是有意义的。根据个人经验,与没有 -march 相比,Clang -march=native 将我游戏的 FPS 从 60 提高到 65。

于 2019-08-26T15:28:13.760 回答