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
,因为buf
是char*
。
如果您想看看哪个更好,请考虑在对您重要的平台上以合理的优化级别查看汇编程序输出。比较和对比——如果他们比较相同,不要感到惊讶,所以没有对比。在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)