C增量指针与for()循环性能[关闭]

Posted

技术标签:

【中文标题】C增量指针与for()循环性能[关闭]【英文标题】:C increment pointer vs for() loop performance [closed] 【发布时间】:2018-08-03 23:52:14 【问题描述】:

我一直在想,是不是在所有情况下都等价于这样写:

char* op = buf;
int l = buflen;
while( l > 0 )

    *op = bufval;
    op++;
    l--;

这样:

int l;
for( l = 0; l < buflen; l++ )

    buf[ l ] = bufval;

从广泛的现代编译器和计算平台性能的角度来看?后面的代码当然更优雅,但这不是重点。我看到检查“l 大于 0”就像汇编器术语中的简单 JNZ,而“l 小于 bufval”需要比较操作。 “buf[l]”可能不需要相对于“op++”的额外指令,但我不知道这如何影响实践中的性能。第一个变体在某些情况下更可取,例如当“op”需要增加 3 时,这比我想写的“l*3”要好得多。

【问题讨论】:

程序员的微优化很少有很大的不同。编译器非常擅长理解您的意思的本质,并根据手头的处理器选择最有效的实现,无论您如何编写它。所以我想说,别担心。 有时您自己的微优化会使情况变得更糟。例如,使用 XOR 交换数组中的值而不是使用临时变量可能会阻止编译器对其进行优化。 memset(buf, bufval, buflen) 可能比这两个 ***.com/questions/1373369/… 都快。但是请注意,您可以安全地使用memset,因为bufchar* 如果您想看看哪个更好,请考虑在对您重要的平台上以合理的优化级别查看汇编程序输出。比较和对比——如果他们比较相同,不要感到惊讶,所以没有对比。在for 循环中,有两个添加(一个来自下标);在while 循环中,有一个加法和一个减法。不会有很多可衡量的性能差异。如果你不能从汇编输出中推断出来,去测量结果,但要小心——性能测量是一件棘手的事情。 先使用正确的类型。 【参考方案1】:

这是为代码 sn-ps 生成的程序集

f2:                          ;; pointer based approach
        test    edx, edx
        jle     .L5
        sub     edx, 1
        movsx   esi, sil
        movsx   rdx, edx
        add     rdx, 1
        jmp     memset
.L5:
        ret

f3:                          ;; loop based approach
        test    edx, edx
        jle     .L8
        sub     edx, 1
        movsx   esi, sil
        add     rdx, 1
        jmp     memset
.L8:
        ret

我知道更短的汇编并不意味着更快的代码,但是编译器确实会为基于指针的版本生成一些额外的指令。当我使用 clang 尝试相同的指令时,指令数量的差异甚至更大。如果有的话,基于指针的版本会慢一点,而不是更快。

请注意,这两个都在调用memset,而在此之前的代码只是检查和设置用于调用memset 的寄存器。您可以自己拨打memset

memset(buf, bufval, buflen)

这会生成以下程序集

f1:                          ;; memset based approach
        movsx   rdx, edx
        jmp     memset

回到最初的问题,哪个版本更快。现代编译器很聪明,这一点怎么强调都不为过。像这样的微优化很少(如果有的话)提供性能优势。编写惯用的代码,让编译器更容易理解其意图,总是会给你带来更好的性能。

如果你想使用汇编输出,这里是 godbolt 的链接:https://godbolt.org/g/NxHS5F

【讨论】:

另外请注意,如果一个人有一种特定的受虐倾向,他们可以进一步剖析指令以确定哪些指令在流水线上停止,以及每条指令的时钟是多少。拥有更多(或更少)指令并不一定等同于更慢(或更快)的代码。没那么简单。更有理由相信专业的编译器作者会生成优化的代码,并且只有在您能够证明存在意想不到的瓶颈并且您可以证明规避它的方法。 int buflen 更改为 size_t buflen 并且编译器生成的准备代码在所有三个版本上都会下降。 @BenVoigt:l 的类型也应该是 size_t,并且名称更改为其他名称以避免与数字 1 混淆。【参考方案2】:

你看到的不同只是因为你的函数是用一种非常不友好和糟糕的编译器优化器编写的。

如果写得更好,两者应该编译相同:

void foo(char *buf, int bufval, size_t buflen)

    while(buflen--)
    
        *buf++ = bufval;
    



void foo1(char *buf, int bufval, size_t buflen)


    size_t l;
    for( l = 0; l < buflen; l++ )
    
        buf[ l ] = bufval;
    



foo:
        test    rdx, rdx
        je      .L11
        movsx   esi, sil
        jmp     memset
.L11:
        ret
foo1:
        test    rdx, rdx
        je      .L14
        movsx   esi, sil
        jmp     memset
.L14:
        ret

程序员方面的优化质量为 80%。如果您编写问题中的程序,您总是会得到糟糕的机器代码。

****编辑****

-fno-builtin相同

void foo(char *buf, int bufval, size_t buflen)

    while(buflen--)
    
        *buf++ = bufval;
    



void foo1(char *buf, int bufval, size_t buflen)


    size_t l;
    for( l = 0; l < buflen; l++ )
    
        buf[ l ] = bufval;
    


foo:
        lea     rax, [rdi+rdx]
        test    rdx, rdx
        je      .L9
.L11:
        add     rdi, 1
        mov     BYTE PTR [rdi-1], sil
        cmp     rdi, rax
        jne     .L11
.L9:
        ret
foo1:
        lea     rax, [rdi+rdx]
        test    rdx, rdx
        je      .L15
.L17:
        mov     BYTE PTR [rdi], sil
        add     rdi, 1
        cmp     rdi, rax
        jne     .L17
.L15:
        ret

【讨论】:

为了公平对待 OP,产生影响的东西(长度变量的类型是 int 还是 size_t)在问题中并不是“坏”,它只是不匹配编译器想要使用的memset 调用。编译器实际上看穿了代码清洁度的东西,比如不必要的临时变量和那些不会影响机器代码的东西。 -fno-builtin 也是如此。查看我修改后的答案

以上是关于C增量指针与for()循环性能[关闭]的主要内容,如果未能解决你的问题,请参考以下文章

喵呜:C++基础系列:auto关键字(C++11)基于范围的for循环(C++11)指针空值nullptr(C++11)

喵呜:C++基础系列:auto关键字(C++11)基于范围的for循环(C++11)指针空值nullptr(C++11)

喵呜:C++基础系列:auto关键字(C++11)基于范围的for循环(C++11)指针空值nullptr(C++11)

C语言 如何定义一个二维指针数组?

C语言中的for循环

长阵列性能问题