assembly - 对代码进行“手动优化”意味着什么?

标签 assembly optimization semantics

这可能是一个非常棘手的问题,我什至不确定这是否是提出此问题的合适论坛,但请您忍受,如果不是,请向我提出建议。

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

在某些情况下,请以Wikipedia article on Program Optimization的摘录为例:


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


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

最佳答案

编译器通常采用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字对齐(且大小合适)的访存。这些循环确实想向下移动,因此每个循环只需要一次获取,而不是两个。因此,我可以获取程序集输出,并在循环开始移动其对齐方式之前,在函数开头的某处添加点。现在这很麻烦,因为您对项目的任何代码都可以更改对齐方式,因此您必须重新调整,或者此调整可能/将导致地址空间中进一步向下移动的任何其他调整,导致需要重新调整它们。 。但是,仅是拥有一些可能被认为很重要的知识的示例,这会导致手工弄乱编译器的输出。会有更简便的方法来调整这样的循环,而不必每次更改工具链或代码时都需要重新触摸。


大多数都是从高级语言编译为汇编和手工编写的
从那里优化。


答案就在您的问题中,其余引号是在这样的情况下,作者不愿使用汇编语言编写整个项目和/或功能,而是让编译器进行繁琐的工作,而人工进行一些手动优化他们认为由于某种原因很重要或需要。

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

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);
}


考试通过了。

关于assembly - 对代码进行“手动优化”意味着什么?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/49641105/

相关文章:

generics - Ada 泛型和汇编代码生成

c++ - 如何达到每个周期 4 FLOP 的理论最大值?

有人可以为上面的c程序编写汇编代码并将其转换为小于100字节的机器代码吗?

windows - 在 DLL 中使用 cpu 特定功能有哪些标准技术?

c - 编写蹦床函数

javascript - 什么是记忆化?

rust - 这个 for 循环模式有没有名字,如果有,有没有更好的写法?

go - 删除转换更改语义

c - 什么是值语义和引用语义及其区别

mysql - 没有适当索引的长时间运行的查询