【问题标题】:Faster way to zero memory than with memset?比使用 memset 更快的零内存方法?
【发布时间】:2011-04-08 23:13:43
【问题描述】:

我了解到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;
    }

结果:

zero_1 是最慢的,但 -O3 除外。 zero_sizet 是最快的,在 -O1、-O2 和 -O3 上的性能大致相同。 memset 总是比 zero_sizet 慢。 (-O3 慢两倍)。有趣的一件事是,在 -O3 zero_1 与 zero_sizet 一样快。然而,反汇编的函数大约有四倍的指令(我认为是由循环展开引起的)。另外,我尝试进一步优化 zero_sizet,但编译器总是超过我,但这并不奇怪。

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

edit3:修复了测试代码中的bug,测试结果不受影响

edit4: 在查看反汇编的 VS2010 C 运行时时,我注意到memset 有一个针对零的 SSE 优化例程。很难打败它。

【问题讨论】:

  • 与其假设memset 使用mov,不如尝试反汇编编译器的输出?不同的编译器会做不同的事情。如果xor 在给定架构上更快,那么如果某些编译器将memset(ptr, 0, nbytes) 优化为xor 指令也就不足为奇了。
  • 我不知道有一个编译器使用 XOR 来归零内存。也许是一个寄存器,但不是内存。为了使用 XOR 到零内存,您必须先读取内存,然后 XOR,然后写入内存。
  • 如果合适,calloc 可能实际上是空闲的,因为实现可能会提前零页,而 CPU 则处于空闲状态。这算不算? ;-)
  • @aaa carp,GregS 是对的,使用 XOR 清除内存几乎永远不会是正确的答案。突然想到的例外是最小的 Microchip PIC CPU,其中所有可用的 RAM 实际上只是一大块通用寄存器。使用 XOR 清除寄存器在 CISC 架构中很常见。一些 RISC 架构有效地包括一个通用寄存器,该寄存器被硬连线以保持零仅用于此目的。
  • @TravisGockel 同样非常不明智:madvise() 可能只是一个空操作。它可能适用于特定的内核和 libc 版本,但听起来如果您升级任何一个,它都可能很容易损坏。

标签: c libc


【解决方案1】:

x86 是相当广泛的设备。

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

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

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

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

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

【讨论】:

  • 10 年前的响应,但 ZeroMemory 完全是 memset 的宏:D
【解决方案2】:

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

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

【讨论】:

  • 另外:理论上,memset 可能有一个特殊情况 0,当该参数是文字时,它在编译时选择(通过内联或作为内在操作)。不知道有没有人。
  • @Steve Jessop:有趣的想法(尤其是它可能是编译时)。我记得曾经读过某人对 memset 的特立独行的实现,它对你实际使用 memset 的所有事情都有特殊情况。
  • gcc 通常使用memset() 的内联内置实现。有趣的是,我记得读过关于 memset() 的错误实现,它总是将值设置为 0 - 这在 年 中没有被注意到,因为显然绝大多数时间 memset() 被使用设置为零!
  • "memset 通常被设计为非常非常快速的通用设置/归零代码..." - 我认为这不太正确。无法保证 memset 将在优化过程中存活,因此可能不会发生归零。 memset_s make 提供了该保证,但 Glibc 人员拒绝提供。另请参阅Issue 17879: Library is missing memset_s。
【解决方案3】:

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

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

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

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

【讨论】:

  • 不,上次我检查时,gcc 没有。但是 g++ 确实优化了对 std::fill 的调用(除非启用了优化 -ftree-loop-distribute-patterns,在这种情况下它也成为对 memset 的调用),这是 memset 的 C++ 模拟。
  • 也许值得一提:我刚刚做了一个测试,发现了一个奇妙的东西:使用-ftree-loop-distribute-patterns,将std∷fill 更改为memset 程序比程序快×10(!)倍没有,即当std∷fill 被g++ 内联时,即使我添加了march=native。因此 gcc-4.9.2 在优化方面并不是那么好,因为这意味着有一种方法可以进一步优化std∷fill。顺便说一句,我还使用 clang 进行了测试,我发现它的优化效果更差——使用 -O3 级别它甚至不会从代码中删除 push-pop。
  • 好吧,“免费”的评论并不完全正确。将内存初始化为零只是发生在操作系统中,而不是在您的程序控制下。谁知道 mmap 函数是谁写的,它的 IT 效率高吗?如果时间关键是重要的,最好得到未初始化的内存,然后用汇编程序自己清除它。
  • 编译器为大多数基本函数提供了内在函数,包括这个。库函数应使用编译器中的内在函数(如果可用)。
【解决方案4】:

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

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

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

【讨论】:

  • 不需要加载缓存行,只需将零写入缓存行即可。
【解决方案5】:

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

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

也就是说,如果您有 特定 需求(其中大小将始终为 tiny *or* huge ),您可以通过下降到装配级别来获得速度提升。例如,使用直写操作将大量内存归零,而不会污染 L2 缓存。

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

【讨论】:

  • 即使在 sparc 上的旧 gcc 实现也替换了 memcpy 和 memset 调用 mith mov instructions,当大小在编译时已知且不太大时。
【解决方案6】:

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

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

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

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

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

【讨论】:

    【解决方案7】:

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

    【讨论】:

    • 感谢您指出这一点。我最初在 Atom 上运行测试,但现在我只能访问 PPC。至少在这台机器上我可以确认你说的话。我猜缓存正在发挥它的魔力。我想我现在必须转向汇编程序。
    【解决方案8】:

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

    【讨论】:

    • 你真的应该使用QueryPerformanceTimer() 系列函数。
    • @xiaomao 这些功能只会给你更高的毫秒精度,但我会幽默你。在 32 位 Windows 上,结果为:memset() took: 5575.6 ms. MemSet() took: 5539.97 ms.,在 64 位 Windows 上,结果为:memset() took: 2778.73 ms. MemSet() took: 2774.18 ms.
    • const _UINT64 m = _UINT64_MAX 看起来很可疑,因为你后来将它与int 进行比较,j &lt; o 应该被好的编译器优化掉,你对 memset() 不公平,因为它也是预热阵列test。
    • @tc。是的,“什么使阵列升温”,是的,运行测试会很有趣,其中两种方法在单个运行时交替多次。我回家后可能会看看它。
    【解决方案9】:

    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。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-12-28
      • 2011-12-16
      • 1970-01-01
      • 1970-01-01
      • 2019-01-31
      相关资源
      最近更新 更多