11

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

6 回答 6

28

这适用于memset()memcpy()

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

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

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


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

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

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

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

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

一个常见的例子是使用 SSE/AVX 内部函数时。(例如复制 s 的 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));
}
于 2011-12-16T00:54:06.537 回答
8

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

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

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

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

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

于 2011-12-16T00:56:24.163 回答
5

请记住,这

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

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

于 2011-12-16T02:09:39.360 回答
4

两大优势:

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

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

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

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

于 2011-12-16T00:57:16.757 回答
1

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

于 2011-12-16T00:55:41.710 回答
1

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

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

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

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

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

于 2011-12-16T17:31:44.757 回答