代码“手动优化”是什么意思?

Sky*_*edy 4 optimization assembly semantics

这可能是一个非常菜鸟的问题,我什至不确定这是否是提问的正确论坛,但请耐心等待,如果不是,请给我一个正确的方向。

我一直听到这个词到处乱说,但我仍然不太确定我知道它的意思。手动优化代码意味着什么?我在网上搜索过,但找不到它的正式定义,stackexchange 或其他。

对于某些上下文,例如摘自维基百科关于程序优化的文章

在最低层次上,如果程序员充分利用机器指令的全部功能,使用专为特定硬件平台设计的汇编语言编写代码可以生成最高效、最紧凑的代码。出于这个原因,嵌入式系统上使用的许多操作系统传统上都是用汇编代码编写的。由于涉及时间和成本,程序(除了非常小的程序)很少在汇编中从头到尾编写。大多数都是从高级语言编译为汇编并从那里进行手工优化。当效率和尺寸不太重要时,大型部件可能会用高级语言编写。

根据上下文,我假设这意味着“手动编辑机器代码以优化算法”或类似的东西。但是我仍然很困惑,因为我听说过在非汇编语言(例如 C++ 和 Java)的上下文中也使用了这个术语。

old*_*mer 5

编译器通常采用 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    
Run Code Online (Sandbox Code Playgroud)

它是乘以 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
Run Code Online (Sandbox Code Playgroud)

那是链接的输出,我碰巧知道这个核心有一个 8 字对齐(和大小)的提取。这些循环真的想向下移动,所以每个循环只需要一次而不是两次。所以我可以获取汇编输出并在循环之前的函数开头的某处添加 nops 以移动它们的对齐方式。现在这很乏味,因为您对项目的任何代码都可以更改对齐方式,并且您必须重新调整和/或此调整可能/将导致地址空间中的任何其他调整进一步向下移动,从而导致它们需要重新调整. 但这只是一个拥有一些可能被认为很重要的知识的例子,导致手动弄乱编译器输出。将有更简单的方法来调整这样的一些循环,而无需每次更改工具链或代码时都需要重新触摸。

大多数都是从高级语言编译为汇编并从那里进行手工优化。

答案就在您的问题中,该引用的其余部分设置了一种情况,即不鼓励作者用汇编语言编写整个项目和/或函数,而是让编译器完成繁重的工作,而人工进行一些手动优化出于某种原因,他们认为这很重要或需要。

编辑,好的,这里是一个思考......

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)
Run Code Online (Sandbox Code Playgroud)

所以如果我把它修剪成

unsigned short fun ( unsigned short x )
{
    return(x/5);
}
Run Code Online (Sandbox Code Playgroud)

我们希望看到 (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)
Run Code Online (Sandbox Code Playgroud)

如果 32*32 = 64 位无符号乘法足以做 1/5 倍的事情并且编译器知道这一点,那么为什么它不知道它具有或可以掩码为未优化的 16*16 = 32 位。

unsigned short fun ( unsigned short x )
{
    return((x&0xFFFF)/(5&0xFFFF));
}
Run Code Online (Sandbox Code Playgroud)

所以接下来我要做的就是做一个实验来确认我没有搞砸我对数学的理解,(在这种情况下,针对具有内置除法的机器尝试每种组合对每种组合与乘以 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);
}
Run Code Online (Sandbox Code Playgroud)

通过测试。