我很好奇在类似下面的情况下利用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;
}
答案 0 :(得分:23)
这适用于memset()
和memcpy()
:
memset()
比那个循环更具可读性)memset()
和memcpy()
可能是唯一的清洁解决方案。要扩展第3点,编译器可以使用SIMD等对memset()
进行大量优化。如果你编写一个循环,编译器首先需要“弄清楚”它在尝试优化之前的作用。
这里的基本思想是memset()
和类似的库函数,在某种意义上,“告诉”编译器你的意图。
正如@Oli在评论中提到的那样,有一些缺点。我将在这里扩展它们:
memset()
实际上做了您想要的。该标准并未说明各种数据类型的零在内存中必然为零。memset()
仅限于1个字节的内容。因此,如果要将memset()
的数组设置为零以外的值(或int
或某些内容......),则无法使用0x01010101
。*我将从我的经验中给出一个例子:
虽然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是由聪明人编写的,是设置整个内存区域的最佳和最快的方式,适用于编译器支持的特定平台和硬件。那是often,but 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)
两个优点:
memset
版本更容易阅读 - 这与代码行数较少有关,但不相同。知道memset
版本的作用需要更少的思考,特别是如果你写的那样
memset(my_buffer, 0, sizeof(my_buffer));
而不是通过p
的间接和不必要的强制转换为void *
(注意:如果您在C中使用真正的编码而不是C ++,则只有不必要 - 有些人不清楚区别)。
memset
可能能够一次写入4或8个字节和/或利用特殊的缓存提示指令;因此它可能比你的一次一个字节循环更快。 (注意:有些编译器足够聪明,可以识别批量清除循环,并用更宽的内存写入或memset
调用。您的里程可能会有所不同。在尝试削减周期之前始终测量性能。)
答案 4 :(得分:1)
memset提供了编写代码的标准方法,让特定平台/编译器库确定最有效的机制。根据数据大小,它可以尽可能多地执行32位或64位存储。
答案 5 :(得分:1)
您的变量p
仅用于初始化循环。 memset的代码应该只是
memset( my_buffer, 0, sizeof(my_buffer));
更简单,更不容易出错。 void*
参数的关键点是它将接受任何指针类型,显式转换是不必要的,并且对不同类型的指针的赋值是没有意义的。
因此,在这种情况下使用memset()
的一个好处是避免使用不必要的中间变量。
另一个好处是任何特定平台上的memset()都可能针对目标平台进行优化,而循环效率则取决于编译器和编译器设置。