47、迭代控制结构全解析

迭代控制结构全解析

1. 布尔表达式评估与非结构化代码

在研究汇编代码时,我们会发现某些程序会对原始布尔表达式的两个部分都进行评估,也就是完全布尔评估。不过,使用非结构化代码时需要格外小心。一方面,这样的代码结果更难阅读;另一方面,采用这种方式很难让编译器生成你期望的代码。而且,在一个编译器上能生成优质代码的代码序列,在其他编译器上可能无法产生类似的效果。

如果你的编程语言不支持 break 语句,你可以使用 goto 语句跳出循环来达到相同的效果。但往代码里注入 goto 语句并非良策。不过,要是你需要完全布尔评估语义,并且在你的语言里使用 goto 是实现这一目的的唯一办法,那也别无选择。

2. 在 while 循环中强制短路布尔评估

有时候,即便语言(如BASIC或Pascal)没有实现短路评估,你也需要确保 while 语句中布尔表达式的短路评估。就像 if 语句一样,你可以通过重新安排循环控制表达式的计算方式来在程序中实现这一点。与 if 语句不同的是,你不能使用嵌套 while 语句或在 while 循环前添加其他语句来实现,但在大多数编程语言中仍然是可行的。

例如下面的C代码片段:

while( ptr != NULL && ptr->data != 0 )
{
    << loop body >>
    ptr = ptr->Next; // Step through a linked list.
}

如果C语言不保证布尔表达式的短路评估,这段代码可能会出错。

在像Pascal这样的语言中,强制完全布尔评估最简单的方法是编写一个函数,使用短路布尔评估来计算并返回布尔结果。但由于函数调用的开销较大,这种方案相对较慢。以下是一个用Borland Delphi编译器编译的Pascal示例:

program shortcircuit;
{$APPTYPE CONSOLE}
uses SysUtils;
var
    ptr     :Pchar;
    function shortCir( thePtr:Pchar ):boolean;
    begin
        shortCir := false;
        if( thePtr <> NIL ) then begin
            shortCir := thePtr^ <> #0;
        end; //if
    end;  // shortCircuit
begin
    ptr := 'Hello world';
    while( shortCir( ptr )) do begin
        write( ptr^ );
        inc( ptr );
    end; // while
end.

对应的80x86汇编代码如下:

sub_408570  proc near
            ;EDX holds function return
            ; result (assume false).
            ;
            ; shortCir := false;
            xor     edx, edx
            ;if( thePtr <> NIL ) then begin
            test    eax, eax
            jz      short loc_40857C    ;branch if NIL
            ; shortCir := thePtr^ <> #0;
            cmp     byte ptr [eax], 0
            setnz   dl  ;DL = 1 if not #0
loc_40857C:
            ;Return result in EAX:
            mov     eax, edx
            retn
sub_408570  endp
; Main Program (pertinent section):
;
; Load EBX with the address of the global "ptr" variable and 
; then enter the "WHILE" loop (Delphi moves the test for the
; while loop to the physical end of the loop's body):
                mov     ebx, offset loc_408628
                jmp     short loc_408617
; --------------------------------------------------------
loc_408600:
                ; Print the current character whose address
                ; "ptr" contains:
                mov     eax, ds:off_4092EC  ;ptr pointer
                mov     dl, [ebx]           ;fetch char
                call    sub_404523          ;print char
                call    sub_404391
                call    sub_402600
                inc     ebx             ;inc( ptr )
; while( shortCir( ptr )) do ...
loc_408617:
                mov     eax, ebx         ;Pass ptr in EAX
                call    sub_408570       ;shortCir
                test    al, al           ;Returns True/False
                jnz     short loc_408600 ;branch if true

sub_408570 过程包含一个函数,用于计算类似于前面C代码中表达式的短路布尔评估。可以看到,如果 thePtr 为NIL(零),解引用 thePtr 的代码永远不会执行。

如果不能使用函数调用,那么唯一合理的解决方案可能是采用非结构化方法。以下是前面C代码中 while 循环的Pascal版本,强制进行短路布尔评估:

while( true ) do begin
    if( ptr = NIL ) then goto 2;
    if( ptr^.data = 0 ) then goto 2;
    << loop body >>
    ptr = ptr^.Next;
end;
2:

同样,生成像这个例子中的非结构化代码应该作为最后的手段。但如果所使用的语言(或编译器)不保证短路评估,而你又需要这些语义,那么非结构化代码或低效代码(使用函数调用)可能是唯一的解决方案。

3. repeat..until do..until / do..while )循环

大多数现代编程语言中另一个常见的循环是 repeat..until 循环。 repeat..until 循环在循环底部测试终止条件,这意味着循环体至少会执行一次,即使布尔控制表达式在循环的第一次迭代中计算结果为False。虽然 repeat..until 循环的适用性比 while 循环稍窄,使用频率也远不如 while 循环,但在很多情况下,它是完成任务的最佳控制结构选择。经典的例子是从用户那里读取输入,直到用户输入某个特定值。以下是一个非常典型的Pascal代码片段:

repeat
write( 'Enter a value (negative quits): ');
readln( i );
// do something with i's value
until( i < 0 );

这个循环的循环体总是会执行一次,这是必要的,因为必须执行循环体才能从用户那里读取程序用于检查循环执行何时结束的值。

repeat..until 循环在其布尔控制表达式计算结果为True时终止(而 while 循环是在计算结果为False时终止)。这是合理的,因为“until”这个词表明循环在控制表达式计算结果为True时终止。不过,这只是一个小的语法问题,C/C++/Java等语言(以及许多继承了C语言传统的语言)提供了 do..while 循环,只要循环条件计算结果为True,就会重复执行循环体。从效率的角度来看,这两种循环没有任何区别,你可以通过使用语言的逻辑非运算符轻松地将一个循环的终止条件转换为另一个循环的终止条件。以下是Pascal、HLA和C/C++的 repeat..until do..while 循环的语法示例:

Pascal的 repeat..until 循环示例:

repeat
    (* Read a raw character from the "input" file, which in this case is 
the keyboard *)
    ch := rawInput( input );  
    (* Save the character away. *)
    inputArray[ i ] := ch;    
    i := i + 1;
    (* Repeat until the user hits the enter key   *)
until( ch = chr( 13 ));      

C/C++的 do..while 版本:

do
{
    /* Read a raw character from the "input" file, which in this case is 
the keyboard */
    ch = getKbd();

    /* Save the character away. */

    inputArray[ i++ ] = ch;
    /* Repeat until the user hits the enter key   */   
}
while( ch != '\r' );         

HLA的 repeat..until 循环:

repeat
    // Read a character from the standard input device.
    stdin.getc();
    // Save the character away.
    mov( al, inputArray[ ebx ] );
    inc( ebx );
    // Repeat until the user hits the enter key.
until( al = stdin.cr );

repeat..until (或 do..while )循环转换为汇编语言相对简单直接。编译器只需要替换布尔循环控制表达式的代码,并在表达式计算结果为肯定( repeat..until 为False, do..while 为True)时跳回到循环体的开头。以下是前面HLA的 repeat..until 循环的纯汇编实现(C/C++和Pascal的编译器为其他示例生成的代码几乎相同):

rptLoop:

    // Read a character from the standard input.
    call stdin.getc; 
    // Store away the character.

    mov( al, inputArray[ ebx ] );  
    inc( ebx );
    // Repeat the loop if the user did not hit
    //  the enter key.
    cmp( al, stdio.cr );           
    jne rptLoop;                   

可以看到,典型编译器为 repeat..until (或 do..while )循环生成的代码通常比普通 while 循环的代码效率略高。

由于编译器通常可以为 repeat..until / do..while 循环生成比 while 循环稍高效的代码,如果语义上可行,应该考虑使用 repeat..until / do..while 形式。在许多程序中,某些循环结构的布尔控制表达式在第一次迭代时总是计算结果为True。例如,在应用程序中经常会看到这样的循环:

i = 0;
while( i < 100 )
{
    printf( "i: %d\n", i );
    i = i * 2 + 1;
    if( i < 50 )
    {
        i += j;
    }
}

这个 while 循环可以很容易地转换为 do..while 循环:

i = 0;
do
{
    printf( "i: %d\n", i );
    i = i * 2 + 1;
    if( i < 50 )
    {
        i += j;
    }
} while( i < 100 );

这种转换是可行的,因为我们知道 i 的初始值(零)小于100,所以循环体至少会执行一次。

通过使用更合适的 repeat..until / do..while 循环而不是普通的 while 循环,可以帮助编译器生成更好的代码。不过要注意,效率提升很小,因此在这样做时要小心不要牺牲代码的可读性或可维护性。总之,始终要使用逻辑上最合适的循环结构。如果循环体至少会执行一次,即使 while 循环也能正常工作,也应该使用 repeat..until / do..while 循环。

下面用一个表格总结 while 循环和 repeat..until do..while )循环的特点:
| 循环类型 | 测试位置 | 循环体执行次数 | 终止条件 |
| ---- | ---- | ---- | ---- |
| while | 循环开始 | 可能为0次 | 条件为False |
| repeat..until / do..while | 循环结束 | 至少1次 | 条件为True |

下面是 repeat..until 循环的执行流程mermaid流程图:

graph TD;
    A[开始] --> B[执行循环体];
    B --> C{条件判断};
    C -- 条件为False --> B;
    C -- 条件为True --> D[结束];
4. 在 repeat..until 循环中强制完全布尔评估

由于 repeat..until (或 do..while )循环的终止测试在循环底部进行,因此在 repeat..until 循环中强制进行完全布尔评估的方式与在 if 语句中类似。考虑以下C/C++代码:

extern int x;
extern int y;
extern int f( int );
extern int g( int );
int main( void )
{
    do
    {
        ++a;
        --b;
    }while( a < f(x) && b > g(y));

    return( 0 );
}

以下是GCC为PowerPC生成的代码(使用C标准的短路评估):

L2:
    // ++a
    // --b
    lwz r9,0(r30)  ; get a
    lwz r11,0(r29) ; get b
    addi r9,r9,-1  ; --a
    lwz r3,0(r27)  ; Set up x parm for f
    stw r9,0(r30)  ; store back into a
    addi r11,r11,1 ; ++b
    stw r11,0(r29) ; store back into b
    ; compute f(x)
    bl L_f$stub    ; call f, result to R3
    ; is a >= f(x)? If so, quit loop
    lwz r0,0(r29)  ; get a
    cmpw cr0,r0,r3 ; Compare a with f's value
    bge- cr0,L3
    lwz r3,0(r28)  ; Set up y parm for g
    bl L_g$stub    ; call g
    lwz r0,0(r30)  ; get b
    cmpw cr0,r0,r3 ; Compare b with g's value
    bgt+ cr0,L2    ; Repeat if b > g's value
L3:

从这个代码示例可以看出,如果表达式 a < f(x) 为False(即 a >= f(x) ),程序会跳过对 b > g(y) 的测试,直接跳转到标签 L3

为了在这种情况下强制进行完全布尔评估,C源代码需要在 while 子句之前计算布尔表达式的子组件(将子表达式的结果保存在临时变量中),然后在 while 子句中仅测试这些结果:

static int a;
static int b;
extern int x;
extern int y;
extern int f( int );
extern int g( int );
int main( void )
{
    int temp1;
    int temp2;

    do
    {
        ++a;
        --b;
        temp1 = a < f(x);
        temp2 = b > g(y);
    }while( temp1 && temp2 );

    return( 0 );
}

以下是GCC将其转换为PowerPC代码的结果:

L2:
    lwz r9,0(r30)    ;r9 = b
    li r28,1         ;temp1 = True
    lwz r11,0(r29)   ;r11 = a
    addi r9,r9,-1    ;--b
    lwz r3,0(r26)    ;r3 = x (set up f's parm)
    stw r9,0(r30)    ;Save b
    addi r11,r11,1   ;++a
    stw r11,0(r29)   ;Save a
    bl L_f$stub      ;Call f
    lwz r0,0(r29)    ;Fetch a
    cmpw cr0,r0,r3   ;Compute temp1 = a < f(x)
    blt- cr0,L5      ;Leave temp1 true if a < f(x)
    li r28,0         ;temp1 = false
L5:
    lwz r3,0(r27)    ;r3 = y, set up g's parm
    bl L_g$stub      ;Call g
    li r9,1          ;temp2 = True
    lwz r0,0(r30)    ;Fetch b
    cmpw cr0,r0,r3   ;Compute b > g(y)
    bgt- cr0,L4      ;Leave temp2 true if b > g(y)
    li r9,0          ;Else set temp2 false
L4:
    ;Here's the actual termination test in
    ;the while clause:
    cmpwi cr0,r28,0
    beq- cr0,L3
    cmpwi cr0,r9,0
    bne+ cr0,L2
L3:

可以注意到,实际的布尔表达式 (temp1 && temp2) 仍然使用短路评估。然而,这种短路评估仅涉及创建的临时变量。无论第一个子表达式的结果如何,循环都会计算两个原始子表达式。

5. 在 repeat..until 循环中强制短路布尔评估

如果编程语言提供了跳出 repeat..until 循环的功能,如C的 break 语句,那么强制进行短路评估相对容易。考虑上一节中强制进行完全布尔评估的C do..while 循环:

do
{
    ++a;
    --b;
    temp1 = a < f(x);
    temp2 = b > g(y);
}while( temp1 && temp2 );

以下是将此代码转换为使用短路布尔评估来计算终止表达式的一种方法:

static int a;
static int b;
extern int x;
extern int y;
extern int f( int );
extern int g( int );
int main( void )
{
    do
    {
        ++a;
        --b;
        if( !( a < f(x) )) break;
    }while( b > g(y) );

    return( 0 );
}

以下是GCC为PowerPC生成的 do..while 循环代码:

L2:
    lwz r9,0(r30)   ;r9 = b
    lwz r11,0(r29)  ;r11 = a
    addi r9,r9,-1   ;--b
    lwz r3,0(r27)   ;Set up f(x) parm
    stw r9,0(r30)   ;Save b
    addi r11,r11,1  ;++a
    stw r11,0(r29)  ;Save a
    bl L_f$stub     ;Call f
    ; break if a < f(x):
    lwz r0,0(r29)
    cmpw cr0,r0,r3
    bge- cr0,L3
    ; while( b > g(y) ):
    lwz r3,0(r28)   ;Set up y parm
    bl L_g$stub     ;Call g
    lwz r0,0(r30)   ;Compute b > g(y)
    cmpw cr0,r0,r3
    bgt+ cr0,L2     ;Branch if true
L3:

如果 a 小于 f(x) 返回的值,此代码会立即跳出循环(到标签 L3 ),而不会测试 b 是否大于 g(y) 返回的值。因此,此代码模拟了表达式 a < f(x) && b > g(y) 的短路布尔评估。

如果使用的编译器不支持类似于C/C++的 break 语句,则需要使用更复杂的逻辑。以下是一种可能的方法:

static int a;
static int b;
extern int x;
extern int y;
extern int f( int );
extern int g( int );
int main( void )
{
    int temp;
    do
    {
        ++a;
        --b;
        temp = a < f(x);
        if( temp )
        {
            temp = b > g(y);
        };
    }while( temp );

    return( 0 );
}

以下是GCC为PowerPC生成的代码:

L2:
    lwz r9,0(r30)   ;r9 = b
    lwz r11,0(r29)  ;r11 = a
    addi r9,r9,-1   ;--b
    lwz r3,0(r27)   ;Set up f(x) parm
    stw r9,0(r30)   ;Save b
    addi r11,r11,1  ;++a
    stw r11,0(r29)  ;Save a
    bl L_f$stub     ;Call f
    li r9,1         ;Assume temp is True
    lwz r0,0(r29)   ;Set temp false if
    cmpw cr0,r0,r3  ;a < f(x)
    blt- cr0,L5
    li r9,0
L5:
    cmpwi cr0,r9,0  ;If !(a < f(x)) then bail
    beq- cr0,L10    ; on the do..while loop
    lwz r3,0(r28)   ;Compute temp = b > f(y)
    bl L_g$stub     ; using a code sequence
    li r9,1         ; that is comparable to
    lwz r0,0(r30)   ; the above.
    cmpw cr0,r0,r3
    bgt- cr0,L9
    li r9,0
L9:
    ; Test the while termination expression:
    cmpwi cr0,r9,0
    bne+ cr0,L2
L10:

即使这些示例使用的是逻辑与( && )操作,使用逻辑或( || )操作也同样容易。以下是一个Pascal示例及其转换:

repeat
a := a + 1;
b := b - 1;
until( a < f(x) OR b > g(y) );

以下是强制进行完全布尔评估的转换:

repeat
a := a + 1;
b := b - 1;
temp := a < f(x);
if( not temp ) then begin
temp := b > g(y);
end;
until( temp );

以下是Borland的Delphi为这两个循环生成的代码(假设在编译器选项中选择了完全布尔评估):

;    repeat
;
;        a := a + 1;
;        b := b - 1;
;
;    until( (a < f(x)) or (b > g(y)));
loc_4085F8:
    inc     ebx         ; a := a + 1;
    dec     esi         ; b := b - 1;
    mov     eax, [edi]  ;EDI points at x
    call    locret_408570
    cmp     ebx, eax    ;Set AL to 1 if
    setl    al          ; a < f(x)
    push    eax         ;Save Boolean result.
    mov     eax, ds:dword_409288    ;y
    call    locret_408574           ;g(6)
    cmp     esi, eax    ;Set AL to 1 if
    setnle  al          ; b > g(y)
    pop     edx         ;Retrieve last value.
    or      dl, al      ;Compute their OR
    jz      short loc_4085F8 ;Repeat if false.
;    repeat
;
;        a := a + 1;
;        b := b - 1;
;        temp := a < f(x);
;        if( not temp ) then begin
;
;            temp := b > g(y);
;
;        end;
;
;    until( temp );
loc_40861B:
    inc     ebx     ;a := a + 1;
    dec     esi     ;b := b - 1;
    mov     eax, [edi]  ;Fetch x
    call    locret_408570 ;call f
    cmp     ebx, eax    ;is a < f(x)?
    setl    al          ;Set AL to 1 if so.
    ; If the result of the above calculation is
    ; True, then don't bother with the second
    ; test (i.e., short-circuit evaluation)
    test    al, al
    jnz     short loc_40863C
    ;Now check to see if b > g(y)
    mov     eax, ds:dword_409288
    call    locret_408574
    ;Set AL = 1 if b > g(y):
    cmp     esi, eax
    setnle  al
; Repeat loop if both conditions were false:
loc_40863C:
    test    al, al
    jz      short loc_40861B

Delphi编译器为强制短路评估生成的代码远不如允许编译器自动处理(不选择完全布尔评估)时生成的代码好。以下是未选择完全布尔评估(即指示Delphi使用短路评估)的Delphi代码:

loc_4085F8:
    inc     ebx
    dec     esi
    mov     eax, [edi]
    call    nullsub_1 ;f
    cmp     ebx, eax
    jl      short loc_408613
    mov     eax, ds:dword_409288
    call    nullsub_2 ;g
    cmp     esi, eax
    jle     short loc_4085F8

虽然在编译器不支持短路评估时,这种技巧对于强制进行短路评估很有用,但后面的Delphi示例充分表明,如果可能的话,应该使用编译器的功能,这样通常会得到更好的机器代码。

下面用一个表格总结不同布尔评估方式的特点:
| 评估方式 | 实现方法 | 效率 | 适用场景 |
| ---- | ---- | ---- | ---- |
| 完全布尔评估 | 提前计算子表达式并保存结果 | 相对较低 | 需要对所有子表达式求值时 |
| 短路布尔评估 | 使用 break 语句或临时变量控制 | 相对较高 | 部分子表达式结果可决定整体结果时 |

下面是在 repeat..until 循环中强制短路布尔评估的执行流程mermaid流程图:

graph TD;
    A[开始] --> B[执行循环体];
    B --> C{条件1判断};
    C -- 条件1为False --> D[结束循环];
    C -- 条件1为True --> E{条件2判断};
    E -- 条件2为False --> D;
    E -- 条件2为True --> B;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值