编译器通常采用高级语言(如 C、C++、Java 等)并将其编译成类似的东西,将其列在汇编语言中,然后在幕后他们通常为您调用汇编器,可能还会调用链接器,以便您所看到的只是高级别的,对象或最终二进制文件作为输出。使用 -save-temps 运行 gcc 以查看 gcc 在生成对象或二进制文件的过程中生成的各种程序之间采取的一些可见步骤。
由人类编写的编译器不会感到疲倦,而且通常很好,但并不完美。没有什么是完美的,因为我的计算机可能比您的计算机具有更快的内存和更慢的处理器,因此来自相同源代码的完美优化的某些定义可能需要与您的计算机不同的编译器输出。所以即使同一个目标说一台 x86 linux 机器并不意味着有一个完美的二进制文件。同时,编译器不会厌倦给它一个大文件或项目一个复杂的算法,甚至是一个简单的算法,它会产生组装的程序集等等。
这就是手部优化的用武之地,基本上您已经引用了问题的答案。没有理由弄乱机器代码,您可以通过编译器生成的各种方式或其中一种方式获取编译器生成的汇编语言并将其留给您(或通过重命名汇编器并将您自己的程序放入其中来窃取它) ,编译器产生它认为它是工具链的一部分,你在那里抓取文件)。然后,作为一个拥有或认为自己拥有出色技能的人,不必为该任务完成创建代码的全部工作,但可以检查编译器输出,找到遗漏的优化,或为他们的系统调整代码,不管是什么原因,无论他们选择什么“更好”的定义。
有一次我在另一个问题上很幸运,但请采用这种典型的优化。
unsigned int fun ( unsigned int a )
{
return(a/5);
}
00000000 <fun>:
0: 4b02 ldr r3, [pc, #8] ; (c <fun+0xc>)
2: fba3 3000 umull r3, r0, r3, r0
6: 0880 lsrs r0, r0, #2
8: 4770 bx lr
a: bf00 nop
c: cccccccd
它执行的是 1/5 的乘法,而不是 5 的除法。为什么更容易找到具有乘法而不是除法的处理器,乘法比除法占用的逻辑更少,稳定速度更快,而许多处理器会声称“一个时钟周期”就像每分钟一辆汽车从侧面驶来,但这并不意味着制造一辆汽车需要一分钟。
但是对于在编译时已知除数的除法,乘法和有时对常数的移位并不是非典型的。在这种情况下,除法将是立即移动和可能完成的除法,两条指令没有额外的内存周期。因此,如果分频和移动采用的时钟应该比负载快得多,那么在这种情况下,如果不是更多的等待状态,取决于设置,微控制器的闪存通常至少是 CPU 时钟频率的一半,编译器不知道的东西。那个负载可能是一个杀手,额外的指令获取可能是一个杀手,我可能碰巧知道这一点。同时,在这种情况下,IP 供应商可能有一个内核,芯片供应商可以选择在两个或更多时钟中编译乘法,以显着节省芯片空间,但代价是该类型的一点性能。手术。如果编译器无论如何都能够分析这种事情,那么它可能没有设置来指示这一点。这不是您需要手动优化的代码,但您可能会在更大的函数输出中看到这些行并选择进行试验。
另一个可能是几个循环:
void dummy ( unsigned int );
void fun ( unsigned int a, unsigned int b, unsigned int c )
{
unsigned int ra;
for(ra=0;ra<a;ra++) dummy(ra);
for(ra=0;ra<b;ra++) dummy(ra);
}
00000000 <fun>:
0: e92d4070 push {r4, r5, r6, lr}
4: e2506000 subs r6, r0, #0
8: e1a05001 mov r5, r1
c: 0a000005 beq 28 <fun+0x28>
10: e3a04000 mov r4, #0
14: e1a00004 mov r0, r4
18: e2844001 add r4, r4, #1
1c: ebfffffe bl 0 <dummy>
20: e1560004 cmp r6, r4
24: 1afffffa bne 14 <fun+0x14>
28: e3550000 cmp r5, #0
2c: 0a000005 beq 48 <fun+0x48>
30: e3a04000 mov r4, #0
34: e1a00004 mov r0, r4
38: e2844001 add r4, r4, #1
3c: ebfffffe bl 0 <dummy>
40: e1550004 cmp r5, r4
44: 1afffffa bne 34 <fun+0x34>
48: e8bd4070 pop {r4, r5, r6, lr}
4c: e12fff1e bx lr
那是链接的输出,我碰巧知道这个核心有一个 8 字对齐(和大小)的提取。这些循环真的想向下移动,所以每个循环只需要一次提取而不是两次。因此,我可以获取程序集输出并在循环之前的函数开头的某处添加 nop 以移动它们的对齐方式。现在这很乏味,因为您对项目的任何代码都可以更改对齐方式,您必须重新调整,或者此调整可能/将导致地址空间中进一步向下移动的任何其他调整,导致它们需要重新调整.但这只是一个例子,说明了一些可能被认为很重要的知识,导致手动弄乱编译器输出。会有更简单的方法来调整这样的一些循环,而无需每次更改工具链或代码时都必须重新修改。
大部分都是从高级语言编译成汇编和手工
从那里优化。
答案就在您的问题中,该引文的其余部分设置了一种情况,即不鼓励作者使用汇编语言编写整个项目和/或函数,而是让编译器完成繁重的工作而由人工完成一些他们认为很重要或出于某种原因需要的手部优化。
编辑,好吧,这是一个值得思考的问题......
unsigned int fun ( unsigned int x )
{
return(x/5);
}
armv7-m
00000000 <fun>:
0: 4b02 ldr r3, [pc, #8] ; (c <fun+0xc>)
2: fba3 3000 umull r3, r0, r3, r0
6: 0880 lsrs r0, r0, #2
8: 4770 bx lr
a: bf00 nop
c: cccccccd stclgt 12, cr12, [r12], {205} ; 0xcd
armv6-m (all thumb variants have mul not umull but mul)
00000000 <fun>:
0: b510 push {r4, lr}
2: 2105 movs r1, #5
4: f7ff fffe bl 0 <__aeabi_uidiv>
8: bc10 pop {r4}
a: bc02 pop {r1}
c: 4708 bx r1
e: 46c0 nop ; (mov r8, r8)
所以如果我把它修剪成
unsigned short fun ( unsigned short x )
{
return(x/5);
}
我们希望看到 (x*0xCCCD)>>18 对吗?不,还有更多代码。
00000000 <fun>:
0: b510 push {r4, lr}
2: 2105 movs r1, #5
4: f7ff fffe bl 0 <__aeabi_uidiv>
8: 0400 lsls r0, r0, #16
a: 0c00 lsrs r0, r0, #16
c: bc10 pop {r4}
e: bc02 pop {r1}
10: 4708 bx r1
12: 46c0 nop ; (mov r8, r8)
如果 32*32 = 64 位无符号乘法足以做 1/5 次的事情并且编译器知道这一点,那么它为什么不知道它拥有或可以屏蔽的 16*16 = 32 位未优化.
unsigned short fun ( unsigned short x )
{
return((x&0xFFFF)/(5&0xFFFF));
}
所以接下来我要做的就是做一个实验,以确认我没有弄乱我对数学的理解,(在这种情况下,将每个组合与每个组合与一个内置除法器与乘法器乘以 1/ 5 件事,看到它匹配)。如果通过了,则手动优化代码以避免库调用。 (实际上我现在在一些代码中这样做,因此意识到应该在 armv6-m 上进行匹配优化)
#include <stdio.h>
int main ( void )
{
unsigned int ra,rb,rc,rd;
for(ra=0;ra<0x10000;ra++)
{
rb=ra/5;
rc=(ra*0xCCCD)>>18;
if(rb!=rc)
{
printf("0x%08X 0x%08X 0x%08X\n",ra,rb,rc);
}
}
printf("done\n");
return(0);
}
测试通过。