我是很少转载文章的,可是这篇写得实在是太棒了。虽然写于12年,但是依然具有学习的意义。

  本文转自zeroevent,我对文章排版做了一定的修改。

  为了使除法的倒数相乘优化成为可能,编译器使用了定点运算方案来表示小数。

  那么何为定点运算,定点运算有什么特点呢?

  我们都知道一般情况下我们的小数都是用浮点类型来表示的,有一位会记录当前的小数点位于那里,当然还有其他的一些转换规则,这些都不是本文所关心的,我们只需知道浮点类型的小数可以位于任意一位,也就是说“小数点是浮动的”。

  而定点运算根据字面意思来理解就是“小数点是固定的”,这种小数的定点表示法有很多优点,首当其冲的就是效率上的提高。当然,作为代价,同样也必须承受随之而来的精度上的丢失。

  那么,这个定点表示法又是怎样运作的呢?它怎么就能保存小数信息呢?这部分内容很难寻找,经过大量近似资料的启示与笔者的试验,最终才证实其原理其实很简单,这首先还要从定点表示法的小数点位置与精度说起。

  首先,编译器一般都将小数点定位在第一位,因为要表示一个数的倒数,那么用小数表示的话必然是一个小于1的数,例如8的倒数与12345678的倒数分别为0.125与~0.000000081,因此其整数部分恒为0。

  其次,就是精度问题,例如我们用一个4bit大小的数来表述小数的话,那么它的精度就是0.0625,其表示的整数每加1或减1,其表示的小数就随之增加或减少0.0625,如下所示:

  确定精度:

  而对于32位的编译器来说,只不过是将上面的例子的位数扩充了一下,因此精度也就随之提高,我们直接看例子:

  大多数情况下除法是最好识别的,因为其特征很明显。另外,在上一节《乘法的识别与优化原理》笔者详细的讲解了编译器利用位移指令(sar、shr)的优化情况,很明显的,除法必然也会存在这种优化,但是鉴于其基础知识已经讲解且原理比较简单,因此除法的位移优化在本小节中将一带而过,我们将重点放在倒数相乘相关的优化上。

  按照惯例,请读者阅读以下源代码,并猜测编译器会将其进行怎样的优化:

  下面我们先看看它的Debug版部分反汇编代码:

  (再次提醒一下,Debug的反汇编代码并不是完整的,与逻辑无关的部分代码都已经被笔者删除了,如有疑问请查看第1.3节)

  通过上面例子可知,虽然Debug模式下未开启任何优化,不过在除数为2的次方的情况下,编译器仍对其做了不小的优化,那么Release版就更是可想而知了:

  后面除数为2的次方的优化没什么可说的,与Debug版里一模一样,但是上面的除法优化可能是有些读者很困惑,这究竟是为什么呢?我们先看看下面这张表:

  由上表可知,不管我们反汇编中体现出来的值最终比实际计算出来的值大几倍,而且都是2的次方,例如11的倒数对应的是大2倍,而59对应的是大8倍。

  其次反汇编中体现出来的值不管比原值大几倍或是大致没变,都要比原值大1。

  着看起来似乎有些怪异,我们先想想后面这种情况,想想为什么非要大个1呢?它为什么不大2或100呢?这其实不难理解,很明显的与精度有关,我们拿除数为59的定点运算结果72796055.68241664000来说,在将其转为16进制后,它的小数会直接被舍去,而按照四舍五

  入的原则其实是应该加1的,但是实际并没有这样做。

  编译器在进行优化时考虑到了这点,为了弥补后面丢失的小数精度,所以在最终生成代码时就统一都给加了一个1。

  第二种加1的问题解决了,思维活跃的读者这时应该可以猜到第一种情况也是精度问题了。事实也正像读者们所想象的那样,将原定点表示法计算出来的值乘以一个2的倍数,就是出于精度的考虑。

  很明显的,当我们试图用定点表示法表示越小的小数时,其精度问题就越明显,就拿一个4位的定点小数来讲,其最小精度为0.0625,也就是说如果我们想表示0.05的话是不行的,那么除了增加位数以外,还有没有什么其他方法呢?答案是肯定的!

  做法很简单,我们只需将0.05乘以5,那么我们就可以用0x4来表示它了,而且精度一丁点也没丢失,再次使用的时候只需再将其除以5即可。

  不过令人沮丧的,上面的思路虽然原理是对的,但是实际情况要复杂一些,因为编译器最终是要将除法优化掉的,所以我们必须保证在数据还原时不能出现除法,这很明显要用到位移了,而位移的特点是只能转换除数为2的次方的除法,因此这就需要我们保证我们在变幻小数时将其增加的倍数也必须是2的次方。

  我们拿1876523938/876523938这个例子来讲,这次我们要将其乘以4以期减少误差,其运算逻辑如下:

  由此可见,这种计算前增加倒数值(称之为‘倒数向上圆整’)后,在将结果减去对应倍数的值(称之为‘商向下圆整’)的方法可以比较完美的解决精度问题。

  到此,我们有关于除法逆向的学习就可以告一段落了,下面我们就趁热打铁,再看看另一个与除法相关的知识点。

  事实上当我们明白了除非运算的优化原理后,在理解取模运算就非常简单了,取模运算只不过是在除法运算的基础上再加一步而已。

  那么即便是如此,我们还是有必要说一说,先看源码:

  简单至极的源码呀!想必编译出来的机器码也是如此,Debug版的肯定更加友好:

  既然Debug版简单的让人窒息,那么Release版势必也不会太难了:

  非常顺利的完成了取模运算的学习,最后在总结一下特点:

  (除数=XXXXXXXXh*2^-32 如果倒数被向上圆整了,那么根据sar指令后的数值向下圆整即可)

  到这里,有关于本小节的所有内容就全部结束了,虽然本小节洋洋洒洒的写了近2万字,但是这已经是笔者精简了近40多小时的结果了,虽不敢说字字珠玑,但也应该算得上是华秩之章了。