【问题标题】:Is memset() more efficient than for loop in C?memset() 是否比 C 中的 for 循环更有效?
【发布时间】:2011-11-14 02:59:21
【问题描述】:

memset()for 循环更有效吗?

考虑这段代码:

char x[500];
memset(x,0,sizeof(x));

还有这个:

char x[500];
for(int i = 0 ; i < 500 ; i ++) x[i] = 0;

哪个更有效,为什么?硬件中是否有任何特殊指令可以进行块级初始化。

【问题讨论】:

  • 是的。不,也许。这取决于。获得有用答案的唯一方法是在您的环境中对其进行分析和分析。在我的编译器、我的程序、我的计算机上,哪个更快,并没有告诉你任何有用的信息。
  • 为什么要调查?除非有数据显示(你没有达到你的性能目标并且调查指向这部分代码),这段代码可能不是热点,你应该尽可能简单、可读和可维护的代码。
  • 如果你有一个编译器不能用 memset() 代替那个循环,那么你应该找到另一个编译器。
  • @Chris: Ummmm....那时他们可能应该学习。我想我是一只 27 岁的恐龙,但我对那些不懂基本装配的所谓“工程师”有意见……我并不是说不应该使用分析器,而是对于这样一个微不足道的比较,它应该是不必要的。
  • @Chris:这就是为什么我接触到这么多网络人(和女孩)编写的应用程序比他们应该的要慢得多。不一定是因为他们不懂汇编,而是因为他们从来没有真正了解过他们使用的数据结构的性能特征,以及他们的高级代码在变成机器代码时如何执行。不过我离题了,这是另一个地点和时间的讨论。

标签: c performance memset


【解决方案1】:

也可以使用其他技术,例如loop unrolling,以减少循环次数。 memset()的代码可以模仿著名的duff's device

void *duff_memset(char *to, int c, size_t count)
{
    size_t n;
    char *p = to;
    n = (count + 7) / 8;
    switch (count % 8) {
    case 0: do { *p++ = c;
    case 7:      *p++ = c;
    case 6:      *p++ = c;
    case 5:      *p++ = c;
    case 4:      *p++ = c;
    case 3:      *p++ = c;
    case 2:      *p++ = c;
    case 1:      *p++ = c;
            } while (--n > 0);
    }
    return to;
}

过去用来提高执行速度的那些技巧。但在现代架构上,这往往会增加代码大小并增加缓存未命中率。

因此,很难说哪个实现更快,因为它取决于编译器优化的质量、C 库利用特殊硬件指令的能力、您正在操作的数据量以及底层操作系统的特性(页面错误管理、TLB 未命中、Copy-On-Write)。

例如,在 glibc 中,memset() 的实现以及各种其他“复制/设置”函数,如 bzero()strcpy () 依赖于架构以利用各种优化的硬件指令,例如 SSEAVX

【讨论】:

    【解决方案2】:

    毫无疑问,memset 会比那个循环快得多。请注意您如何一次处理一个字符,但这些函数经过优化,一次设置几个字节,甚至在可用时使用 MMX 和 SSE 指令。

    我认为这些优化的典型示例是 GNU C 库 strlen 函数,但通常不会引起注意。有人会认为它至少具有 O(n) 性能,但实际上它具有 O(n/4) 或 O(n/8) 取决于架构(是的,我知道,在大 O() 中将是相同的,但实际上你得到了 八分之一 的时间)。如何?棘手,但很好:strlen.

    【讨论】:

    • 任何优化编译器都会将 for 循环替换为最优序列(可能是对 memset 的调用)。
    • 此外,即使编译器为循环发出次优代码,也不能保证它“更快”。 500 并不是一个非常高的数字,如果发生软或硬页面错误,这将大大超过循环本身的成本。
    • @Stephen Canon:呵呵。我正在用 clang/LLVM 编译一个 C 库,它用对 memset 的调用替换了库的 memset for 循环。哎呀!深度递归。
    • @Diego 这不是 500/8 分配比 500 分配到 0 慢或快的问题。由于系统中的其他影响,像这样的微基准很少有用。在现代处理器上,仅比较之间的差异可能在 62 个周期与 500 个周期之间。我的建议是,如果您在运行代码时遇到大约 1000 万个周期的硬页面错误,那么您节省的 438 个周期只是噪音。
    • @Richard Pennington: -fno-builtin-memset.
    【解决方案3】:

    好吧,不如我们看看生成的汇编代码,VS 2010下全面优化。

    char x[500];
    char y[500];
    int i;      
    
    memset(x, 0, sizeof(x) );   
      003A1014  push        1F4h  
      003A1019  lea         eax,[ebp-1F8h]  
      003A101F  push        0  
      003A1021  push        eax  
      003A1022  call        memset (3A1844h)  
    

    还有你的循环...

    char x[500];
    char y[500];
    int i;    
    
    for( i = 0; i < 500; ++i )
    {
        x[i] = 0;
    
          00E81014  push        1F4h  
          00E81019  lea         eax,[ebp-1F8h]  
          00E8101F  push        0  
          00E81021  push        eax  
          00E81022  call        memset (0E81844h)  
    
          /* note that this is *replacing* the loop, 
             not being called once for each iteration. */
    }
    

    所以,在这个编译器下,生成的代码是完全一样的。 memset 很快,并且编译器足够聪明,知道您正在做与调用 memset 一次相同的事情,所以它会为您完成。

    如果编译器实际上将循环保持原样,那么它可能会更慢,因为您一次可以设置多个字节大小的块(即,您可以至少展开循环一点。您可以假设memset至少与循环等幼稚实现一样快。在调试版本下尝试,您会注意到循环没有被替换。

    也就是说,这取决于编译器为您做什么。查看反汇编始终是准确了解发生了什么的好方法。

    【讨论】:

    • 有趣,我的版本并没有导致循环被转换为 memset,但这可能是因为对于我的测试,循环在全局上运行(否则整个循环被删除是不必要的。)
    • @Michael:我使用xyprintf 添加了几个调用,以确保它们没有被完全优化掉,因为它们没有被使用。它当然在某种程度上依赖于编译器和平台,但是任何半途而废的优化编译器都应该在优化打开的情况下摆脱循环。
    • 甚至 memset() 与数组初始化(例如:a[n]={0})采用相同的代码。 memset 的优点是数组大小可以是一个变量,这在初始化时是不可能的。我说的对吗?
    • 如何检查“反汇编”。
    • @young_souvlaki:在 VS 中? docs.microsoft.com/en-us/visualstudio/debugger/…
    【解决方案4】:

    答案是“视情况而定”。 memset 可能更高效,或者它可能在内部使用 for 循环。我想不出memset 效率会降低的情况。在这种情况下,它可能会变成更有效的 for 循环:您的循环迭代 500 次,每次将数组的字节值设置为 0。在 64 位机器上,您可以循环遍历,一次设置 8 个字节(一个 long long),这几乎快 8 倍,最后只处理剩余的 4 个字节(500%8)。

    编辑:

    事实上,这就是memset在glibc中所做的:

    http://repo.or.cz/w/glibc.git/blob/HEAD:/string/memset.c

    正如 Michael 指出的,在某些情况下(数组长度在编译时已知),C 编译器可以内联 memset,从而摆脱函数调用的开销。 Glibc 还为大多数主要平台(如 amd64)提供了 memset 的汇编优化版本:

    http://repo.or.cz/w/glibc.git/blob/HEAD:/sysdeps/x86_64/memset.S

    【讨论】:

    • 我能想到 memset 效率较低的情况:不能内联(很好)的编译器。
    • 我想如果后两个参数在编译时都知道,大多数人将很难与编译器生成的代码相等。
    【解决方案5】:

    同意以上观点。这取决于。但是,可以肯定 memset 更快或等于 for 循环。如果您不确定自己的环境或懒得测试,请选择安全路线并使用 memset。

    【讨论】:

    • 请说明您所说的“上面”是指哪一个。
    【解决方案6】:

    优秀的编译器会识别 for 循环,并用最佳的内联序列或对 memset 的调用来替换它。当缓冲区较小时,它们还将用最佳内联序列替换 memset。

    实际上,使用优化编译器生成的代码(因此性能)将是相同的。

    【讨论】:

    • 你能提供引用吗?
    • 用任何好的优化编译器试试看(例如goo.gl/2mWsxq)。我不确定这里有什么可以“引用”的。
    • 学术引用总是很重要,即使只是灰色文献。
    【解决方案7】:

    这真的取决于编译器和库。对于较旧的编译器或简单的编译器,memset 可能在库中实现,并且性能不会比自定义循环好。

    对于几乎所有值得使用的编译器,memset 是一个内在函数,编译器将为它生成优化的内联代码。

    其他人建议进行分析和比较,但我不会打扰。只需使用 memset。代码简单易懂。在您的基准测试告诉您这部分代码是性能热点之前,请不要担心。

    【讨论】:

      猜你喜欢
      • 2016-04-15
      • 2013-06-28
      • 2023-03-07
      • 1970-01-01
      • 1970-01-01
      • 2012-07-18
      • 2020-07-22
      • 2014-09-01
      • 2015-11-30
      相关资源
      最近更新 更多