函数与过程

  • 80X86上的函数/过程调用.
    • call指令来调用过程;ret指令(return)返回调用程序.过程如下:
    • 1)确定执行完过程后要返回的指令地址(返回/链接地址).
    • 2)将该地址保存到已知位置.
      • 在没有递归时,可将其放在任意位置.
      • 放到内存中的栈是最常见的,执行过程:call->push/ret->pop
      • 优点:
        • 1)栈”后进先出”,完全支持嵌套和递归.
        • 2)栈在内存中可高效操作.不同过程的返回地址可重用同一内存空间.
        • 3)频繁访问内存的栈时可以使用缓存.
        • 4)栈也是保持活动记录的场所.
    • 3)通过跳转机制将控制转移到过程的第一条指令.
    • 4)CPU开始执行,直到遇到ret指令为止.
    • 5)ret取出返回地址后,将控制转移到该地址.
  • PowerPC上的bl(branch then link)指令.
    • 将控制转移到其操作数指令的目标地址;并将bl后第一条指令的地址拷贝到链接寄存器.进入过程后,如果没有代码修改链接寄存器的值,过程就通过blr(branch to LINK register)指令返回调用处;若函数要将链接寄存器挪作它用,就要负责将返回地址保存,并在函数调用结束处(即将通过blr指令返回前)恢复链接寄存器的值.
    • 两者的比较.
      • call/ret指令是复杂指令(一条指令完成多个独立任务).其额外开销与维护栈的开销需要评定.bl通过link地址的间接跳转指令,没有维护软件栈的开销,相对较快.
      • blz指令必须包含操作码和函数的偏移量.如果函数太远,超出了预留的偏移量位数范围,就只能生成一串指令来计算目标例程的地址,以便控制间接转移到哪里.在大程序时会这样.还有编译器不知道目标函数(外部函数)地址的时候只能使用最长形式的函数调用.
  • 叶函数/过程.
    • 在展平调用树时切勿牺牲可读性和可维护性.只调用其他函数,其他什么事情都不干的函数/过程最好不要写.应使调用树尽可能的浅且更多的叶过程.
    • 叶函数一般不会修改链接寄存器,所以无需额外代码保存链接寄存器的值.
  • 宏和内联函数
    • 调用函数的一个特性:开销总是固定的,不管函数体有一条指令,还是一千条.其设置参数和创建,销毁活动记录的指令数都是一样的.所以我们应该尽量放置较大的过程或函数,而将短序列作为内联代码使用.
    • 一方面要编程结构模块化,另一方面,过度频繁地调用某过程将要求过大的开销.平衡两者不容易.
    • 纯粹的宏在调用过程/函数的地方将后者展开为过程/函数体.由于没有调用和返回代码,宏的展开避免了与call/ret指令相关的开销.宏使用参数文本替换,而非将参数压入栈或传送到寄存器.不足是如果宏本身很大,又在不同的地方调用,那么可执行程序的体积将会膨胀.其是时间与空间的折中.
    • 内联:支持内联的语言并不保证会将代码内联展开.这由编译器做主:如果函数体太大,或参数过多,不会展开;即使将其展开,仍会存在一些纯宏代码没有的开销(要创建活动记录来处理局部变量,临时需要).
  • 传递参数
    • 传递的参数数据越多,调用函数的开销越大.若干的可选参数带来通用性的好处,但是也会增大开销.
    • 通过值传递大型数据,编译器就得生成相关的机器代码,将该数据的值复制到过程的活动记录中.这相当耗时.另外,CPU寄存器组可能放不下大型数据,造成更大的开销.建议通过引用方式来传递大值.
    • 传递小值数据时,通过值/引用的效率高低取决于所用的CPU和编译器.
    • 存放数据的全局变量可供函数调用,调用该函数时就不需要额外指令来传递数据.于是减少了调用开销.但是过多使用全局变量使得编译器很难优化程序.使用全局变量可减少调用函数的开销,但同时阻止了各种可能的优化.
  • 参数传递机制
    • 传递值:调用过程的代码对参数数据生成一份拷贝,将此拷贝传递给过程.优势是CPU只需将参数视为活动记录里的局部变量.
    • 传递引用:好处:1)参数总是占据固定数目的内存(指针变量的尺寸),而与原参数的尺寸无关.2)能够修改实参的值.缺点:访问引用参数的开销大,因为子过程需要在每次访问数据前解析其地址(为了使用寄存器间接寻址模式解析指针,就得将此指针调入寄存器).
原文地址:https://www.cnblogs.com/robyn/p/3779604.html