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

emb*_*guy 9 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;
Run Code Online (Sandbox Code Playgroud)

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

memset((void *)p, 0, sizeof(my_buffer));
Run Code Online (Sandbox Code Playgroud)

在此:

for (i = 0; i < sizeof(my_buffer); i++)
{
    *p++ = 0;
}
Run Code Online (Sandbox Code Playgroud)

Mys*_*ial 24

这适用于memset()memcpy():

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

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

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


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

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

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

虽然memset()memcpy()通常是编译器内在与编译器进行特殊处理,他们仍然是通用的功能.他们对数据类型没有任何说明,包括数据的对齐.

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

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

  • +1以获得全面的答案。然而,“memset”的一个缺点是它并不总是您想要的行为(根据标准)。例如,指针和浮点数都不一定具有所有零位的 0/NULL 表示形式。 (2认同)

Cra*_*rks 7

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

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

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

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

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


old*_*mer 5

请记住这一点

for (i = 0; i < sizeof(my_buffer); i++)
{
    p[i] = 0;
}
Run Code Online (Sandbox Code Playgroud)

也可以快于

for (i = 0; i < sizeof(my_buffer); i++)
{
    *p++ = 0;
}
Run Code Online (Sandbox Code Playgroud)

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

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