变量乘常量
- 常量为2的幂
乘法将会被替换为执行周期更短的移位指令。
int fun(int n) {
return n * 16;
}
// mov eax, n
// shl eax, 4
- 常量为非2的幂
因为 thumb 和 x86 指令集的差异,安卓平台上处理的更好一些。
我并不推荐你把自己当成编译器,看到算式想着怎么转成汇编,而是推荐记下这种算法,看到计算过程知道怎么转成原式,当然也不追求100%还原,逻辑一致即可。
编译器会对非2的幂进行拆解,例如:
- n * 15 = n * 16 - n = n << 4 - n
- n * 12 = n * 3 * 4 = (n << 1 + n) << 2
int value = n * 15;
// rsb.w r0, r1, r1, lsl #4
int value = n * 12;
// add.w r0, r1, r1, lsl #1
当然 windows 平台也不是一无是处,某些乘法会通过 lea 将两条指令合并成一条。
- n * 4 + 5 = lea edx, [ecx * 4 + 5]
printf("%d", n * 4 + 5);
// mov ecx, n
// lea edx, [ecx * 4 + 5]
// push edx
至于值为不可拆分的素数,就改用 mul 指令。
变量乘变量
这一步没有什么优化空间,因为都是未知的,只能老老实实用 mul 指令。
int fun(int n, int m) {
return n * m;
}
// mov eax, n
// mov ecx, m
// imul ecx
除法
在看下面内容之前,不妨再问问自己,真的了解除法吗?除法的本质是什么?
ok,现在是复习时间,简单总结一下以下两个问题。
- 符号问题
- 两个无符号整数相除,结果依然是无符号
- 两个有符号整数相除,结果依然是有符号
- 混除,参数全被当成无符号计算,结果是无符号
- 取整问题
除数为无符号数
- 大数(负数)
在无符号中,负数的值是很大的,例如 -8 = 0xFFFFFFF8。
而除以这种大数,只能出现两种情况,1或 0,换个思路来想就可以写成这样:[被除数] >= [除数] ? 1 : 0
我们来看看 thumb 下是怎么优化的?
UINT value = (UINT)n / -8;
// cmn.w r0, #9 ; cmp r0, -9
// it hi
// movhi r1, #1 ; n > -9 ? 1 : 0
他这里做了一个小小的变形:[被除数] > [除数 - 1] ? 1 : 0,逻辑上仍然成立。
- 2的幂
简单的移位
UINT value = (UINT)n / 4;
// lsrs r1, r0, #2
- 非2的幂
接下来就要引入一个非常魔幻的设定,magic number。说来这个魔数,依稀记得早在几年前的知乎上看到过一篇文章,讲的是雷神之锤游戏引擎就使用了这么一个魔数,那时的cpu是非常低效的,而为了避免使用除法这种 cpu 周期偏长的指令,天才的程序员们想出了各种奇技淫巧,其中最为后人津津乐道的就是游戏中对平方根倒数的优化,将计算过程等价替换为加法和移位操作,损失少量的精度来换取绝对的性能。
我们这里的魔数稍有不同,它是用来优化除法的,而且逻辑上也相对容易理解一些,废话不多说,进入正题。
对于普通除法,我们可以得到以下的换算:(x => 被除数变量,c => 除数常量,M => 魔数)
假设用 M 代替 2^n / c 这个 Magic 变量,于是有:
也就是说,除法将会被转会成 (x * M) >> n 的逻辑进行运算,至于 M 和 n 值怎么来的,我们不关心,这是编译器根据除数算出来的最优值,会尽力保证偏差达到最小,我们要做的是认出魔数和移了多少位,然后根据 m = 2^n/c 公式求得原本的除数 c = 2^n/m
公式来源于《C++反汇编与逆向分析技术揭秘》,真的是非常非常的细,书中整个推导过程很完整,很建议各位去仔细研读一遍
以下代码为例:
printf("%u", (unsigned)argc / 3);
// mov eax, 0xAAAAAAAB ; M
// mul [argc] ; edx:eax = argc * M
// shr edx, 1 ; edx = argc * M >> 32 >> 1
// push edx
-
代码
+关注
关注
30文章
4787浏览量
68589 -
编译器
+关注
关注
1文章
1634浏览量
49128 -
Andorid
+关注
关注
0文章
7浏览量
6989
发布评论请先 登录
相关推荐
评论