代码“手动优化”意味着什么?

时间:2018-04-04 00:53:05

标签: assembly optimization semantics

这可能是一个真正的noobie问题,我甚至不确定这是否是正确的论坛,但请耐心等待我,如果不是,请给我一个正确方向的推动。

我总是听到这个词被抛出,我仍然不确定我知道这意味着什么。对于手动优化的代码意味着什么?我在网上搜索过,我无法找到它的正式定义,stackexchange或其他。

对于某些上下文,请摘自Wikipedia article on Program Optimization中的摘录:

  

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

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

1 个答案:

答案 0 :(得分:2)

编译器通常使用高级语言,如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.为什么更有可能找到具有乘法而不是除法的处理器,乘法所需的逻辑比除数更少,结算更快,而许多处理器会声称&#34;一个时钟周期&#34;这就像一辆汽车每分钟都在这个因素的一侧,这并不意味着需要一分钟来制造一辆汽车。

但是对于具有在编译时已知的除数的除法,对常数的乘法和有时的移位不是非典型的。在这种情况下的分歧将是立即移动和分割并且可能完成,两个指令没有额外的存储周期。因此,如果除法和移动需要一个应该比负载快得多的时钟,那么在这种情况下,微控制器的闪存通常至少是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字对齐(和大小)的提取。这些循环确实想要向下移动,因此每个循环只需要一次获取而不是两次。所以我可以获取程序集输出并在函数开头的某处添加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)

所以,如果我修剪它

unsigned short fun ( unsigned short x )
{
    return(x/5);
}

我们希望看到(x * 0xCCCD)&gt;&gt; 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);
}

测试通过。

相关问题