[链接] 除法优化与编译时常量参数的函数调用优化

晚上在FV群里说起编译器优化的事情时,汉公找出了几个看起来颇不直观的例子让我猜其意义。我脑子跟不上节奏了,把东西写成代码试了下觉得有问题,然后讨论变得激烈了起来……我决定直接写段简单的代码看看我机上的编译器会如何优化。结果我得到了些一时没料到的结果。仔细想想,以前其实碰到过一模一样的状况。

不过和谐的TX无法提供顺畅的沟通环境(老是丢信息……),汉公说干脆在澄空发个帖:[url=http://bbs.sumisora.com/read.php?tid=10876422]贴一下你们的反汇编结果[/url]

汉公:
[quote]源码:
#include <stdio.h>

static unsigned int do_div(unsigned int div)
{
return div / 7;
}

int main(int argc, char* argv[])
{
int a = 50;
// 这里的AAAAA完全是为了在oly里看的清楚
printf("AAAAAAAAAAAAAAAA %d\n", do_div(a));
return 0;
}


主要看一下div / 7这句话的反汇编是什么。
请按这个格式注明(debug版的不用贴了,肯定是DIV了,如果被强制内联变成直接push 7的就都算了):

编译器:VC6
反汇编:
[code]MOV ECX, DWORD PTR SS:[ESP+4] ; 参数div
MOV EAX, 24924925
MUL ECX
MOV EAX, ECX
SUB EAX, EDX
SHR EAX, 1
ADD EAX, EDX
SHR EAX, 2
RETN[/code]

最后想听听各位对于除以7为什么编译成这样的看法。
这玩意比较有用,经常能碰到编译器莫名的mov eax, 66666667或者cccccccd什么的。[/quote]

补充:米粒和汉公用GCC3.4.x编译出来的结果:
-O1:
[quote][code]004012e0 <_do_div>:
4012e0: 55 push %ebp
4012e1: 89 e5 mov %esp,%ebp
4012e3: 8b 4d 08 mov 0x8(%ebp),%ecx
4012e6: ba 25 49 92 24 mov $0x24924925,%edx
4012eb: 89 c8 mov %ecx,%eax
4012ed: f7 e2 mul %edx
4012ef: 89 c8 mov %ecx,%eax
4012f1: 29 d0 sub %edx,%eax
4012f3: d1 e8 shr %eax
4012f5: 01 c2 add %eax,%edx
4012f7: 89 d0 mov %edx,%eax
4012f9: c1 e8 02 shr $0x2,%eax
4012fc: 5d pop %ebp
4012fd: c3 ret[/code][/quote]
-O2:
[code] 4012ff: b8 07 00 00 00 mov $0x7,%eax
401304: 89 44 24 04 mov %eax,0x4(%esp)
401308: e8 33 05 00 00 call 401840 <_printf>[/code]

我:
[quote]汉公的代码原封不动拿来编译:
#include <stdio.h>

static unsigned int do_div(unsigned int div)
{
return div / 7;
}

int main(int argc, char* argv[])
{
int a = 50;
// 这里的AAAAA完全是为了在oly里看的清楚
printf("AAAAAAAAAAAAAAAA %d\n", do_div(a));
return 0;
}


得到:
[code]00401000 /$ 6A 07 push 7
00401002 |. 68 58A14000 push zz.0040A158 ; ASCII "AAAAAAAAAAAAAAAA %d"
00401007 |. E8 06000000 call zz.00401012
0040100C |. 83C4 08 add esp,8
0040100F |. 33C0 xor eax,eax
00401011 \. C3 retn[/code]
=========================================================
#include<stdio.h>

void main(int argc, char* argv[]){
int a=46, b;

printf("XXXXXXXXXXX\n");
b = divBySeven(a);
printf("%d", b);
}

int divBySeven(int a) {
return (a / 7);
}

得到:
[code]00401000 /$ 68 5CA14000 push zz.0040A15C ; ASCII "XXXXXXXXXXX"
00401005 |. E8 12000000 call zz.0040101C
0040100A |. 6A 06 push 6
0040100C |. 68 58A14000 push zz.0040A158 ; ASCII "%d"
00401011 |. E8 06000000 call zz.0040101C
00401016 |. 83C4 0C add esp,0C
00401019 |. 33C0 xor eax,eax
0040101B \. C3 retn[/code]
=========================================================
#include<stdio.h>

void main(int argc, char* argv[]){
int a=50, b;

printf("XXXXXXXXXXX\n");
b = divBySeven(a);
printf("%d", b);
}

int divBySeven(int a) {
return (a / 7);
}


得到:
[code]00401000 /$ 68 5CA14000 push zz.0040A15C ; ASCII "XXXXXXXXXXX"
00401005 |. E8 12000000 call zz.0040101C
0040100A |. 6A 06 push 7
0040100C |. 68 58A14000 push zz.0040A158 ; ASCII "%d"
00401011 |. E8 06000000 call zz.0040101C
00401016 |. 83C4 0C add esp,0C
00401019 |. 33C0 xor eax,eax
0040101B \. C3 retn[/code]
=========================================================
以上结果以VC8编译得到.
[code]cl /O2 /Ox zz.c[/code]

为了避免运算被折叠,用这段代码再编译一次:
#include<stdio.h>
#include<stdlib.h>

void main(int argc, char* argv[]){
int a, b;

a = atoi(argv[1]);
printf("XXXXXXXXXXX\n");
b = divBySeven(a);
printf("%d", b);
}

int divBySeven(int a) {
return (a / 7);
}

得到:
[code]00401000 /$ 8B4424 08 mov eax,dword ptr ss:[esp+8]
00401004 |. 8B48 04 mov ecx,dword ptr ds:[eax+4]
00401007 |. 56 push esi
00401008 |. 51 push ecx
00401009 |. E8 07010000 call zz.00401115
0040100E |. 68 5CA14000 push zz.0040A15C ; ASCII "XXXXXXXXXXX"
00401013 |. 8BF0 mov esi,eax
00401015 |. E8 25000000 call zz.0040103F
0040101A |. B8 93244992 mov eax,92492493
0040101F |. F7EE imul esi
00401021 |. 03D6 add edx,esi
00401023 |. C1FA 02 sar edx,2
00401026 |. 8BC2 mov eax,edx
00401028 |. C1E8 1F shr eax,1F
0040102B |. 03C2 add eax,edx
0040102D |. 50 push eax
0040102E |. 68 58A14000 push zz.0040A158 ; ASCII "%d"
00401033 |. E8 07000000 call zz.0040103F
00401038 |. 83C4 10 add esp,10
0040103B |. 33C0 xor eax,eax
0040103D |. 5E pop esi
0040103E \. C3 retn[/code]
同样是在VC8上,开了/O2与/Ox

#include<stdio.h>
#include<stdlib.h>

void main(int argc, char* argv[]){
unsigned int a, b;

a = atoi(argv[1]);
printf("XXXXXXXXXXX\n");
b = divBySeven(a);
printf("%d", b);
}

unsigned int divBySeven(unsigned int a) {
return (a / 7);
}

得到:
[code]00401000 /$ 8B4424 08 mov eax,dword ptr ss:[esp+8]
00401004 |. 8B48 04 mov ecx,dword ptr ds:[eax+4]
00401007 |. 56 push esi
00401008 |. 51 push ecx
00401009 |. E8 04010000 call zz.00401112
0040100E |. 68 5CA14000 push zz.0040A15C ; ASCII "XXXXXXXXXXX"
00401013 |. 8BF0 mov esi,eax
00401015 |. E8 22000000 call zz.0040103C
0040101A |. B8 25499224 mov eax,24924925
0040101F |. F7E6 mul esi
00401021 |. 2BF2 sub esi,edx
00401023 |. D1EE shr esi,1
00401025 |. 03F2 add esi,edx
00401027 |. C1EE 02 shr esi,2
0040102A |. 56 push esi
0040102B |. 68 58A14000 push zz.0040A158 ; ASCII "%d"
00401030 |. E8 07000000 call zz.0040103C
00401035 |. 83C4 10 add esp,10
00401038 |. 33C0 xor eax,eax
0040103A |. 5E pop esi
0040103B \. C3 retn[/code]
这个跟汉公在开头贴的那个差不多.常量的不同确实是来自unsigned.
=====================================================
另外,如果不开优化的话,
#include<stdio.h>

void main(int argc, char* argv[]){
int a=46, b;

printf("XXXXXXXXXXX\n");
b = divBySeven(a);
printf("%d", b);
}

int divBySeven(int a) {
return (a / 7);
}

得到:
[code]00401000 /$ 55 push ebp
00401001 |. 8BEC mov ebp,esp
00401003 |. 83EC 08 sub esp,8
00401006 |. C745 FC 2E00000>mov dword ptr ss:[ebp-4],2E
0040100D |. 68 00C04000 push zz.0040C000 ; ASCII "XXXXXXXXXXX"
00401012 |. E8 39000000 call zz.00401050
00401017 |. 83C4 04 add esp,4
0040101A |. 8B45 FC mov eax,dword ptr ss:[ebp-4]
0040101D |. 50 push eax ; /Arg1
0040101E |. E8 1D000000 call zz.00401040 ; \zz.00401040
00401023 |. 83C4 04 add esp,4
00401026 |. 8945 F8 mov dword ptr ss:[ebp-8],eax
00401029 |. 8B4D F8 mov ecx,dword ptr ss:[ebp-8]
0040102C |. 51 push ecx
0040102D |. 68 10C04000 push zz.0040C010 ; ASCII "%d"
00401032 |. E8 19000000 call zz.00401050
00401037 |. 83C4 08 add esp,8
0040103A |. 33C0 xor eax,eax
0040103C |. 8BE5 mov esp,ebp
0040103E |. 5D pop ebp
0040103F \. C3 retn
00401040 /$ 55 push ebp
00401041 |. 8BEC mov ebp,esp
00401043 |. 8B45 08 mov eax,dword ptr ss:[ebp+8]
00401046 |. 99 cdq
00401047 |. B9 07000000 mov ecx,7
0040104C |. F7F9 idiv ecx
0040104E |. 5D pop ebp
0040104F \. C3 retn[/code]
直接用了idiv。[/quote]

VC6、VC8、GCC3.4等编译器在打开O2开关后基本上都会把上面例子中的常参数函数调用折叠优化掉。汉公的本意是指出利用定点小数来把整型除法转变成乘法的优化,在开了O2开关后却看不到了(被折叠了)。

=============================================================

汉公想说明的是这样的优化:
[code]mov eax,24924925
mul esi
sub esi,edx
shr esi,1
add esi,edx
shr esi,2
push esi[/code]
这里,0x24924925是个定点小数。通过这段代码的运算,得到的是esi /= 7的效果。不直接使用idiv是为了速度的最大化。

[quote]汉公 20:50:04
这个东西对解包来说实用价值有2:
1。混淆算法 一个div / 7 比那个乱78遭的东西好看的多 也不容易错
2。当索引项定长,且只有索引段整个长度的时候,要算资源数的时候用这个[/quote]

这种技巧印象中在[i]Hacker's Delight[/i]里有提到。回头得查查看是不是有。没有的话还得想想清楚这代码究竟是怎么跑通的。

[quote]汉公 19:08:35
[code]00416F76 |. B8 CDCCCCCC |MOV EAX, CCCCCCCD
00416F7B |. 81E1 FFFFFF7F |AND ECX, 7FFFFFFF
00416F81 |. F7E1 |MUL ECX
00416F83 |. C1EA 05 |SHR EDX, 5
00416F86 |. B8 64000000 |MOV EAX, 64
00416F8B |. 2BC2 |SUB EAX, EDX
00416F8D |> 99 |CDQ
00416F8E |. B9 65000000 |MOV ECX, 65
00416F93 |. F7F9 |IDIV ECX[/code]

你能猜出这个是干吗的吗

汉公 19:16:18
(ecx & 0x7fffffff) * 0.8 / 5

汉公 19:17:35
用定点小数做除法

汉公 19:17:52
cccccccd实际上是32位的定点小数0.8

汉公 19:18:45
mul完以后 edx里面是整数部分 eax是余数

汉公 19:19:01
不 eax是小数部分

汉公 19:20:05
啊 刚才写错了 实际上他做的是 * 4 / 5 再除32 也就是除以40

汉公 19:20:15
(ecx & 0x7fffffff) / 40

汉公 19:22:07
既然上面的代码都用乘法替换除法了 那么你知道为什么下面的代码又用了DIV这样的慢速除法指令了吗?编译器闹残?

汉公 19:22:40
因为只有在除数是常数的时候 编译器可以做优化 而除数是变量的化 编译器只能用div了[/quote]

John_He大:
[quote]这应该是所谓的倒数乘法,因为IA-32结构中乘法比除法快4~6倍,所以编译器经常用倒数除法处理除数为常数的除法。

除以7的情况:
[code]MOV ECX, DWORD PTR SS:[ESP+4] ; 参数div
MOV EAX, 24924925
MUL ECX
MOV EAX, ECX
SUB EAX, EDX
SHR EAX, 1
ADD EAX, EDX
SHR EAX, 2[/code]
可以这样转化:
y = ((x - x * sr) / 2 + x * sr) / 4

其中x为被除数,sr = 0x24924925
sr * 7 = 0x100000003 = 0x100000000 + 3
(sr - 1) * 7 = 0xFFFFFFFC = 0x100000000 - 4

可以将x * sr理解成“定点小数乘法”,即小数点位置固定的实数的乘法。从上面的式子不难看出sr为 (1/7) * 0x100000000,使用mul指令后,高位(edx)保存的就是x * (1/7)的整数部分,低位(eax)就是小数部分。因此上面的代码可以看做是div * (1/7),结果自然就是div / 7。[/quote]

=============================================================

后来又说起GCC的优化框架的事情。想起以前读过的一篇资料,介绍GCC4系列采用了新的Tree-SSA优化框架,与GCC3.x有很大不同。但是当时读的那篇东西却找不到了。这次学乖了,至少把这次看到的资料的关键字和地址记下来的好。

GCC4里增加的优化框架是由先前的[url=http://gcc.gnu.org/projects/tree-ssa]SSA for Trees项目[/url]演变而来的。[url=http://gcc.gnu.org/onlinedocs/gccint/Tree-SSA.html]Tree-SSA[/url]包括两个主要部分,[url=http://gcc.gnu.org/onlinedocs/gccint/GENERIC.html]GENERIC[/url]与[url=http://gcc.gnu.org/onlinedocs/gccint/GIMPLE.html]GIMPLE[/url]。
[img]http://gcc.gnu.org/projects/tree-ssa/tree-opt.png[/img]

目前进行中的相关领域的项目有:
[url=http://gcc.gnu.org/wiki/tuples]GIMPLE tuples[/url]([url=http://gcc.gnu.org/wiki/tuples?action=AttachFile&do=get&target=tuples.pdf]tuples.pdf[/url]阅读中。总觉得这种把树的形式转换为以tuple表现的思想之前在书上读到过)
[url=http://gcc.gnu.org/projects/tree-ssa/vectorization.html]Auto-vectorization in GCC[/url](或者[url]http://gcc.gnu.org/wiki/VectorizationTasks[/url])

顺便看到了段有趣的文字。这些错误谁能不犯呢,诶……
[url=http://gcc.gnu.org/wiki/DeadlySins]The "Deadly Sins" from P. J. Brown'sWriting Interactive Compilers and Interpreters Wiley 1979.[/url]
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值