在C中使用memset()有什么好处

我很好奇在类似下面的情况下使用memset()在效率方面是否有任何优势。

鉴于以下缓冲区声明……

struct More_Buffer_Info { unsigned char a[10]; unsigned char b[10]; unsigned char c[10]; }; struct My_Buffer_Type { struct More_Buffer_Info buffer_info[100]; }; struct My_Buffer_Type my_buffer[5]; unsigned char *p; p = (unsigned char *)my_buffer; 

除了少量代码之外,使用它还有一个优点:

 memset((void *)p, 0, sizeof(my_buffer)); 

在此:

 for (i = 0; i < sizeof(my_buffer); i++) { *p++ = 0; } 

这适用于memset()memcpy()

  1. 更少的代码:正如您已经提到的,它更短 – 代码行更少。
  2. 更易读:更短通常也使它更具可读性。 ( memset()比该循环更具可读性)
  3. 它可以更快:它有时可以允许更积极的编译器优化。 (所以它可能会更快)
  4. 未对齐:在某些情况下,当您处理不支持未对齐访问的处理器上的未对齐数据时, memset()memcpy()可能是唯一的干净解决方案。

为了扩展第3点, memset()可以由编译器使用SIMD等进行大量优化。 如果你编写一个循环,编译器首先需要“弄清楚”它在尝试优化之前的作用。

这里的基本思想是memset()和类似的库函数,在某种意义上,“告诉”编译器你的意图。


正如@Oli在评论中提到的那样,有一些缺点。 我将在这里扩展它们:

  1. 你需要确保memset()实际上做你想要的。 该标准并未说明各种数据类型的零在内存中必然为零。
  2. 对于非零数据, memset()仅限于1个字节的内容。 因此,如果要将int的数组设置为零以外的值(或0x01010101或其他…),则不能使用memset() )。
  3. 虽然很少见,但有一些极端情况,实际上可以通过自己的循环在性能上击败编译器。*

*我将从我的经验中给出一个例子:

尽管memset()memcpy()通常是编译器内在函数,并且编译器具有特殊处理function,但它们仍然是通用函数。 他们对数据类型没有任何说明,包括数据的对齐。

因此,在一些(非常罕见的)情况下,编译器无法确定内存区域的对齐,因此必须生成额外的代码来处理未对齐。 然而,如果你是程序员,100%确定对齐,使用循环可能实际上更快。

一个常见的例子是使用SSE / AVX内在函数时。 (例如复制一个16/32字节对齐的float数组)如果编译器无法确定16/32字节对齐,则需要使用未对齐的加载/存储和/或处理代码。 如果您只是使用SSE / AVX对齐的加载/存储内在函数编写循环,您可能会做得更好。

 float *ptrA = ... // some unknown source, guaranteed to be 32-byte aligned float *ptrB = ... // some unknown source, guaranteed to be 32-byte aligned int length = ... // some unknown source, guaranteed to be multiple of 8 // memcopy() - Compiler can't read comments. It doesn't know the data is 32-byte // aligned. So it may generate unnecessary misalignment handling code. memcpy(ptrA, ptrB, length * sizeof(float)); // This loop could potentially be faster because it "uses" the fact that // the pointers are aligned. The compiler can also further optimize this. for (int c = 0; c < length; c += 8){ _mm256_store_ps(ptrA + c, _mm256_load_ps(ptrB + c)); } 

这取决于编译器和库的质量。 在大多数情况下,memset是优越的。

memset的优点是在许多平台上它实际上是编译器内在的 ; 也就是说,编译器可以“理解”将大量内存设置为某个值的意图,并可能生成更好的代码。

特别是,这可能意味着使用特定的硬件操作来设置大的内存区域,例如x86上的SSE,PowerPC上的AltiVec,ARM上的NEON等等。 这可以带来巨大的性能提升。

另一方面,通过使用for循环,你告诉编译器做一些更具体的事情,“将这个地址加载到寄存器中。给它写一个数字。在地址上加一个。给它写一个数字”,等等上。 从理论上讲,一个完全智能的编译器会识别它的循环,并将其转换为memset。 但我从未遇到过这样做的真正的编译器。

因此,假设memset是由聪明人编写的,是为编译器支持的特定平台和硬件设置整个内存区域的最佳和最快的方法。 这通常是, 但并非总是如此。

请记住这一点

 for (i = 0; i < sizeof(my_buffer); i++) { p[i] = 0; } 

也可以快于

 for (i = 0; i < sizeof(my_buffer); i++) { *p++ = 0; } 

正如已经回答的那样,编译器通常具有memset()memcpy()和其他字符串函数的手动优化例程。 而且我们谈得快得多。 现在来自编译器的快速 memcpy或memset的代码量,指令数通常比你建议的循环解决方案大得多。 更少的代码行,更少的指令并不意味着更快。

无论如何,我的信息是尝试两者。 反汇编代码,看到差异,尝试理解,如果你不在堆栈溢出问问题。 然后使用计时器和时间两个解决方案,调用几千或几十万次的memcpy函数和时间整个事情(以消除时间错误)。 确保你做了7个项目或5个项目的简短副本,以及每个memset的数百个字节的大型副本,并在你使用时尝试一些素数。 在某些系统上的某些处理器上,对于像3或5之类的东西,或者类似的东西,你的循环可以更快,尽管速度很慢。

这是关于性能的一个提示。 计算机中的DDR内存可能是64位宽,需要一次写入64位,也许它有ecc,你必须计算这些位并一次写入72位。 并不总是那个确切的数字,但按照这里的想法,它将有意义的32位或64或128或其他什么。 如果你对ram执行单字节写指令,硬件将需要做两件事之一,如果沿途没有缓存,内存系统必须执行64位读取,修改一个字节,然后把它写回来。 如果没有某种硬件优化,在那个dram行中写入8个字节,就是16个内存周期,并且dram非常慢,不要被1333mhz数字所欺骗。

现在,如果你有一个缓存,第一个字节写将要求从dram读取一个缓存行,这是这些64位读取中的一个或多个,接下来的7或15或任何字节写入可能会非常快他们只去高速缓存,而不去ddr,最终高速缓存行出现了这些64位或任何ddr位置的dram,slow,所以一两个或四个等等。 因此,即使您只是在进行写操作,您仍然需要读取所有ram然后编写它,因此需要两倍的循环。 如果可能的话,对于某些处理器和内存系统,memcpy的memset或写入部分可以是具有整个缓存行或整个ddr位置的单个指令,并且不需要读取,立即加倍速度。 这不是所有优化的工作方式,但它希望能让您了解如何考虑问题。 将程序拖入缓存行中的缓存中,您可以将执行的指令数量增加一倍或三倍,如果返回的话,您可以减少DDR周期数量的一半或四分之一或更多削减,并获得整体胜利。

如果起始地址为奇数,编译器memset和memcpy例程将至少执行字节操作,如果未在32位上对齐,则执行16位。 然后是32位,如果没有在64上对齐,直到它们达到该指令集/系统的最佳传输大小。 在arm上,他们倾向于瞄准128位。 因此,前端的最坏情况是单个字节,然后单个半字然后是几个字,然后进入主集或复制循环。 在ARM 128位传输的情况下,每条指令写入128位。 然后在后端如果未对齐相同的交易,几个字,一个半字,一个字节最坏的情况。 你也会看到这些库做的事情,如果字节数小于X,其中X是一个像13这样的小数字,那么它就像你的那样进入循环,只是复制一些字节,因为指令数和时钟周期支持该循环更小/更快。 反汇编或找到ARM的gcc源代码,可能是mips和其他一些好的处理器,看看我在说什么。

两个优点:

  1. 具有memset的版本更易于阅读 – 这与具有较少行代码的行相关,但不相同。 知道memset版本的作用需要更少的思考 ,特别是如果你写它

     memset(my_buffer, 0, sizeof(my_buffer)); 

    而不是通过p的间接和不必要的转换为void * (注意:如果你真的用C而不是C ++编写代码,那么这是不必要的 – 有些人不清楚这些差异)。

  2. memset 可能一次能够写入4或8个字节和/或利用特殊的缓存提示指令; 因此它可能比你的一次一个字节循环更快。 (注意:有些编译器足够聪明,可以识别批量清除循环,并用更宽的内存写入或者对memset的调用替换。你的里程可能会有所不同。在尝试削减周期之前一定要测量性能。)

memset提供了编写代码的标准方法,让特定的平台/编译器库确定最有效的机制。 基于数据大小,它可以例如尽可能地执行32位或64位存储。

您的变量p仅用于初始化循环。 memset的代码应该很简单

 memset( my_buffer, 0, sizeof(my_buffer)); 

这更简单,更不容易出错。 void*参数的关键在于它将接受任何指针类型,显式转换是不必要的,并且对不同类型的指针的赋值是没有意义的。

因此,在这种情况下使用memset()一个好处是避免使用不必要的中间变量。

另一个好处是,任何特定平台上的memset()都可能针对目标平台进行优化,而循环效率则取决于编译器和编译器设置。