函数与过程:参数传递、返回值及效率优化
1. 80x86 代码特点
80x86 架构在传递参数和存储局部变量时,可用的寄存器数量有限,因此需要在激活记录中分配更多的局部变量。同时,80x86 为 EBP 寄存器提供的偏移范围仅为 -128 到 +127 字节,所以大量指令需要使用 4 字节偏移,而非 1 字节偏移。不过,80x86 允许在访问内存的指令中编码完整的 32 位地址,这样在访问距离 EBP 指向位置较远的变量时,无需执行多条指令。
以下是一段 80x86 代码示例:
andl $-16, %esp
; d[0] (eax) = argc (ecx) + a (eax);
leal (%eax,%ecx), %eax
; Make room for printf parameters:
subl $8, %esp
movl %eax, -67093516(%ebp)
; e = a + c
leal (%ebx,%ecx), %eax
pushl %eax ; e
pushl %edx ; d[0]
pushl %ebx ; c
pushl %edx ; b[0]
pushl %ecx ; a
pushl $.LC0
movl %edx, -1032(%ebp)
movl %edx, -67109896(%ebp)
call printf
xorl %eax, %eax
movl -4(%ebp), %ebx
leave
ret
2. 参数传递机制
大多数高级语言提供了至少两种将实际参数数据传递给子程序的机制:值传递(pass-by-value)和引用传递(pass-by-reference)。
2.1 值传递
值传递是最容易理解的参数传递机制。调用过程的代码会复制参数的数据,并将该副本传递给过程。对于小值,通过值传递参数通常只需要一条 push 指令(或者在使用寄存器传递参数时,将值移动到寄存器的指令),因此值传递通常非常高效。
值传递参数的一个重要优点是,CPU 将它们视为激活记录中的局部变量。由于传递给过程的参数数据很少超过 120 字节,支持索引寻址模式下短位移的 CPU 能够使用更短(因而更高效)的指令来访问大多数参数值。
然而,当需要传递大型数据结构(如数组或记录)时,值传递可能会变得低效。调用代码需要将实际参数逐字节复制到过程的激活记录中,这可能是一个非常缓慢的过程。例如,将一个包含一百万个元素的数组按值传递给子程序,会消耗大量时间。因此,除非绝对必要,否则应避免按值传递大型对象。
2.2 引用传递
引用传递机制传递的是对象的地址,而非其值。与值传递相比,它有两个明显的优点:
- 无论参数大小如何,引用传递参数始终占用相同的内存量,即指针的大小(通常为双字)。
- 引用传递参数允许修改实际参数的值,这是值传递参数无法做到的。
不过,引用传递参数也有其缺点。通常,在过程中访问引用参数比访问值参数更昂贵。这是因为子程序每次访问对象时都需要解引用该地址,这通常涉及将指针加载到寄存器中,以便使用寄存器间接寻址模式解引用指针。
以下是一个 Pascal 代码示例及其等效的 HLA/x86 汇编代码:
procedure RefValue
(
var dest:integer;
var passedByRef:integer;
passedByValue:integer
);
begin
dest := passedByRef + passedByValue;
end;
procedure RefValue
(
var dest:int32;
var passedByRef:int32;
passedByValue:int32
); @noframe;
begin RefValue;
// Standard Entry Sequence (needed because of @noframe).
// Set up base pointer.
// Note: don't need SUB(nn,esp) because
// we don't have any local variables.
push( ebp );
mov( esp, ebp );
// Get pointer to actual value.
mov( passedByRef, edx );
// Fetch value pointed at by passedByRef
mov( [edx], eax );
// Add in the value parameter.
add( passedByValue, eax );
// Get address of destination reference parameter.
mov( dest, edx );
// Store sum away into dest.
mov( eax, [edx] );
// Exit sequence doesn't need to deallocate any local
// variables because there are none.
pop( ebp );
ret( 12 );
end RefValue;
仔细观察这段代码可以发现,与使用值传递的版本相比,它多了两条指令,即加载 dest 和 passedByRef 地址到 EDX 寄存器的指令。一般来说,访问值传递参数的值只需要一条指令,而引用传递参数则需要两条指令(一条用于获取地址,一条用于操作该地址的数据)。因此,除非需要引用传递的语义,否则应尽量使用值传递。
当 CPU 有大量可用寄存器来维护指针值时,引用传递的问题会有所减轻。在这种情况下,CPU 可以使用一条指令通过寄存器中维护的指针来获取或存储值。
3. 函数返回值
大多数高级语言在一个或多个 CPU 寄存器中返回函数结果。编译器具体使用哪个寄存器,取决于数据类型、CPU 和编译器。不过,大多数情况下,函数会在寄存器中返回结果。
不同 CPU 架构和数据类型的返回寄存器如下表所示:
| CPU 架构 | 数据类型 | 返回寄存器 |
| ---- | ---- | ---- |
| 80x86 | 整数(ordinal) | AL、AX 或 EAX |
| 80x86 | 64 位整数(long long int) | EDX:EAX(EDX 包含高双字) |
| 80x86(64 位变体) | 64 位整数 | RAX |
| PowerPC | 8、16 和 32 位值 | R3 |
| PowerPC(32 位版本) | 64 位整数 | R4:R3(R4 包含高字) |
| PowerPC(64 位变体,推测) | 64 位整数 | R3 |
对于浮点结果,编译器通常在 CPU(或 FPU)的浮点寄存器中返回。例如:
- 80x86(32 位变体):大多数编译器在 80 位 ST0 浮点寄存器中返回浮点结果。
- 80x86(64 位版本,如 Windows64):通常使用 SSE 寄存器(XMM0)返回浮点值。
- PowerPC 系统:一般在 F1 浮点寄存器中返回浮点函数结果。
有些语言允许函数返回非标量(聚合)值。编译器返回大型函数返回结果的具体机制因编译器而异。一种常见的解决方案是将存储返回结果的存储地址传递给函数。
以下是一个 C++ 程序示例,其中 func 函数返回一个结构体对象:
#include <stdio.h>
typedef struct
{
int a;
char b;
short c;
char d;
} s_t;
s_t func( void )
{
s_t s;
s.a = 0;
s.b = 1;
s.c = 2;
s.d = 3;
return s;
}
int main( void )
{
s_t t;
t = func();
printf( "%d", t.a, func().a );
return( 0 );
}
以下是 GCC 为该 C++ 程序生成的 PowerPC 代码:
.text
.align 2
.globl _func
; func() -- Note: upon entry, this
; code assumes that R3
; points at the storage
; to hold the return result.
_func:
li r0,1
li r9,2
stb r0,-28(r1) ; s.b = 1
li r0,3
stb r0,-24(r1) ; s.d = 3
sth r9,-26(r1) ; s.c = 2
li r9,0 ; s.a = 0
; Okay, set up the return
; result.
lwz r0,-24(r1) ; r0 = d::c
stw r9,0(r3) ; result.a = s.a
stw r0,8(r3) ; result.d/c = s.d/c
lwz r9,-28(r1)
stw r9,4(r3) ; result.b = s.b
blr
.data
.cstring
.align 2
LC0:
.ascii "%d\0"
.text
.align 2
.globl _main
_main:
mflr r0
stw r31,-4(r1)
stw r0,8(r1)
bcl 20,31,L1$pb
L1$pb:
; Allocate storage for t and
; temporary storage for second
; call to func:
stwu r1,-112(r1)
; Restore LINK from above:
mflr r31
; Get pointer to destination
; storage (t) into R3 and call func:
addi r3,r1,64
bl _func
; Compute "func().a"
addi r3,r1,80
bl _func
; Get t.a and func().a values
; and print them:
lwz r4,64(r1)
lwz r5,80(r1)
addis r3,r31,ha16(LC0-L1$pb)
la r3,lo16(LC0-L1$pb)(r3)
bl L_printf$stub
lwz r0,120(r1)
addi r1,r1,112
li r3,0
mtlr r0
lwz r31,-4(r1)
blr
; stub for printf function:
.data
.picsymbol_stub
L_printf$stub:
.indirect_symbol _printf
mflr r0
bcl 20,31,L0$_printf
L0$_printf:
mflr r11
addis r11,r11,ha16(L_printf$lazy_ptr-L0$_printf)
mtlr r0
lwz r12,lo16(L_printf$lazy_ptr-L0$_printf)(r11)
mtctr r12
addi r11,r11,lo16(L_printf$lazy_ptr-L0$_printf)
bctr
.data
.lazy_symbol_pointer
L_printf$lazy_ptr:
.indirect_symbol _printf
.long dyld_stub_binding_helper
以下是 GCC 为同一函数生成的 80x86 代码:
.file "t.c"
.text
.p2align 2,,3
.globl func
.type func,@function
; On entry, assume that the address
; of the storage that will hold the
; function's return result is passed
; on the stack immediately above the
; return address.
func:
pushl %ebp
movl %esp, %ebp
subl $24, %esp ; Allocate storage for s.
movl 8(%ebp), %eax ; Get address of result
movb $1, -20(%ebp) ; s.b = 1
movw $2, -18(%ebp) ; s.c = 2
movb $3, -16(%ebp) ; s.d = 3
movl $0, (%eax) ; result.a = 0;
movl -20(%ebp), %edx ; Copy the rest of s
movl %edx, 4(%eax) ; to the storage for
movl -16(%ebp), %edx ; the return result.
movl %edx, 8(%eax)
leave
ret $4
.Lfe1:
.size func,.Lfe1-func
.section .rodata.str1.1,"aMS",@progbits,1
.LC0:
.string "%d"
.text
.p2align 2,,3
.globl main
.type main,@function
main:
pushl %ebp
movl %esp, %ebp
subl $40, %esp ; Allocate storage for
andl $-16, %esp ; t and temp result.
; Pass the address of t to func:
leal -24(%ebp), %eax
subl $12, %esp
pushl %eax
call func
; Pass the address of some temporary storage
; to func:
leal -40(%ebp), %eax
pushl %eax
call func
; Remove junk from stack:
popl %eax
popl %edx
; Call printf to print the two values:
pushl -40(%ebp)
pushl -24(%ebp)
pushl $.LC0
call printf
xorl %eax, %eax
leave
ret
从这些 80x86 和 PowerPC 示例中可以注意到,返回大型对象的函数在返回之前通常会复制函数结果数据。这种额外的复制可能会花费大量时间,特别是当返回结果很大时。因此,通常更好的解决方案是将指向目标存储的指针显式传递给返回大型结果的函数,让函数进行必要的复制操作,这样通常可以节省一些时间和代码。
以下是一个实现此策略的 C 代码示例及其 80x86 汇编代码:
#include <stdio.h>
typedef struct
{
int a;
char b;
short c;
char d;
} s_t;
void func( s_t *s )
{
s->a = 0;
s->b = 1;
s->c = 2;
s->d = 3;
return;
}
int main( void )
{
s_t s,t;
func( &s );
func( &t );
printf( "%d", s.a, t.a );
return( 0 );
}
.file "t.c"
.text
.p2align 2,,3
.globl func
.type func,@function
func:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %eax
movl $0, (%eax) ; s->a = 0
movb $1, 4(%eax) ; s->b = 1
movw $2, 6(%eax) ; s->c = 2
movb $3, 8(%eax) ; s->d = 3
leave
ret
.Lfe1:
.size func,.Lfe1-func
.section .rodata.str1.1,"aMS",@progbits,1
.LC0:
.string "%d"
.text
.p2align 2,,3
.globl main
.type main,@function
main:
; Build activation record and allocate
; storage for s and t:
pushl %ebp
movl %esp, %ebp
subl $40, %esp
andl $-16, %esp
subl $12, %esp
; Pass address of s to func and
; call func:
leal -24(%ebp), %eax
pushl %eax
call func
; Pass address of t to func and
; call func:
leal -40(%ebp), %eax
movl %eax, (%esp)
call func
; Remove junk from stack:
addl $12, %esp
; Print the results:
pushl -40(%ebp)
pushl -24(%ebp)
pushl $.LC0
call printf
xorl %eax, %eax
leave
ret
这种方法更高效,因为代码无需进行两次数据复制(一次复制到数据的本地副本,一次复制到最终目标变量)。
综上所述,在编写代码时,需要根据具体情况选择合适的参数传递机制和返回值处理方式,以提高代码的效率。同时,了解不同 CPU 架构的特点和编译器的行为,对于优化代码至关重要。
4. 其他参数传递机制
除了值传递和引用传递,还有其他一些参数传递机制,如 FORTRAN 和 HLA 支持的按值/结果传递(pass-by-value/result)、Ada 和 HLA 支持的按结果传递(pass-by-result)、HLA 和 Algol 支持的按名称传递(pass-by-name)等。但这些机制不太常见,若需要使用,可查阅编程语言设计相关书籍或 HLA 文档。
5. 80x86 与 PowerPC CPU 家族简要比较
Intel/AMD 的 80x86 和 IBM/Motorola 的 PowerPC 是当今个人计算机系统和游戏控制台中最流行的 CPU 家族。大多数软件应用程序都是为这两个 CPU 家族编写的,这也是本文使用 80x86 和 PowerPC 代码示例的主要原因。
此外,这两种处理器代表了当今常用的两种基本 CPU 设计:复杂指令集计算机(CISC)和精简指令集计算机(RISC)。如果了解如何为一种 CISC 处理器生成优质代码,那么在其他 CISC 处理器上也能做得不错,RISC 处理器也是如此。不过,正如示例所示,RISC 和 CISC CPU 之间存在一些根本差异,因此在编写可能在不同架构上运行的代码时,需要了解这两种基本架构的差异。
6. 软件编写的其他方面
高效的代码编写不仅仅关注效率,代码的可读性和可维护性同样重要。良好的编码风格、注释和代码布局等,能让代码更易于他人理解和维护。即使代码效率很高,但如果无法被他人读懂和维护,也不能称之为优秀的代码。
总结
本文详细介绍了函数与过程中的参数传递机制、函数返回值的处理方式,以及不同 CPU 架构的特点。通过具体的代码示例,分析了值传递和引用传递的优缺点,以及返回大型对象时的优化策略。同时,强调了代码效率、可读性和可维护性在软件开发中的重要性。在实际编程中,应根据具体需求选择合适的方法,以实现高效、易读和可维护的代码。
希望这些内容能帮助你更好地理解函数与过程的相关知识,在编写代码时做出更明智的决策。如果你有任何疑问或需要进一步的解释,请随时留言。
函数与过程:参数传递、返回值及效率优化
6. 代码优化建议
为了编写高效且易于维护的代码,我们可以根据前面的分析给出以下具体的优化建议:
-
参数传递方面
- 对于小型数据,优先使用值传递。因为小型数据按值传递通常只需一条简单指令,且 CPU 能高效访问,如传递单个整数或字符等。
- 对于大型数据结构,如数组、结构体等,尽量使用引用传递。避免了大量的数据复制,节省时间和内存。但要注意,引用传递在访问时可能需要额外指令进行解引用。
-
函数返回值方面
- 当返回标量值(如整数、浮点数)时,利用 CPU 寄存器返回结果,这是最直接高效的方式。不同 CPU 架构和数据类型对应不同的返回寄存器,要根据具体情况选择。
- 当返回大型非标量对象时,不要直接返回对象,而是将存储结果的地址传递给函数,让函数直接将结果存储到指定位置,避免额外的数据复制。
下面用一个 mermaid 流程图来展示参数传递和返回值的选择策略:
graph TD
A[参数传递选择] --> B{数据大小}
B -->|小型数据| C[值传递]
B -->|大型数据| D[引用传递]
E[返回值选择] --> F{返回值类型}
F -->|标量值| G[寄存器返回]
F -->|大型非标量对象| H[传递存储地址]
7. 实际应用案例分析
为了更好地理解上述优化策略的实际效果,我们来看一个具体的案例。假设我们要实现一个图像处理程序,需要对图像数据进行处理。图像数据通常是一个大型的二维数组,属于大型数据结构。
以下是一个简单的伪代码示例,展示了按值传递和引用传递在处理图像数据时的差异:
# 按值传递示例
def process_image_value(image):
# 模拟图像处理操作
new_image = [[pixel * 2 for pixel in row] for row in image]
return new_image
# 引用传递示例
def process_image_reference(image):
# 直接在原图像上进行处理
for row in image:
for i in range(len(row)):
row[i] = row[i] * 2
return image
# 生成一个大型图像数据
image = [[i + j for j in range(1000)] for i in range(1000)]
# 按值传递处理
import time
start_time = time.time()
new_image_value = process_image_value(image)
end_time = time.time()
print(f"按值传递处理时间: {end_time - start_time} 秒")
# 引用传递处理
start_time = time.time()
new_image_reference = process_image_reference(image)
end_time = time.time()
print(f"引用传递处理时间: {end_time - start_time} 秒")
从这个案例中可以明显看出,按值传递需要复制整个图像数据,处理时间较长;而引用传递直接在原数据上操作,避免了复制,处理时间大幅缩短。
8. 编码风格与可维护性
除了代码效率,代码的可读性和可维护性也是软件开发中不可忽视的重要方面。以下是一些提高代码可读性和可维护性的建议:
-
注释
:在代码中添加清晰的注释,解释代码的功能、实现思路和关键步骤。例如,在函数开头说明函数的功能、输入参数和返回值的含义。
# 处理图像数据的函数
# 参数: image - 待处理的图像数据(二维数组)
# 返回值: 处理后的图像数据
def process_image(image):
# 对图像的每个像素进行乘以 2 的操作
for row in image:
for i in range(len(row)):
row[i] = row[i] * 2
return image
- 代码布局 :保持代码的整齐和规范,使用一致的缩进和空格。例如,在 Python 中使用 4 个空格进行缩进。
-
命名规范
:使用有意义的变量名和函数名,让代码更易于理解。例如,使用
image_width和image_height来表示图像的宽度和高度,而不是使用无意义的w和h。
9. 总结与展望
通过本文的介绍,我们了解了函数与过程中参数传递机制、函数返回值的处理方式,以及不同 CPU 架构的特点。同时,我们也认识到代码效率、可读性和可维护性在软件开发中的重要性。
在实际编程中,我们要根据具体需求,综合考虑各种因素,选择合适的参数传递方式和返回值处理策略,同时注重代码的编码风格和注释,以实现高效、易读和可维护的代码。
未来,随着计算机技术的不断发展,CPU 架构会更加多样化,编程语言也会不断推陈出新。我们需要不断学习和掌握新的知识,以适应新的编程环境和需求。同时,我们也要关注代码的安全性和性能优化,为开发出高质量的软件而努力。
希望本文能为你在编写函数与过程相关代码时提供有益的参考,让你能够编写出更加优秀的代码。如果你在实践过程中有任何问题或想法,欢迎随时交流和分享。
超级会员免费看
173万+

被折叠的 条评论
为什么被折叠?



