首页
社区
课程
招聘
[讨论]浅析VC++6.0对整数除以常量的处理
发表于: 2010-7-18 00:39 26370

[讨论]浅析VC++6.0对整数除以常量的处理

2010-7-18 00:39
26370
1
2
3
4
5
6
7
8
// 代码1
printf("8 %% -3 = %d\r\n", 8 % -3);
 
// 代码2
printf("-8 %% -3 = %d\r\n", -8 % -3);
 
// 代码3
printf("-8 %% 3 = %d\r\n", -8 % 3);

[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

收藏
免费 7
支持
分享
赞赏记录
参与人
雪币
留言
时间
Youlor
为你点赞~
2024-5-31 03:01
伟叔叔
为你点赞~
2024-2-10 00:05
QinBeast
为你点赞~
2024-1-15 05:11
shinratensei
为你点赞~
2024-1-9 01:06
心游尘世外
为你点赞~
2023-12-23 00:13
飘零丶
为你点赞~
2023-12-11 00:34
PLEBFE
为你点赞~
2023-3-19 00:38
最新回复 (50)
雪    币: 1946
活跃值: (283)
能力值: (RANK:330 )
在线值:
发帖
回帖
粉丝
2
学习123456
2010-7-18 00:39
0
雪    币: 95
活跃值: (44)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
3
先附上参考资料:
1. Concrete Mathematics - Ronald L. Graham
2. The Art of Computer Programming - Donald E. Knuth
3. divcnst-pldi94.pdf (感谢forgot提供)
4. The Solver Web Online Help - ? (http://www.thesolver.it/Manuali/Factotum/source/001.htm 感谢bughoho提供)

由于本人没学会如何将word中的数学公式转换为论坛格式,故以下讨论用抓图表达。





上传的附件:
  • 1.jpg (105.02kb,956次下载)
  • 2.jpg (37.67kb,940次下载)
  • 3.jpg (47.25kb,946次下载)
2010-7-18 00:41
0
雪    币: 95
活跃值: (44)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
4
以下数学相关的定义和推导,若已掌握或者暂无兴趣,可跳过本节阅读其后的讨论,在后面的讨论中,涉及到的定义和推导将会以编号形式指出,感兴趣的读者可以回到本节考察相关推论和证明。









上传的附件:
  • 4.jpg (52.81kb,932次下载)
  • 5.jpg (63.54kb,932次下载)
  • 6.jpg (36.94kb,931次下载)
  • 7.jpg (55.38kb,934次下载)
  • 8.jpg (36.72kb,923次下载)
2010-7-18 00:43
0
雪    币: 95
活跃值: (44)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
5
3.        VC++6.0对整数除法的优化和论证
A.        VC++6.0 对除以整型常量的各类处理
整数除法运算对应汇编指令分有符号idiv、无符号div。除法是指令中执行周期较长的,因此其效率也是较低的,所以编译器会尽量将除法指令用其它运算指令代替。C++中的除法和数学中的除法不同。在C++中除法运算不保留余数,有专门提求取余数的运算“%”也称之为取模运算。而且对于整数除法,C++的规则是仅仅保留整数部分,小数部分完全舍弃。
如果除数是变量,则只能使用除法指令。但是如果除数为常量,便有了优化的余地,根据除数值的相关特性,编译器有对应的处理方式。
在本节我们讨论编译其对除数为2的次方、非2次方、负数等各类情景的处理方式,我们假定整型为4字节补码形式。

见代码中实例演示。
各类型除法转换——Debug调试版
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// C++源码说明:除法运算
// 变量定义
int nVarOne = argc;
int nVarTwo = argc;
// 两变量做除法
printf("nVarOne / nVarTwo = %d", nVarOne / nVarTwo);
// 变量除以常量,常量2的1次方
printf("nVarOne / 2 = %d", nVarOne / 2);
// 变量除以非二次方数
printf("nVarTwo / 7 = %d", nVarTwo / 7);
// 变量对非二次方数取模
printf("nVarTwo % 7 = %d", nVarTwo % 7);
// 变量除以常量,常量为2的3次方
printf("nVarOne / 8 = %d", nVarOne / 8);


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// C++源码于对应汇编代码讲解
// C++源码对比,变量定义
int nVarOne = argc;
0040B7E8   mov         eax,dword ptr [ebp+8]
0040B7EB   mov         dword ptr [ebp-4],eax
// C++源码对比,变量定义
37:       int nVarTwo = argc;
0040B7EE   mov         ecx,dword ptr [ebp+8]
0040B7F1   mov         dword ptr [ebp-8],ecx
// 除法运算转换特性
// C++源码对比,变量 / 变量
printf("nVarOne / nVarTwo = %d", nVarOne / nVarTwo);
; 取出被除数放入eax中
0040B7F4   mov         eax,dword ptr [ebp-4]
; 扩展高位
0040B7F7   cdq
; 两变量相除,直接使用有符号除法指令idiv
0040B7F8   idiv        eax,dword ptr [ebp-8]
; eax保存商值,作为参数压栈,调用函数printf,此函数讲解略
0040B7FB   push        eax
0040B7FC   push        offset string "nVarOne / nVarTwo = %d" (00420034)
0040B801   call        printf (0040b750)
0040B806   add         esp,8


1
2
3
4
5
6
7
8
9
// C++源码对比,变量 / 常量(常量值为2)
printf("nVarOne / 2 = %d", nVarOne / 2);
0040B809   mov         eax,dword ptr [ebp-4]
0040B80C   cdq
; 自身减去扩展高位
0040B80D   sub         eax,edx
; 和乘法运算类似,乘法是左移,对应的除法为右移
0040B80F   sar         eax,1
; printf 函数说明略……


1
2
3
4
5
6
7
8
9
10
11
12
13
// C++源码对比,变量 / 常量(常量值为2的3次方)
printf("nVarOne / 8 = %d", nVarOne / 8);
; 取出被除数放入eax
0040B851   mov         eax,dword ptr [ebp-4]
; 扩展eax高位到edx,eax中为负数则edx为0xFFFFFFFF
0040B854   cdq
; 如果eax为负数,则0xFFFFFFFF & 0x00000007 <==> 0x00000007 反之为0
0040B855   and         edx,7
; 使用eax加edx,如eax为负数则加7,反之加0
0040B858   add         eax,edx
; 将eax右移3位
0040B85A   sar         eax,3
; printf 函数说明略……


没有打开O2的时候下,以上代码中只有除数为2 的次方值的前提下,才进行了优化处理。我们先从最为较简单的除以常量2的优化开始分析。



上传的附件:
2010-7-18 00:43
0
雪    币: 95
活跃值: (44)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
6


上传的附件:
2010-7-18 00:45
0
雪    币: 95
活跃值: (44)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
7


上传的附件:
2010-7-18 00:45
0
雪    币: 95
活跃值: (44)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
8
回顾代码清单中的关键部分:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
……
; 92492493h疑似
.text:004010AA     mov eax, 92492493h
; 这里是流水线优化,esp和上次调用的call指令相关,和除法计算无关,可暂不理会。
.text:004010AF     add esp, 8
; 有符号乘法,用esi乘以eax,esi中保存被除数
.text:004010B2     imul esi
; 这里又多出一个诡异的加法
.text:004010B4     add edx, esi
; 右移2位,也可看作除4
.text:004010B6     sar edx, 2
; 结果给eax
.text:004010B9     mov eax, edx
; 负数调整加1
.text:004010BB     shr eax, 1Fh
.text:004010BE     add edx, eax
.text:004010C0     push edx
.text:004010C1     push offset aNvartwo7D ; "nVarTwo / 7 = %d"
.text:004010C6     call _printf
……






上传的附件:
2010-7-18 00:46
0
雪    币: 95
活跃值: (44)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
9
123456
上传的附件:
2010-7-18 00:48
0
雪    币: 95
活跃值: (44)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
10
4.        除数为负的非二次方(上)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.text:00401000 _main proc near ; CODE XREF: start+AFp
.text:00401000 arg_0= dword ptr  4
.text:00401000     mov ecx, [esp+arg_0]
[B].text:00401004     mov eax, 99999999h[/B]
.text:00401009     imul ecx
.text:0040100B     sar edx, 1
.text:0040100D     mov eax, edx
.text:0040100F     shr eax, 1Fh
.text:00401012     add edx, eax
.text:00401014     push edx
.text:00401015     push offset Format ; "%d\n"
.text:0040101A     call _printf
.text:0040101F     add esp, 8
.text:00401022     xor eax, eax
.text:00401024     retn
.text:00401024 _main endp
       

对于负除数求值过程中,有什么需要注意的呢?我们先看看除法转乘法的过程:



上传的附件:
2010-7-18 00:48
0
雪    币: 95
活跃值: (44)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
11
5.        除数为负的非二次方(下)
上例中我们讨论了对于Magic Number大于0x7fffffff的处理,那么在什么情况下 Magic Number会小于等于0x7fffffff,而且这个时候应该怎么处理呢?请先阅读以下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.text:00401000 _main proc near ; CODE XREF: start+AFp
.text:00401000 arg_0= dword ptr  4
.text:00401000     mov ecx, [esp+arg_0]
.text:00401004     mov eax, 6DB6DB6Dh
.text:00401009     imul ecx
.text:0040100B     sub edx, ecx
.text:0040100D     sar edx, 2
.text:00401010     mov eax, edx
.text:00401012     shr eax, 1Fh
.text:00401015     add edx, eax
.text:00401017     push edx
.text:00401018     push offset Format ; "%d"
.text:0040101D     call _printf
.text:00401022     add esp, 8
.text:00401025     retn
.text:00401025 _main endp

回忆前面除数等于+7的讨论,对于正除数,Magic Number大于0x7fffffff的处理:



上传的附件:
2010-7-18 00:49
0
雪    币: 95
活跃值: (44)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
12
B.        除法优化的原则(上)
看到这里,大家应该注意到以上讨论并还原除数的结果是近似值,说明了我们的公式给出还不够严格,那么我们可以好好思考一下其值近似但不等的原因,先看看余数是多少。
        回忆一下除法和余数的关系,根据(性质3),有:
b =(a - r) / q



上传的附件:
2010-7-18 00:50
0
雪    币: 95
活跃值: (44)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
13


上传的附件:
2010-7-18 00:50
0
雪    币: 95
活跃值: (44)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
14
现在分析一下VC++6.0中计算除法MagicNumber的过程,大家找到VC++6.0 bin目录下c2.dll(版本12.0.9782.0),先在LoadLibrary下断,等c2加载,其有符号整数除法MagicNumber的计算过程在c2的文件偏移5FACE处,加载后的虚拟地址请读者自行计算,断在此处可以看到有符号整数除法MagicNumber的推算过程,其汇编代码过长,我就不贴了,自己给出F5后修改的C代码,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 对于除数在3到13之间,直接查表,表结构如下
struct SignedMagicNumber
{
  int nMagic;
  int nExpInc;
};
 
// 对于除数为2的幂,有其他处理,故表内无对应值
struct SignedMagicNumber MagicTable[] = {
  {1, 1},           // 0
  {1, 1},           // 1
  {1, 1},           // 2
  {0x55555556, 0},
  {0, 0},           // 4
  {0x66666667, 1},
  {0x2AAAAAAB, 0},
  {0x92492493, 2},
  {0, 0},           // 8
  {0x38E38E39, 1},
  {0x66666667, 2},
  {0x2E8BA2E9, 1},
  {0x2AAAAAAB, 1}
};


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#define EXP31 0x80000000
 
// 以下代码还原修改自VC++6.0 bin目录下c2.dll(版本12.0.9782.0),文件偏移5FACE,
// 原程序的返回值定义为结构体,这里修改为参数返回
int GetMagic(unsigned int nDivC, int *nOutExpInc)
{
//   if ((int)nDivC >= 3 && nDivC < 13)
//   {
//     *nOutExpInc = MagicTable[nDivC].nExpInc;
//     return MagicTable[nDivC].nMagic;
//   }
 
  unsigned int nAbsDivC = abs(nDivC);
  int nExcBase = 31;
 
  // t = 2^31 if nDivC > 0
  // or t = 2^31 + 1 if nDivC < 0
  unsigned int t = (nDivC >> 31) + EXP31;
 
  // |nc| = t - 1 - rem(t, |nDivC|)
  unsigned int nLargestMultiple  = t - t % nAbsDivC - 1;
  unsigned int q1 = EXP31 / nLargestMultiple;
  unsigned int r1 = EXP31 - nLargestMultiple * q1;
  unsigned int nMagicNumber = EXP31 / nAbsDivC;
  unsigned int r2 = EXP31 - nAbsDivC * nMagicNumber;
 
  do
  {
    r1 *= 2;
    q1 *= 2;
    ++nExcBase;
    if ( r1 >= nLargestMultiple )
    {
      ++q1;
      r1 -= nLargestMultiple;
    }
    r2 *= 2;
    nMagicNumber *= 2;
    if ( r2 >= nAbsDivC )
    {
      ++nMagicNumber;
      r2 -= nAbsDivC;
    }
  }
  while ( q1 < nAbsDivC - r2 || q1 == nAbsDivC - r2 && !r1 );
 
  nMagicNumber++;
 
  if ( (int)nDivC < 0 )
    nMagicNumber = -(int)nMagicNumber;
 
  *nOutExpInc = nExcBase - 32;
 
  return nMagicNumber;
}


然后写个程序验证一下看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
int main(int argc)
{
  int nExpInc;
  int nMagicNumber;
 
 
  int nDividend = argc-201; // 这是被除数
  int nDivisor = -100;      // 这是除数
  int nQuotient;            // 这里存放商
 
  // GetMagic用来计算magic number,
  // 第一个参数指定除数,第二个参数OUT指数相对32的增量
  // 这个例子用来模拟计算70 / -7的结果
  do
  {
    nMagicNumber = GetMagic(nDivisor, &nExpInc);
    printf("nMagicNumber = 0x%08x, ExpInc = %d\r\n", nMagicNumber, nExpInc);
 
    if (nDivisor >= 0)
    {
      __asm
      {
        mov eax, nMagicNumber // 编译器会做成imm寻址,nMagicNumber早已在编译期间算出
        mov esi, nDividend
        imul esi
 
        // 编译器不会产生这里的跳转,
        // 因为编译阶段就计算出nMagicNumber的取值了,
        // 所以编译期间就可以决定是否产生其后的add指令,
        // nMagicNumber小于0x80000000(负数)则不需增加add
        test nMagicNumber, 80000000h
        jz NEXT1
        add edx, esi
NEXT1:
        mov ecx, nExpInc
        sar edx, cl
        shr esi, 31
        add edx, esi
        mov nQuotient, edx
      }
    }
    else
    {
      __asm
      {
        mov eax, nMagicNumber
        mov esi, nDividend
        imul esi
 
        test nMagicNumber, 80000000h
        jnz NEXT2
        sub edx, esi
NEXT2:
        mov ecx, nExpInc
        sar edx, cl
        mov ecx, edx
        shr ecx, 31
        add edx, ecx
        mov nQuotient, edx
      }
    }
     
    printf("%d / %d = %d\r\n", nDividend, nDivisor, nQuotient);
    printf("%d / %d = %d\r\n", nDividend, nDivisor, nDividend / nDivisor);
    if (nQuotient != nDividend / nDivisor)
    {
      puts("Error");
      break;
    }
     
    nDivisor++;
    if (nDivisor == 0 || nDivisor == -1 || nDivisor == 1)
    {
      nDivisor = 2;
    }
    nDividend += 10;
  }
  while(nDivisor <= 100);
   
  return 0;
}


见附件

  下次讨论此代码的数学推导
上传的附件:
2010-7-18 00:51
0
雪    币: 678
活跃值: (12)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
15
前两天上课讲的这么快就放上来了啊?
不知道ImageBase是哪位童鞋啊?
2010-7-18 01:09
0
雪    币: 401
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
16
占了这么多楼,应该有不少期,我也占楼等学习。
2010-7-18 03:15
0
雪    币: 17
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
17
座等学习
2010-7-18 10:53
0
雪    币: 95
活跃值: (44)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
18
7月19日更新章节
2010-7-19 15:42
0
雪    币: 780
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
19
不错,学习了
2010-7-19 15:48
0
雪    币: 95
活跃值: (44)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
20
7月20日更新
2010-7-20 15:25
0
雪    币: 217
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
21
貌似跟vc++没啥关联,标题应该为“浅析C语言对整数除以常量的处理”
2010-7-20 16:07
0
雪    币: 95
活跃值: (44)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
22
C语言是规则,具体实现每个厂商的编译器都不同
2010-7-21 15:02
0
雪    币: 95
活跃值: (44)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
23
7月21日更新
2010-7-21 16:38
0
雪    币: 200
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
24
说明环境,方便读者测试

占楼学习
2010-7-21 21:38
0
雪    币: 560
活跃值: (313)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
25
学习了
2010-7-27 13:05
0
游客
登录 | 注册 方可回帖
返回

账号登录
验证码登录

忘记密码?
没有账号?立即免费注册