43、控制结构与编程决策

控制结构与编程决策

1. 控制结构的重要性与性能影响

控制结构是高级语言编程的基础。基于特定条件评估做出决策的能力,是计算机实现各类自动化的基础。将高级语言(HLL)的控制结构转换为机器码,对程序的性能和大小有着重大影响。了解在特定情况下使用哪种控制结构,是编写优秀代码的关键。

控制结构相关的机器指令在程序中占了相当比例,而控制转移指令通常会刷新指令流水线,因此往往比执行简单计算的指令慢。为了编写高效的程序,应尽量减少控制转移指令的数量,若无法减少,则选择最快的指令。

不同CPU用于控制程序流程的指令集有所不同,但许多CPU(包括常见的两类)采用比较和跳转模式来控制程序流程。即通过比较或修改CPU标志的指令后,条件跳转指令根据CPU标志设置将控制转移到另一个位置。有些CPU可以用单条指令完成,有些则需要多条指令。不同CPU允许比较的条件范围也不同,但HLL语句在不同CPU上的映射序列具有可比性,理解一种CPU的基本转换方式,就能大致了解编译器在所有CPU上的工作原理。

2. 低级控制结构介绍

大多数CPU通过两步过程进行编程决策:
1. 比较两个值,并将比较结果保存在机器寄存器或标志中。
2. 执行第二条指令,测试比较结果,并根据结果将控制转移到两个位置之一。通过这种比较和条件分支序列,几乎可以合成大多数主要的HLL控制结构。

在比较和条件分支模式下,CPU通常采用两种不同方法实现条件代码序列:
- 方法一 :在基于栈的架构(如Java虚拟机)中常见,有不同形式的比较指令来测试特定条件,如等于比较、不等于比较、小于比较、大于比较等,每个比较结果为布尔值,然后通过“真分支”和“假分支”两条条件分支指令根据比较结果转移控制。
- 方法二 :CPU指令集包含一条设置或清除CPU程序状态或标志寄存器中多个位的比较指令,然后使用更具体的条件分支指令(如等于跳转、不等于跳转、小于跳转、大于跳转等)转移控制。80x86和PowerPC采用这种“比较和跳转”技术。

条件分支通常是双向分支,根据测试条件的真假将控制转移到不同位置。为减少指令大小,大多数CPU的条件分支指令只编码一个可能分支位置的地址,另一个位置采用隐含地址。通常,条件为真时转移到目标位置,为假时执行下一条指令。

例如,以下80x86的 je (等于跳转)指令序列:

// Compare the value in EAX to the value in EBX
        cmp( eax, ebx ); 
// Branch to label EAXequalsEBX if EAX==EBX
        je EAXequalsEBX; 
        mov( 4, ebx );      // Drop down here if EAX != EBX
            .
            .
            .
EAXequalsEBX:

该序列先比较 EAX EBX 的值,设置80x86的 EFLAGS 寄存器中的条件码位。若 EAX 等于 EBX je 指令将控制转移到 EAXequalsEBX 标签后的指令;若不相等, je 指令继续执行下一条 mov 指令。

80x86的条件跳转指令有两种形式:
- 2字节形式(1字节操作码和1字节有符号位移,范围为 -128 到 +127)。
- 6字节形式(2字节操作码和4字节有符号位移,范围为 -20亿 到 +20亿)。

位移值指定程序跳转到目标位置的字节距离。为转移到附近位置,可使用短形式分支。由于80x86指令长度通常在1到15字节之间(典型为3或4字节),短形式条件跳转指令通常可跳过约32到40条机器指令。当目标位置超出 ±127 字节范围时,6字节版本可将范围扩展到当前指令周围20亿字节。显然,若追求最高效率,应尽可能使用2字节形式。

在现代(流水线)CPU中,分支操作成本较高,因为可能需要刷新和重新加载流水线。对于条件分支,只有当分支被执行时才会产生此成本。若条件分支指令继续执行下一条指令,CPU将继续使用流水线中的指令而不刷新。因此,在许多系统中,继续执行下一条指令的分支比跳转分支更快。不过,有些CPU(如PowerPC)支持分支预测功能,可提前从分支目标位置预取指令,但不同处理器的分支预测算法不同,难以一概而论其对HLL代码的影响。一般来说,除非为特定处理器编写代码,否则继续执行下一条指令比跳转更高效。

除了比较和条件分支模式,还有其他基于计算结果转移控制的方式,间接跳转(尤其是通过地址表)是最常见的替代形式。例如:

readonly
    jmpTable: dword[4] := [&label1, &label2, &label3, &label4];
            .
            .
            .
        jmp( jmpTable[ ebx*4 ] );

jmp 指令根据 EBX 的值从 jmpTable 数组中获取双字值,将控制转移到四个不同位置之一。这大致相当于但通常短于以下指令序列:

        cmp( ebx, 0 );
        je label1;
        cmp( ebx, 1 );
        je label2;
        cmp( ebx, 2 );
        je label 3;
        cmp( ebx, 3 );
        je label4;
        // Results are undefined if EBX <> 0, 1, 2, or 3

以下是一个简单的流程图,展示了比较和条件分支的基本过程:

graph TD;
    A[开始] --> B[比较两个值];
    B --> C{条件判断};
    C -- 真 --> D[跳转到目标位置];
    C -- 假 --> E[继续执行下一条指令];
3. goto语句

goto 语句可能是最基本的低级控制结构。自20世纪60年代末和70年代的“结构化编程”浪潮以来,HLL代码中 goto 语句的使用逐渐减少,一些现代高级编程语言甚至不提供无结构的传统 goto 语句。即使在支持无限制 goto 的语言中,编程风格指南也通常将其使用限制在特殊情况下。从可读性角度看,减少 goto 语句的使用是好事,因为过多的 goto 会使代码难以阅读和维护。

不过,有些程序员认为使用 goto 语句可以编写更高效的代码。 goto 语句的一个重要效率论据是它有助于避免重复代码。例如,以下C/C++代码:

    if( a == b || c < d )
    {
        << execute some number of statements >>
        if( x == y )
        {
            << execute some statements if x == y >>
        }
        else
        {
            << execute some statements if x != y >>
        }
    }
    else
    {
        << execute the same sequence of statements 
            that the code executes if x != y in the 
            previous else section >>
    }

为提高效率,可改写为:

    if( a == b || c < d )
    {
        << execute some number of statements >>
        if( x != y ) goto DuplicatedCode;
        << execute some statements if x == y >>
    }
    else
    {
DuplicatedCode:
        << execute the same sequence of statements 
            if x != y or the original
            Boolean expression is false >>
    }

改写后的代码虽然减少了重复代码,但存在一些软件工程问题,如可读性、可修改性和可维护性略有降低。不过,也可以认为它更易于维护,因为只需在一处修复公共代码的缺陷。

许多现代编译器的优化器会识别类似的重复代码序列,并生成与使用 goto 语句相同的代码。例如,以下C/C++代码编译为PowerPC代码:

#include <stdio.h>
static int a;
static int b;
extern int x;
extern int y;
extern int f( int );
extern int g( int );
int main( void )
{
    if( a==f(x))
    {
        if( b==g(y))
        {
            a = 0;
        }
        else
        {
            printf( "%d %d\n", a, b );
            a = 1;
            b = 0;
        }
    }
    else
    {
        printf( "%d %d\n", a, b );
        a = 1;
        b = 0;
    }

    return( 0 );
}

GCC编译后的PowerPC代码如下:

        ; f(x):
        lwz r3,0(r9)
        bl L_f$stub
        ; Compute a==f(x), jump to L2 if false
        lwz r4,0(r30)
        cmpw cr0,r4,r3
        bne+ cr0,L2
        ; g(y):
        addis r9,r31,ha16(L_y$non_lazy_ptr-L1$pb)
        addis r29,r31,ha16(_b-L1$pb)
        lwz r9,lo16(L_y$non_lazy_ptr-L1$pb)(r9)
        la r29,lo16(_b-L1$pb)(r29)
        lwz r3,0(r9)
        bl L_g$stub
        ; Compute b==g(y), jump to L3 if false:
        lwz r5,0(r29)
        cmpw cr0,r5,r3
        bne- cr0,L3
        ; a = 0
        li r0,0
        stw r0,0(r30)
        b L5
        ;Set up a and b parameters if
        ; a==f(x) but b != g(y):
L3:
        lwz r4,0(r30)
        addis r3,r31,ha16(LC0-L1$pb)
        b L6
        ; Set up parameters if a != f(x):
L2:
        addis r29,r31,ha16(_b-L1$pb)
        addis r3,r31,ha16(LC0-L1$pb)
        la r29,lo16(_b-L1$pb)(r29)
        lwz r5,0(r29)
        ; Common code shared by both
        ; ELSE sections:
L6:
        la r3,lo16(LC0-L1$pb)(r3) ;Call printf
        bl L_printf$stub
        li r9,1                 ;a = 1
        li r0,0                 ;b = 0
        stw r9,0(r30)           ;Store a
        stw r0,0(r29)           ;Store b
L5:

并非所有编译器都有能识别重复代码的优化器。若想编写无论使用何种编译器都能编译为高效机器代码的程序,可能会倾向于使用 goto 语句。但传统软件工程方法是将公共代码放入过程或函数中调用,不过函数调用和返回的开销可能较大,尤其是重复代码较少时。对于短的公共代码序列,创建宏或内联函数可能是最佳解决方案。总之,使用 goto 语句提高效率应作为最后手段。

goto 语句的另一个常见用途是处理异常情况。当嵌套在多个语句中且需要退出所有语句时,若重构代码不能提高可读性,使用 goto 是可以接受的。然而,从嵌套块中跳出的跳转可能会影响优化器为整个过程或函数生成优质代码的能力。 goto 语句可能在局部代码中节省一些字节或处理器周期,但可能对函数的其余部分产生不利影响,导致整体代码效率降低。因此,在代码中使用 goto 语句时要谨慎。

4. break、continue、next、return等受限形式的goto语句

为支持结构化无 goto 编程,许多编程语言添加了受限形式的 goto 语句,允许程序员立即退出某些控制结构,如循环、过程或函数。常见语句包括:
- break/exit :跳出封闭循环。
- continue/cycle/next :重启封闭循环。
- return/exit :立即从封闭过程/函数返回。

这些语句比标准 goto 更具结构性,因为程序员无需选择目标位置,控制会根据封闭的控制语句(或函数/过程)转移到固定位置。

几乎所有这些语句都编译为单条 jmp 指令。跳出循环的语句(如 break )编译为将控制转移到循环底部之后第一条语句的 jmp 指令;重启循环的语句(如 continue next cycle )编译为将控制转移到循环终止测试(如 while repeat..until/do..while 循环)或循环顶部(大多数其他循环)的 jmp 指令。

虽然这些语句通常编译为单条机器指令( jmp ),但不要认为使用它们一定高效。即使忽略 jmp 指令可能导致CPU刷新指令流水线的开销,从循环中分支出来的语句也可能对性能产生影响。

以下是一个简单的表格,总结了这些受限形式 goto 语句的编译情况:
| 语句 | 功能 | 编译结果 |
| ---- | ---- | ---- |
| break/exit | 跳出封闭循环 | 转移到循环底部之后第一条语句的jmp指令 |
| continue/cycle/next | 重启封闭循环 | 转移到循环终止测试或循环顶部的jmp指令 |
| return/exit | 立即从封闭过程/函数返回 | 相应的返回指令 |

综上所述,在编写代码时,应根据具体情况合理选择控制结构和语句,权衡代码的效率、可读性和可维护性。

5. 不同控制结构的性能对比与选择策略

在实际编程中,我们需要根据具体场景来选择合适的控制结构,以平衡代码的性能和可读性。下面通过一个表格来对比不同控制结构在常见场景下的性能特点:
| 控制结构 | 性能特点 | 适用场景 |
| ---- | ---- | ---- |
| 比较和条件分支 | 常见且通用,但分支操作可能影响性能 | 简单的条件判断场景,如判断数值大小关系等 |
| 间接跳转(通过地址表) | 可根据计算结果快速定位目标位置,代码相对简洁 | 有多个固定目标位置可供选择的场景,如菜单选项跳转 |
| goto语句 | 可能避免重复代码,但影响可读性和维护性 | 处理重复代码或异常退出情况,但需谨慎使用 |
| break、continue等受限goto语句 | 结构清晰,编译简单,但跳转可能影响性能 | 循环内的快速退出或重启操作 |

例如,在一个简单的菜单选择程序中,如果有多个固定的菜单项,使用间接跳转可能是一个不错的选择:

readonly
    menuTable: dword[4] := [&option1, &option2, &option3, &option4];
            .
            .
            .
        jmp( menuTable[ userChoice*4 ] );

而在一个复杂的嵌套循环中,如果需要在满足特定条件时快速跳出所有循环,使用 break 语句可能更合适:

for (int i = 0; i < 10; i++) {
    for (int j = 0; j < 10; j++) {
        if (someCondition) {
            break; // 跳出内层循环
        }
    }
    if (someOtherCondition) {
        break; // 跳出外层循环
    }
}
6. 优化控制结构以提高性能的技巧

为了提高代码的性能,我们可以采用一些优化控制结构的技巧:
- 减少控制转移指令 :尽量避免不必要的条件判断和跳转,减少指令流水线的刷新次数。例如,将一些简单的条件判断合并,避免多次比较。
- 合理使用短形式跳转指令 :在目标位置较近时,优先使用短形式的条件跳转指令(如80x86的2字节形式),以减少指令长度和提高执行效率。
- 利用编译器优化 :现代编译器通常具有强大的优化功能,能够识别和处理重复代码、优化控制结构等。编写代码时,尽量遵循编译器的优化规则,让编译器发挥最大作用。
- 考虑分支预测 :虽然不同CPU的分支预测算法不同,但在编写代码时,可以尽量让代码的执行路径符合大多数情况下的预期,以提高分支预测的准确率。例如,将大概率执行的代码放在条件判断为真的分支中。

以下是一个优化前后的代码对比示例:
优化前

if (a > 10) {
    // 执行一些操作
}
if (b < 20) {
    // 执行另一些操作
}

优化后

if (a > 10 && b < 20) {
    // 合并条件判断,减少跳转
    // 执行相关操作
}
7. 控制结构在不同编程语言中的实现差异

不同编程语言对控制结构的实现方式可能存在差异,这会影响代码的编写和性能。以下是几种常见编程语言中控制结构的特点:
| 编程语言 | 控制结构特点 | 示例 |
| ---- | ---- | ---- |
| C/C++ | 提供丰富的控制结构,包括 if switch goto 等,可直接操作底层硬件 | plaintext if (a == b) { // 操作 } |
| Java | 不支持传统的 goto 语句,提供 break continue 等受限跳转语句,强调面向对象编程 | plaintext for (int i = 0; i < 10; i++) { if (i == 5) { break; } } |
| Python | 采用缩进来表示代码块,控制结构简洁明了,没有 goto 语句 | plaintext if a > 10: # 操作 |

例如,在Python中,由于没有 goto 语句,要实现类似的跳转功能,可能需要通过函数调用或循环控制来实现:

def someFunction():
    # 执行一些操作
    if someCondition:
        return
    # 继续执行其他操作

someFunction()
8. 总结与建议

控制结构是编程中不可或缺的一部分,它对程序的性能、可读性和可维护性有着重要影响。在编写代码时,我们应该根据具体需求和场景,合理选择和使用控制结构。
- 性能优先场景 :在对性能要求极高的场景下,如嵌入式系统、游戏开发等,可以适当使用 goto 语句来避免重复代码,但要注意代码的可读性和维护性。同时,尽量减少控制转移指令,合理利用编译器优化和分支预测。
- 可读性优先场景 :在大多数业务逻辑开发中,应优先考虑代码的可读性和可维护性。避免使用过多的 goto 语句,采用结构化的控制结构(如 if-else for while 等)和受限跳转语句(如 break continue )。
- 跨平台开发 :不同CPU和编程语言对控制结构的实现存在差异,在进行跨平台开发时,要充分了解这些差异,编写具有通用性的代码。

总之,掌握控制结构的原理和优化技巧,能够帮助我们编写更加高效、可靠的代码。

以下是一个流程图,展示了选择控制结构的基本思路:

graph TD;
    A[开始] --> B{性能要求高吗?};
    B -- 是 --> C{是否可避免重复代码?};
    C -- 是 --> D[考虑使用goto语句];
    C -- 否 --> E[减少控制转移指令];
    B -- 否 --> F{强调可读性吗?};
    F -- 是 --> G[使用结构化控制结构];
    F -- 否 --> H[根据具体情况选择];
    D --> I[注意可读性和维护性];
    E --> J[利用编译器优化];
    G --> K[避免使用goto语句];
    I --> L[结束];
    J --> L;
    K --> L;
    H --> L;

通过以上内容,我们对控制结构和编程决策有了更深入的了解,希望这些知识能够帮助你在实际编程中做出更好的选择。

根据原作 https://pan.quark.cn/s/459657bcfd45 的源码改编 Classic-ML-Methods-Algo 引言 建立这个项目,是为了梳理和总结传统机器学习(Machine Learning)方法(methods)或者算法(algo),和各位同仁相互学习交流. 现在的深度学习本质上来自于传统的神经网络模型,很大程度上是传统机器学习的延续,同时也在不少时候需要结合传统方法来实现. 任何机器学习方法基本的流程结构都是通用的;使用的评价方法也基本通用;使用的一些数学知识也是通用的. 本文在梳理传统机器学习方法算法的同时也会顺便补充这些流程,数学上的知识以供参考. 机器学习 机器学习是人工智能(Artificial Intelligence)的一个分支,也是实现人工智能最重要的手段.区别于传统的基于规则(rule-based)的算法,机器学习可以从数据中获取知识,从而实现规定的任务[Ian Goodfellow and Yoshua Bengio and Aaron Courville的Deep Learning].这些知识可以分为四种: 总结(summarization) 预测(prediction) 估计(estimation) 假想验证(hypothesis testing) 机器学习主要关心的是预测[Varian在Big Data : New Tricks for Econometrics],预测的可以是连续性的输出变量,分类,聚类或者物品之间的有趣关联. 机器学习分类 根据数据配置(setting,是否有标签,可以是连续的也可以是离散的)和任务目标,我们可以将机器学习方法分为四种: 无监督(unsupervised) 训练数据没有给定...
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值