【问题标题】:What does it mean for code to be "hand optimized"?“手动优化”代码意味着什么?
【发布时间】:2018-04-04 00:53:04
【问题描述】:

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

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

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

在最低级别,使用汇编语言编写代码,设计 对于特定的硬件平台可以产生最有效和 紧凑的代码,如果程序员利用完整的曲目 的机器指令。嵌入式上使用的许多操作系统 系统传统上是用汇编代码编写的 原因。很少写程序(非常小的程序除外) 由于所涉及的时间和成本,从开始到结束组装。 大多数都是从高级语言编译成汇编和手工 从那里优化。当效率和规模不那么重要时 大部分内容可以用高级语言编写。

根据上下文,我推测它的意思是“手动编辑机器代码以优化算法”或类似的东西。但是我仍然很困惑,因为我听说这个术语也用于 C++ 和 Java 等非汇编语言的上下文中。

【问题讨论】:

  • 这真是一个口语化的名词。它指的是使用此代码和一些度量工具并对其进行重构以获得针对度量的最佳结果(例如内存占用、分配数量、执行时间、对 I/O 的查询数量等)的人。这与静态分析器/linter 不同,静态分析器/linter 可能建议进行实际上对性能有害的重构。
  • 这意味着汇编代码然后由一个人分析和修改以提高其性能。将其视为经过洗车然后仔细手工抛光以确保其最佳外观的汽车(而不是仅仅依靠洗车的机器来提供它)。
  • 哇,谢谢你的故事。梅尔是传奇!哈哈,我认为 Mel 向我展示了手动优化的真正含义。

标签: assembly optimization semantics


【解决方案1】:

编译器通常采用高级语言(如 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);
}

测试通过。

【讨论】:

    猜你喜欢
    • 2021-11-04
    • 1970-01-01
    • 2018-04-25
    • 2017-12-31
    • 2013-01-11
    • 1970-01-01
    • 1970-01-01
    • 2023-03-10
    • 1970-01-01
    相关资源
    最近更新 更多