代码为 "hand optimized" 是什么意思?

What does it mean for code to be "hand optimized"?

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

我经常听到这个词,但我仍然不太确定我知道它的意思。 手动优化代码是什么意思?我在网上搜索过,但没能找到它的正式定义,无论是 stackexchange 还是其他。

对于某些上下文,以 Wikipedia article on Program Optimization 中的这段摘录为例:

At the lowest level, writing code using an assembly language, designed for a particular hardware platform can produce the most efficient and compact code if the programmer takes advantage of the full repertoire of machine instructions. Many operating systems used on embedded systems have been traditionally written in assembler code for this reason. Programs (other than very small programs) are seldom written from start to finish in assembly due to the time and cost involved. Most are compiled down from a high level language to assembly and hand optimized from there. When efficiency and size are less important large parts may be written in a high-level language.

根据上下文,我推测它的意思是 "editing machine code by hand to optimize an algorithm" 或类似的意思。但我仍然很困惑,因为我听说过这个术语在非汇编语言(如 C++ 和 Java 的上下文中使用。

编译器通常采用高级语言,如 C、C++、Java 等,并将其编译成类似的东西,将那些列出的汇编语言,然后在幕后他们通常会为你调用汇编程序也可能是链接器,这样您所看到的都是高级别的,并且输出为对象或最终二进制文件。 运行 gcc 与 -save-temps 以查看 gcc 在生成对象或二进制文件的过程中产生的各种程序之间采取的一些可见步骤。

由人类编写的编译器不会厌倦,而且通常很好,但并不完美。没有什么是完美的,因为我的计算机可能比您的计算机具有更快的内存和更慢的处理器,因此来自相同源代码的完美优化的某些定义可能需要与您的计算机不同的编译器输出。所以即使同一个目标说 x86 linux 机器并不意味着有一个完美的二进制文件。同时,编译器不会厌倦给它一个大文件或设计一个复杂的算法,甚至是一个简单的算法,它会生成被组装的程序集等等。

这是手牌优化的用武之地,基本上您已经引用了问题的答案。没有理由弄乱机器代码,您可以通过编译器生成的汇编语言或编译器可以生成它的各种方式之一获取汇编语言并将其留给您(或者通过重命名汇编程序并将您自己的程序放入其中来窃取它,编译器会认为它是工具链的一部分而产生它,然后您可以在那里获取文件)。然后,作为一个拥有或认为自己拥有出色技能的人,不必完成为该任务创建代码的全部工作,而是可以检查编译器输出,发现遗漏的优化,或为他们的系统调整代码,无论是什么原因,无论他们选择 "better" 的什么定义。

我有一次在另一个问题中很幸运,但采取这种典型的优化。

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 的除法。为什么更容易找到具有乘法而不是除法的处理器,乘法比除法需要更少的逻辑,稳定速度更快,而许多处理器会声称 "one clock cycle" 这就像每分钟都有一辆汽车从这个因素的一侧驶来,这并不意味着制造一辆汽车需要一分钟。

但是乘法和有时对常量的移位是 not-atypical 用于在编译时已知除数的除法。在这种情况下,除法是立即移动和除法,可能完成,两条指令没有额外的内存周期。因此,如果分频和移动采用的时钟应该比负载快得多,那么在这种情况下,微控制器的闪存通常至少是 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 以移动它们的对齐方式。现在这很乏味,因为您对项目的任何代码都可以更改对齐方式,您必须 re-tune 和/或此调整 can/will 会导致地址 space 中进一步向下的任何其他调整移动导致他们需要重新调整。但是,这只是一个例子,因为拥有一些可能被认为很重要的知识,导致手动弄乱编译器输出。会有更简单的方法来调整像这样的一些循环,而不需要每次更改工具链或代码时都必须 re-touch 的痛苦。

Most are compiled down from a high level language to assembly and hand optimized from there.

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

编辑,好的,这是一个值得思考的问题...

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)

所以如果我 trim 它到

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 位无符号乘法iply 足以完成 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);
}

测试通过。