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

时间:2011-12-16 00:50:01

标签: c embedded 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;
}

6 个答案:

答案 0 :(得分:23)

这适用于memset()memcpy()

  1. 减少代码:正如您已经提到的,它更短 - 代码行更少。
  2. 更易读:更短通常会使其更具可读性。 (memset()比那个循环更具可读性)
  3. 它可以更快:有时可以允许更积极的编译器优化。 (所以它可能会更快)
  4. 未对齐:在某些情况下,当您处理不支持未对齐访问的处理器上的未对齐数据时,memset()memcpy()可能是唯一的清洁解决方案。
  5. 要扩展第3点,编译器可以使用SIMD等对memset()进行大量优化。如果你编写一个循环,编译器首先需要“弄清楚”它在尝试优化之前的作用。

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


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

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

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

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

      一个常见的例子是使用SSE / AVX内在函数时。 (例如复制一个16/32字节对齐的float s数组)如果编译器无法确定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));
      }
      

答案 1 :(得分:7)

这取决于编译器和库的质量。在大多数情况下,memset更胜一筹。

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

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

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

因此,假设memset是由聪明人编写的,是设置整个内存区域的最佳和最快的方式,适用于编译器支持的特定平台和硬件。那是oftenbut not always,是真的。

答案 2 :(得分:5)

请记住这个

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

也可以比

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

正如已经回答的那样,编译器通常具有memset()memcpy()和其他字符串函数的手动优化例程。而且我们谈得快得多。现在,编译器的 fast 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,最终高速缓存行走向dram,slow,所以这些64位或任何ddr位置中的一个或两个或四个等等。因此,即使您只是在进行写操作,您仍然必须阅读所有该ram然后编写它,因此需要两倍的循环。如果可能,并且它与某些处理器和存储器系统一起,memcpy的memset或写入部分可以是具有整个高速缓存行或整个ddr位置的单个指令,并且不需要读取,立即加倍速度。这不是所有优化的工作方式,但它希望能让您了解如何思考问题。将您的程序拉入缓存行中的缓存中,您可以将执行的指令数量增加一倍或三倍,如果返回的话,您可以减少DDR周期数量的一半或四分之一或更多,并且总体上获胜。

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

答案 3 :(得分:3)

两个优点:

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

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

    而不是通过p的间接和不必要的强制转换为void *(注意:如果您在C中使用真正的编码而不是C ++,则只有不必要 - 有些人不清楚区别)。

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

答案 4 :(得分:1)

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

答案 5 :(得分:1)

您的变量p仅用于初始化循环。 memset的代码应该只是

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

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

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

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