40、算术表达式优化与副作用解析

算术表达式优化与副作用解析

1. 整数类型选择与强度削减风险

在编程中,当不需要处理负数时,应尽量使用无符号整数而非有符号整数。例如在进行有符号数除以4的操作时,若使用 sarl 操作,当数值为负时需要给其值加3;进行有符号取模操作时,若初始值为负,需先给其值加31。以下是相关代码示例:

leave
ret
.p2align 2,,3
; For signed division by four,
;  using a sarl operation, we need
;  to add 3 to i's value if i was
;  negative.
.L5:
        leal    3(%ecx), %eax
        jmp     .L3
        .p2align 2,,3
; For signed % operation, we need to
;  first add 31 to i's value if it was
;  negative to begin with:
.L4:
        leal    31(%eax), %eax
        jmp     .L2

手动进行强度削减是有风险的。虽然在大多数CPU上,某些操作(如除法)几乎总是比其他操作(如右移)慢,但许多强度削减优化在不同CPU之间不具有可移植性。例如,用左移操作替代乘法在不同CPU上编译时,并不总是能产生更快的代码。一些较旧的C程序中手动添加的强度削减,如今可能会导致程序运行变慢。所以,应让编译器来处理强度削减优化。

2. 归纳优化

在许多表达式中,尤其是循环中的表达式,一个变量的值可能完全依赖于另一个变量。以下是Pascal和C语言的示例:
- Pascal示例

for i := 0 to 15 do begin
    j := i * 2;
    vector[ j ] := j;
    vector[ j + 1 ] := j + 0.5;
end;

编译器优化器可能会识别出 j 完全依赖于 i ,并将代码重写为:

ij := 0;  {ij is the combination of i and j from the previous code}
while( ij < 30 ) do
    vector[ ij ] := j;
    vector[ ij + 1 ] := ij + 0.5;
    ij := ij + 2;
end;

这样的优化节省了循环中的计算量(具体是 j := i * 2 的计算)。
- C语言示例

extern unsigned vector[16];
extern void someFunc( unsigned v[] );
extern void preventOptimization( int arg1, ... );
int main( void )
{
    unsigned i, j;
    someFunc( vector );
    for( i = 0; i < 16; ++i )
    {
        j = i * 2;
        vector[ j ] = j;
        vector[ j + 1 ] = j + 1;
    }
    preventOptimization( vector[0], vector[15] );
    return( 0 );
}

Microsoft的Visual C++编译器生成的MASM(80x86)代码如下:

_main   PROC NEAR ; COMDAT
; File t.c
; Line 19 -- call someFunc to initialize vector:
        push    OFFSET FLAT:_vector
        call    _someFunc
        add     esp, 4
        ;ECX is roughly "j".
        ;EAX points at the current vector element.
        xor     ecx, ecx
        mov     eax, OFFSET FLAT:_vector+4
;This is what the for loop translates to:
$L403:
; Line 23
        ;vector[j] = j;
        mov     DWORD PTR [eax-4], ecx
; Line 24
        ;vector[ j + 1 ] = j + 1;
        lea     edx, DWORD PTR [ecx+1]
        mov     DWORD PTR [eax], edx
        ;Advance EAX (pointer to vector
        ; element) past two elements:
        add     eax, 8
        ;Bump up j by two:
        add     ecx, 2
        ;Repeat for each element of the array
        ; that we process:
        cmp     eax, OFFSET FLAT:_vector+132
        jl      SHORT $L403
; Line 26 - call preventOptimization:
        mov     eax, DWORD PTR _vector+60
        mov     ecx, DWORD PTR _vector
        push    eax
        push    ecx
        call    _preventOptimization
        add     esp, 8
; Line 27
        xor     eax, eax
; Line 28
        ret     0
_main   ENDP

从MASM输出可以看出,编译器识别出 i 在循环中未被使用,将其完全优化掉,并且没有进行 j = i * 2 的计算。编译器使用归纳法确定 j 每次迭代增加2,并直接发出相应代码。此外,编译器通过指针遍历数组,而不是对数组进行索引,从而产生了更快更短的代码序列。虽然可以手动进行归纳优化,但结果通常更难阅读和理解,只有在编译器优化器无法生成良好机器代码时才考虑手动优化。

3. 循环不变式优化

循环不变式是指在循环的每次迭代中都不改变的表达式。以下是Basic和C语言的示例:
- Basic示例

i = 5;
for j = 1 to 10
    k = i * 2
next j

在这个循环中, k 的值在循环执行期间不会改变。可以将 k = i * 2 的计算移到循环之前:

i = 5;
k = i * 2
for j = 1 to 10
next j

这样, k 的值与之前的示例相同,但只计算了一次。
- C语言示例

extern unsigned someFunc( void );
extern void preventOptimization( unsigned arg1, ... );
int main( void )
{
    unsigned i, j, k, m;
    k = someFunc();
    m = k;
    for( i = 0; i < k; ++i )
    {
        j = k + 2;    // Loop-invariant calculation
        m += j + i;
    }
    preventOptimization( m, j, k, i );
    return( 0 );
}

Visual C++编译器生成的80x86 MASM代码如下:

_main   PROC NEAR ; COMDAT
; File t.c
; Line 5
        push    ecx
        push    esi
; Line 8
        call    _someFunc
; Line 10
        xor     ecx, ecx  ; i = 0
        test    eax, eax  ; see if k == 0
        mov     edx, eax  ; m = k
        jbe     SHORT $L108
        push    edi
; Line 12
; Compute j = k + 2, but only execute this
; once (code was moved out of the loop):
        lea     esi, DWORD PTR [eax+2] ;j = k + 2
; Here's the loop the above code was moved
;  out of:
$L99:
; Line 13
        ;m(edi) = j(esi) + i(ecx)
        lea     edi, DWORD PTR [esi+ecx]
        add     edx, edi
        ; ++i
        inc     ecx
        ;While i < k, repeat:
        cmp     ecx, eax
        jb      SHORT $L99
        pop     edi
; Line 15
;
; This is the code after the loop body:
        push    ecx
        push    eax
        push    esi
        push    edx
        call    _preventOptimization
        add     esp, 16                                 ; 00000010H
; Line 16
        xor     eax, eax
        pop     esi
; Line 17
        pop     ecx
        ret     0
$L108:
; Line 10
        mov     esi, DWORD PTR _j$[esp+8]
; Line 15
        push    ecx
        push    eax
        push    esi
        push    edx
        call    _preventOptimization
        add     esp, 16                                 ; 00000010H
; Line 16
        xor     eax, eax
        pop     esi
; Line 17
        pop     ecx
        ret     0
_main   ENDP

从代码注释可以看出,循环不变式表达式 j = k + 2 被移出循环并在循环开始前执行,从而节省了每次迭代的执行时间。与大多数优化不同,应尽量将所有循环不变式计算移出循环,除非有合理的理由将其留在循环中。如果要将不变式代码留在循环中,应注释原因。

4. 不同类型程序员与编译器优化

根据对编译器优化的理解程度,可以将高级语言(HLL)程序员分为三组:
| 程序员类型 | 特点 |
| ---- | ---- |
| 第一组 | 不知道编译器优化如何工作,编写代码时不考虑代码组织对优化器的影响。 |
| 第二组 | 理解编译器优化的工作原理,编写代码时更注重可读性,相信编译器能正确优化代码。 |
| 第三组 | 了解编译器可以进行的优化类型,但不信任编译器,手动将优化融入代码。 |

编译器优化器实际上是为第一组不了解编译器操作的程序员设计的。一个好的编译器通常会为这三组程序员生成大致相同质量的代码(至少在算术表达式方面)。但这仅适用于具有良好优化能力的编译器。如果需要在大量编译器上编译代码,且不能确保所有编译器都有好的优化器,手动优化可能是实现一致良好性能的一种方法。可以通过一些网站(如www.willus.com/ccomp_benchmark.shtml)来比较不同编译器的优化能力。

5. 算术表达式中的副作用

在表达式中,副作用是指代码产生的直接结果之外对程序全局状态的任何修改。C、C++、C#、Java等基于C的语言尤其容易在算术表达式中产生副作用。以下是C和Pascal的示例:
- C语言示例

i = i + *pi++ + (j = 2) * --k

这个表达式有四个副作用:
- 表达式末尾 k 的递减
- 使用 j 的值之前对 j 的赋值
- 解引用 pi pi 指针的递增
- 对 i 的赋值
- Pascal示例

var
   k:integer;
   m:integer;
   n:integer;
function hasSideEffect( i:integer; var j:integer ):integer;
begin
    k := k + 1;
    hasSideEffect := i + j;
    j = i;
end;
m := hasSideEffect( 5, n );

调用 hasSideEffect 函数产生两个副作用:
- 全局变量 k 的修改
- 按引用传递的参数 j (实际参数是 n )的修改

大多数语言不保证表达式各组成部分的计算顺序。例如,对于表达式 i := f(x) + g(x); ,编译器可能先调用 f 再调用 g ,也可能先调用 g 再调用 f 。如果 f g 产生副作用,这两种调用顺序可能会产生完全不同的结果。虽然大多数语言对表达式副作用的产生顺序未作定义,但有一些常见规则:
- 表达式中的所有副作用会在语句执行完成之前发生。
- 赋值语句左边变量的赋值不会在该变量在表达式右边被使用之前发生。

由于大多数语言中表达式副作用的产生顺序未定义,以下Pascal代码的结果通常是未定义的:

function incN:integer;
begin
    incN := n;
    n := n + 1;
end;
n := 2;
writeln( incN + n * 2 );

编译器可以先调用 incN 函数(这样在执行子表达式 n * 2 之前 n 将包含3),也可以先计算 n * 2 再调用 incN 函数。因此,不同的编译可能会产生不同的输出。不要试图通过实验来确定计算顺序,因为不同编译器或同一编译器在不同上下文中可能会有不同的计算顺序。如果必须依赖计算顺序,应将计算分解为一系列更简单的语句,以控制计算顺序。例如,若需要程序在 i := f(x) + g(x); 中先调用 f 再调用 g ,可以将代码改写为:

temp1 := f(x);
temp2 := g(x);
i := temp1 + temp2;

综上所述,在编程中应合理利用编译器优化,同时注意表达式中的副作用,以确保代码的正确性和性能。

算术表达式优化与副作用解析

6. 副作用对表达式计算顺序的影响及应对策略

副作用在算术表达式中会给计算顺序带来不确定性。为了更清晰地说明这一点,我们再来看几个不同语言的例子。

  • C语言示例
int a = 5, b = 3, c;
c = a++ + --b;

在这个表达式中,存在两个副作用: a 的后置自增和 b 的前置自减。由于C语言没有明确规定子表达式的计算顺序,编译器可能先计算 a++ ,也可能先计算 --b 。这就导致不同编译器或者同一编译器在不同优化级别下,最终 c 的值可能不同。

  • Python示例
x = 2
y = (x := x + 1) + x

这里使用了Python 3.8引入的海象运算符 := ,它在赋值的同时产生了副作用。同样,Python也没有规定子表达式的计算顺序,所以 y 的值可能因为计算顺序的不同而不同。

为了避免副作用带来的不确定性,我们可以采用以下策略:
1. 拆分表达式 :将复杂的表达式拆分成多个简单的语句,明确计算顺序。例如,对于上面的C语言示例,可以改写为:

int a = 5, b = 3, c;
int temp1 = a;
a = a + 1;
b = b - 1;
c = temp1 + b;
  1. 使用中间变量 :通过引入中间变量,将有副作用的操作单独处理。对于Python示例,可以改写为:
x = 2
x = x + 1
y = x + x
7. 不同语言对副作用的处理差异

不同的编程语言对副作用的处理方式存在差异。下面通过表格来对比几种常见语言:
| 语言 | 对副作用的支持程度 | 计算顺序规定 |
| ---- | ---- | ---- |
| C、C++ | 支持多种产生副作用的操作,如自增自减、指针操作等 | 未明确规定子表达式计算顺序 |
| Java | 支持一定的副作用操作,如对象属性修改 | 未明确规定子表达式计算顺序,但赋值语句有一定规则 |
| Python | 相对较少鼓励副作用,但支持海象运算符等产生副作用的操作 | 未明确规定子表达式计算顺序 |
| Haskell | 纯函数式语言,尽量避免副作用,表达式计算顺序由函数调用和求值策略决定 | 基于函数式编程的求值规则 |

从这个表格可以看出,不同语言在副作用处理上有不同的侧重点。函数式语言(如Haskell)更倾向于避免副作用,以保证代码的可预测性和可维护性;而命令式语言(如C、C++)则提供了更多产生副作用的手段,但也带来了计算顺序的不确定性。

8. 编译器优化与副作用的交互

编译器在进行优化时,副作用可能会干扰优化过程。例如,当编译器遇到有副作用的表达式时,可能无法对其进行一些常规的优化,因为副作用可能会影响程序的正确性。

下面是一个简单的示例:

int func(int *x) {
    *x = *x + 1;
    return *x * 2;
}

int main() {
    int a = 5;
    int result = func(&a) + func(&a);
    return 0;
}

在这个例子中, func 函数有副作用,它会修改传入指针所指向的值。编译器在优化 func(&a) + func(&a) 这个表达式时,不能简单地认为两次调用 func 函数的结果是相同的,因为第一次调用会修改 a 的值,从而影响第二次调用的结果。

为了让编译器能够更好地进行优化,我们应该尽量减少副作用的使用。如果无法避免副作用,应该在代码中清晰地标注,让编译器和其他开发者能够理解代码的行为。

9. 总结与建议

通过以上对算术表达式优化和副作用的分析,我们可以总结出以下几点建议:
1. 整数类型选择 :在不需要处理负数时,优先使用无符号整数,减少有符号数操作带来的额外计算。
2. 强度削减 :避免手动进行强度削减,让编译器来完成,除非编译器的优化能力不足。
3. 归纳优化 :在编译器无法生成良好机器代码时,才考虑手动进行归纳优化。
4. 循环不变式 :尽量将循环不变式计算移出循环,提高代码性能。
5. 副作用处理 :减少副作用的使用,明确计算顺序,避免因副作用导致的不确定性。

以下是一个简单的mermaid流程图,总结了优化和处理副作用的流程:

graph TD;
    A[编写代码] --> B{是否需要处理负数};
    B -- 否 --> C[使用无符号整数];
    B -- 是 --> D[使用有符号整数];
    D --> E{是否需要手动强度削减};
    E -- 否 --> F[让编译器优化];
    E -- 是 --> G[手动优化];
    F --> H{是否有循环不变式};
    H -- 是 --> I[移出循环];
    H -- 否 --> J{是否有副作用};
    I --> J;
    J -- 是 --> K[拆分表达式或使用中间变量];
    J -- 否 --> L[完成优化];
    K --> L;

按照这些建议进行编程,可以提高代码的性能和可维护性,避免因副作用导致的错误。在实际开发中,我们应该根据具体情况灵活运用这些优化策略,不断提升代码质量。

源码地址: https://pan.quark.cn/s/d1f41682e390 miyoubiAuto 米游社每日米游币自动化Python脚本(务必使用Python3) 8更新:更换cookie的获取地址 注意:禁止在B站、贴吧、或各大论坛大肆传播! 作者已退游,项目不维护了。 如果有能力的可以pr修复。 小引一波 推荐关注几个非常可爱有趣的女孩! 欢迎B站搜索: @嘉然今天吃什么 @向晚大魔王 @乃琳Queen @贝拉kira 第三方库 食用方法 下载源码 在Global.py中设置米游社Cookie 运行myb.py 本地第一次运行时会自动生产一个文件储存cookie,请勿删除 当前仅支持单个账号! 获取Cookie方法 浏览器无痕模式打开 http://user.mihoyo.com/ ,登录账号 按,打开,找到并点击 按刷新页面,按下图复制 Cookie: How to get mys cookie 当触发时,可尝试按关闭,然后再次刷新页面,最后复制 Cookie。 也可以使用另一种方法: 复制代码 浏览器无痕模式打开 http://user.mihoyo.com/ ,登录账号 按,打开,找到并点击 控制台粘贴代码并运行,获得类似的输出信息 部分即为所需复制的 Cookie,点击确定复制 部署方法--腾讯云函数版(推荐! ) 下载项目源码和压缩包 进入项目文件夹打开命令行执行以下命令 xxxxxxx为通过上面方式或取得米游社cookie 一定要用双引号包裹!! 例如: png 复制返回内容(包括括号) 例如: QQ截图20210505031552.png 登录腾讯云函数官网 选择函数服务-新建-自定义创建 函数名称随意-地区随意-运行环境Python3....
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值