我正在学习C,请考虑以下代码段:
#include <stdio.h>
int main(void) {
int fahr;
float calc;
for (fahr = 300; fahr >= 0; fahr = fahr - 20) {
calc = (5.0 / 9.0) * (fahr - 32);
printf("%3d %6.1f\n", fahr, calc);
}
return 0;
}
将Celsius到华氏温度转换表从300打印到0.我用以下代码编译:
$ clang -std=c11 -Wall -g -O3 -march=native main.c -o main
我还使用此命令生成汇编代码:
$ clang -std=c11 -Wall -S -masm=intel -O3 -march=native main.c -o main
生成1.26kb文件和71行。
我稍微编辑了代码并将逻辑移动到另一个函数中,该函数在main()中被初始化:
#include <stdio.h>
void foo(void) {
int fahr;
float calc;
for (fahr = 300; fahr >= 0; fahr = fahr - 20) {
calc = (5.0 / 9.0) * (fahr - 32);
printf("%3d %6.1f\n", fahr, calc);
}
}
int main(void) {
foo();
return 0;
}
这将产生2.33kb的汇编代码,包含128行。
使用time ./main
运行这两个程序我发现执行速度没有区别。
我的问题是,尝试按汇编代码的长度优化C程序是否重要?
答案 0 :(得分:12)
您似乎正在比较GCC生成的.S
文件的大小,因为这显然毫无意义,我只是假装您遇到的二进制大小为2,GCC生成的代码片段。
虽然在所有其他条件相同的情况下,较短的代码大小可能会提高速度(由于更高的代码密度),但通常x86 CPU足够复杂,需要在代码大小优化和优化之间进行解耦代码速度。
具体来说,如果你瞄准代码速度,你应该优化代码速度。有时这需要选择最短的片段,有时则不需要。
考虑编译器优化的经典示例,乘以2的幂:
int i = 4;
i = i * 8;
这可能被严重翻译为:
;NO optimizations at all
mov eax, 4 ;i = 4 B804000000 0-1 clocks
imul eax, 8 ;i = i * 8 6BC009 3 clocks
;eax = i 8 bytes total 3-4 clocks total
;Slightly optimized
;4*8 gives no sign issue, we can use shl
mov eax, 4 ;i = 4 B804000000 0-1 clocks
shl eax, 3 ;i = i * 8 C1E003 1 clock
;eax = i 8 bytes total 1-2 clocks total
两个片段具有相同的代码长度,但第二个片段的速度几乎是两倍。
这是一个非常基本的例子 1 ,其中甚至没有太多需要考虑微架构。
另一个更微妙的例子如下,取自Agner Fog对部分寄存器档位的讨论 2 :
;Version A Version B
mov al, byte ptr [mem8] movzx ebx, byte ptr [mem8]
mov ebx, eax and eax, 0ffffff00h
or ebx, eax
;7 bytes 14 bytes
两个版本都给出相同的结果,但版本B 比版本A 快5-6个时钟,尽管前者是后者的两倍。
答案是不,代码大小不够;它可能是一个打破平局。
如果您真的对优化装配感兴趣,您将享受这两个读数:
第一个链接还有一本优化C和C ++代码的手册。
如果用C语言编写,请记住影响最大的优化是1)数据的表示/存储方式,即数据结构2)数据的处理方式,即算法。
有宏优化。
考虑到生成的程序集正在转向微优化,最有用的工具是1)智能编译器2)一组很好的内在函数 3 。
1 在实践中如此简单地进行优化 2 现在可能有点过时,但它有助于达到目的 3 内置的非标准功能,可转换为特定的装配说明。
答案 1 :(得分:3)
与往常一样,答案是&#34;它取决于&#34;。有时使代码更长可以提高效率:例如,CPU不必在每次循环后浪费额外的指令。一个经典的例子(字面意思是&#39;经典&#39;:1983!)是"Duff's Device"。以下代码
f
使用这个更多更大,更复杂的代码,更快
:register short *to, *from;
register count;
{
do { /* count > 0 assumed */
*to = *from++;
} while(--count > 0);
}
但这可以走极端:使代码过大会增加缓存未命中和各种其他问题。简而言之:&#34;过早的优化是邪恶的&#34; - 在决定这是一个好主意之前,你需要在之前和之后,经常在多个平台上测试你。
我会问你:上面代码的第二个版本&#34;更好&#34;比第一个版本?它的可读性较差,维护性较差,并且比它所替代的代码复杂得多。
答案 2 :(得分:2)
在内联之后,实际运行的代码在两种情况下都是相同的。第二种方式更大,因为它还必须发出函数的独立定义,而不是内联到main
。
如果你在函数上使用static
,你就可以避免这种情况,因此编译器会知道没有任何东西可以从编译单元外部调用它,因此一个独立的定义不是&#39 ;如果它被内联到其唯一的来电者中,则需要。
此外,编译器输出中的大多数.s
行是注释或汇编程序指令,而不是指令。因此,您甚至不计算说明。
Godbolt编译器浏览器是查看编译器asm输出的好方法,只需要指令和实际使用的标签。看看your code there。
如果存在循环或分支,则计算可执行文件中的指令总数完全是假的。或者特别是在循环内调用函数,就像在这种情况下一样动态指令计数(实际运行了多少指令,即每次循环计数等等)与性能非常粗略相关,但某些代码每周期运行4条指令,有些运行远低于1(例如大量的div或sqrt,缓存未命中和/或分支错误预测)。
要详细了解代码运行缓慢或快速的原因,请参阅x86代码wiki,尤其是Agner Fog's stuff。
我最近还写了Deoptimizing a program for the pipeline in Intel Sandybridge-family CPUs的答案。考虑恶魔般地使程序运行得慢的方法是一种有趣的练习。