49

memset() 比循环更有效for

考虑到这段代码:

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

和这个:

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

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

4

7 回答 7

40

最肯定的是,memset将比那个循环快得多。请注意一次处理一个字符的方式,但这些函数经过优化,一次设置几个字节,即使在可用时使用 MMX 和 SSE 指令。

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

于 2011-09-09T21:37:03.147 回答
37

好吧,不如我们看看生成的汇编代码,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它将在至少和循环这样的简单实现一样快。在调试版本下尝试一下,你会注意到循环没有被替换。

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

于 2011-09-09T21:45:31.070 回答
13

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

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

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

于 2011-09-09T21:35:04.277 回答
9

答案是“视情况而定”。 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

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

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

于 2011-09-09T21:37:38.367 回答
3

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

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

于 2011-09-09T21:38:50.657 回答
2

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

于 2011-09-09T21:40:49.340 回答
1

也可以使用其他技术,如减少循环数量的循环展开。memset()的代码可以模仿著名的 duff 的设备

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 未命中、写时复制)。

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

于 2021-02-11T10:50:37.937 回答