【问题标题】:What is the advantage of using memset() in C在 C 中使用 memset() 有什么好处
【发布时间】:2012-01-21 15:36:57
【问题描述】:

我很好奇在类似于以下情况的情况下使用 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;
}

【问题讨论】:

  • 如果p 将指向struct My_Buffer_Type 类型的对象,请以这种方式声明它,而不是给它一个不同的类型并用不必要的强制转换使代码混乱。
  • 还要考虑初始化:struct My_Buffer_Type my_buffer[5] = {0}; 递归地将所有内容初始化为(正确的)0
  • @sarnold 感谢您的建议。我试图为示例简化一个更复杂的缓冲区。
  • @pmg 该代码实际上用于(最终)清除 spi 缓冲区。它被用作经常调用的函数的一部分。
  • @Embedded:可以简化更复杂的代码。 :)

标签: c embedded memset


【解决方案1】:

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

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

这更简单,更不容易出错。 void* 参数的意义在于它可以接受任何指针类型,不需要显式转换,并且分配给不同类型的指针是没有意义的。

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

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

【讨论】:

    【解决方案2】:

    这适用于memset()memcpy()

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

    为了扩展第三点,memset() 可以由编译器使用 SIMD 等进行大量优化。如果您改为编写循环,编译器首先需要“弄清楚”它的作用,然后才能尝试对其进行优化。

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


    正如@Oli 在 cmets 中提到的,存在一些缺点。我将在这里扩展它们:

    1. 您需要确保memset() 确实按照您的意愿行事。标准并没有说各种数据类型的零在内存中一定是零。
    2. 对于非零数据,memset() 仅限于 1 字节内容。因此,如果您想将 ints 的数组设置为零以外的值(或 0x01010101 或其他值...),则不能使用 memset()
    3. 虽然很少见,但在某些极端情况下,实际上可以使用您自己的循环在性能上击败编译器。*

    *我将根据我的经验举一个例子:

    虽然memset()memcpy() 通常是编译器内部函数并由编译器进行特殊处理,但它们仍然是通用 函数。他们只字不提数据类型,包括数据的对齐方式。

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

    一个常见的例子是使用 SSE/AVX 内部函数时。 (例如复制floats 的 16/32 字节对齐数组)如果编译器无法确定 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 以获得全面的答案。但是,memset 的一个缺点是它并不总是您想要的行为(根据标准)。例如,指针和浮点数都不一定有所有零位的 0/NULL 表示。
    • ++1 ;对齐和跨步可以在大容量内存操作中带来天壤之别。我工作的一个平台实际上基于已知的数据对齐方式(例如 memcpy32()、memcpy128()、memcpy256())提供了多种不同的 memcpy/memset/etc 实现,因为使用硬件的特殊“一次移动整个缓存行”操作。
    • memcpy/memset 实现中,我看到有序言和尾声要复制到一定的对齐,然后使用最大的寄存器。所以你的循环必须非常小才能注意到差异,不是吗?
    【解决方案3】:

    记住这个

    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,速度很慢,因此这些 64 位或任何 ddr 位置中的一两个或四个等。因此,即使您只进行写入,您仍然必须读取所有内存然后写入,因此需要两倍的周期。如果可能的话,并且对于某些处理器和内存系统,memset 或 memcpy 的写入部分可以是具有整个高速缓存行或整个 ddr 位置的单个指令,并且不需要读取,立即加倍速度。这不是所有优化的工作方式,但它希望能让您了解如何思考问题。随着您的程序在高速缓存行中被拉入高速缓存,您可以将执行的指令数量增加一倍或三倍,如果作为回报,您将 DDR 周期数减少一半或四分之一或更多,并且您总体上是赢家。

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

    【讨论】:

    • 很棒的答案。谢谢你这么彻底。我在想我会反汇编并查看指令差异。
    【解决方案4】:

    两个优点:

    1. 带有memset 的版本更易于阅读——这与更少的代码行有关,但并不相同。了解memset 版本的功能不需要思考,尤其是如果您编写它

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

      而不是通过p 进行间接转换和不必要的强制转换为void *(注意:只有当您真的 使用C 而不是C++ 编码时才不必要 - 有些人不清楚区别)。

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

    【讨论】:

    • memset(my_buffer, 0, sizeof my_buffer) 应该是 memset(my_buffer, 0, sizeof *my_buffer)(如果 my_buffer 是指针)或 memset(&amp;my_buffer, 0, sizeof my_buffer)(否则)。不幸的是,为其中的第一个提供诊断并非易事......
    • @TobySpeight 在 OP 代码的上下文中,我写的内容是正确的(除非您的样式指南在应用于变量时需要非括号 sizeof,这在我的不那么 -拙见是错误的)
    • 你是对的@zwol,因为my_buffer 在第一个参数中衰减为一个指针,但在sizeof my_buffer 它是整个数组。我应该回头看看 OP 而不是假设它是指针或值类型!
    • @TobySpeight 在生产 C 中,我避免在变量上使用 sizeof,因为它很容易搞砸并采用指针的大小而不是指针的大小。因此,如果我编写了 OP 的代码,它将是 memset(my_buffer, 0, sizeof(My_Buffer_Type) * N_BUFFER_ELTS) 其中 N_BUFFER_ELTS 是在数组声明中也使用的#define。我没有在这个答案中这样做,因为它与 OP 写的内容相差太远。
    • 我持相反的观点——永远不要在一个类型上使用sizeof,因为当我们发现我们需要为变量使用不同的类型时很容易被忽视。但这都是题外话,所以我们就这样吧!
    【解决方案5】:

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

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

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

    另一方面,通过使用 for 循环,您可以告诉编译器执行更具体的操作,“将此地址加载到寄存器中。向其写入一个数字。将一个数字添加到该地址。向其写入一个数字, “ 等等。从理论上讲,一个完全智能的编译器会识别出这个循环的本质并将其转换为 memset;但是我从来没有遇到过真正的编译器可以做到这一点。

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

    【讨论】:

      【解决方案6】:

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

      【讨论】:

        猜你喜欢
        • 2013-03-06
        • 2012-07-19
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-08-15
        • 2011-01-15
        • 2016-01-30
        • 2019-01-16
        相关资源
        最近更新 更多