注:本文中的算法来自于 Division by Invariant Integers using Multiplication [1]


众所周知,编译器可以把变量除以常量优化为乘法和移位。 例如:

Uint32 f(Uint32 a) { return a / 3; }

会生成下面这样的汇编(x86_64):

f:mov eax, edimov edx, 2863311531imul rax, rdxshr rax, 33ret

你一定很好奇,这个 2863311531 是怎么得到的?在这之前,我们需要先准备一些预备知识。

首先我们需要知道,对于常见的指令集,都提供了N位整数乘以N位整数得到2N位结果的指令。以32位为例,对于 x86 指令集来说:

  • mul r/m32:计算 eaxr/m32 进行无符号乘法的结果,将低32位存入 eax,高32位存入 edx
  • imul r/m32:计算 eaxr/m32 进行有符号乘法的结果,将低32位存入 eax,高32位存入 edx

对于 ARM 指令集来说:

  • umull RdLo, RdHi, Rm, Rs:计算 RmRs 进行无符号乘法的结果,将低32位存入 RdLo,高32位存入 RdHi
  • smmul Rd, Rm, Rs:计算 RmRs 进行有符号乘法的结果,将高32位存入 Rd

我们可以用下面两个函数代表计算乘法高位的结果:

Uint32 muluh(Uint32 a, Uint32 b) { return (Uint64(a) * b) >> 32; }
Int32 mulsh(Int32 a, Int32 b) { return (Int64(a) * b) >> 32; }

这些指令为我们的优化提供了可能性。 为了充分利用这些指令,我们的目标是将 n / d 优化为 muluh(n, m) >> l,写成数学表达式就是

。 那么,我们应该如何通过
计算出
呢?

定理1

是非负整数,
,且满足
,则对于
的所有整数
,有

证明

,则由题设可得
。 对于
,我们可以将
写成
的形式,其中
。 也就是说,我们需要证明

因为

,所以

所以

,得证。

换而言之,

的取值范围是
。 我们可以先令
,这样可以保证取值范围不为空,然后不断减小
,直到取值范围内只有一个整数。 这样可以使得
尽量小,并且在少数情况下可以使得
达到0,从而省略最后的移位。 (例如对于32位无符号乘法,除以641时就能省略最后的移位。)

代码如下(注:本文中均以32位乘除法为例):

constexpr int N = 32;inline int clz(Uint32 x) { return __builtin_clz(x); }struct Multiplier {Uint64 m;int l;
};Multiplier chooseMultiplier(Uint32 d) {assert(d != 0);// l = ceil(log2(d))int l = N - clz(d - 1);Uint64 low = (Uint64(1) << (N + l)) / d;Uint64 high = ((Uint64(1) << (N + l)) + (Uint64(1) << l)) / d;while((low >> 1) < (high >> 1) && l > 0)low >>= 1, high >>= 1, --l;return {high, l};
}

试验一下:

chooseMultiplier(3, 32);

得到

{2863311531, 1}

也就是说,我们可以将 n / 3 优化为

return muluh(n, 2863311531) >> 1;

然而,这个算法是存在问题的。例如,当

时,我们会得到这样的结果:
{4908534053, 3}

发现了吗?

,超过了

Uint32 能够表示的范围! 究其原因,是因为原始的范围中就只有一个整数,无法缩小,而

不过不用担心,注意到

,也就是说,
最大不会达到
。 所以我们可以利用乘法分配律将
拆成
两个部分。 其中
,可以像之前一样用

muluh 解决,而

的高位结果直接就是
自己了。 也就是说:
Uint32 t = muluh(n, m - (Uint64(1) << 32);
return (n + t) >> l;

但是这样还是有一点问题,如果

很大的话,
还是可能会溢出。 所以我们需要再进行一次变形:
Uint32 t = muluh(n, m - (Uint64(1) << 32);
return (((n - t) >> 1) + t) >> (l - 1);

也就是

。 将
代入的话,结果就是:
Uint32 t = muluh(n, 613566757);
return (((n - t) >> 1) + t) >> 2;

另外还有一种情况就是当

为偶数时,我们可以将
拆成
。 比如当
的时候,我们可以将它拆成
。 你可能会好奇,
时的
不是超过了

Uint32 的范围吗? 实际上,在前一步的

之后,我们的除法实际上只需要31位的有效精度,在这个精度下计算出的
是不会超过范围的。 加入有效精度限制后的

chooseMultiplier 如下:

Multiplier chooseMultiplier(Uint32 d, int p) {assert(d != 0);assert(p >= 1 && p <= N);// l = ceil(log2(d))int l = N - clz(d - 1);Uint64 low = (Uint64(1) << (N + l)) / d;Uint64 high = ((Uint64(1) << (N + l)) + (Uint64(1) << (N + l - p))) / d;while((low >> 1) < (high >> 1) && l > 0)low >>= 1, high >>= 1, --l;return {high, l};
}

(需要注意的是,当

的时候,

lowhigh 的计算过程中是会产生溢出的,但这种情况下除法的结果只可能是0或1,可以直接用比较解决,所以这里不做考虑。)

调用

chooseMultiplier(7, 31);

的结果是

{2454267027, 2}

所以 n / 14 可以被优化为:

return muluh(n >> 1, 2454267027) >> 2;

当然,众所周知,如果

可以写作
的形式的话,那么除法就可以直接优化为一个移位了。

另外需要注意的是,如果按完整精度计算出的

没有达到
的话,就没有必要进行这个操作,否则反而可能会使得结果更差。 例如对于
来说,直接计算的结果

muluh(n, 2863311531) >> 2 优于先除以二的结果 muluh(n >> 1, 2863311531) >> 1(后者多了一次移位)。

结合上述几种情况,完整代码如下:

#include <cassert>
#include <initializer_list>
#include <iostream>using Uint32 = unsigned int;
using Uint64 = unsigned long long;
using Int32 = int;
using Int64 = long long;inline int clz(Uint32 x) { return __builtin_clz(x); }
inline int ctz(Uint32 x) { return __builtin_ctz(x); }Uint32 muluh(Uint32 a, Uint32 b) { return (Uint64(a) * b) >> 32; }
Int32 mulsh(Int32 a, Int32 b) { return (Int64(a) * b) >> 32; }constexpr int N = 32;struct Multiplier {Uint64 m;int l;
};Multiplier chooseMultiplier(Uint32 d, int p) {assert(d != 0);assert(p >= 1 && p <= N);// l = ceil(log2(d))int l = N - clz(d - 1);Uint64 low = (Uint64(1) << (N + l)) / d;Uint64 high = ((Uint64(1) << (N + l)) + (Uint64(1) << (N + l - p))) / d;while((low >> 1) < (high >> 1) && l > 0)low >>= 1, high >>= 1, --l;return {high, l};
}void generateUnsignedDivision(Uint32 d) {assert(d != 0);std::cout << "Uint32 div" << d << "(Uint32 n) {n";if(d >= (Uint32(1) << (N - 1))) {std::cout << "    return n >= " << d << ";n";} else {int s = ctz(d);if(d == (Uint32(1) << s)) {std::cout << "    return n";if(s > 0) std::cout << " >> " << s;std::cout << ";n";} else {Multiplier multiplier = chooseMultiplier(d, N);if(multiplier.m < (Uint64(1) << N)) s = 0;else multiplier = chooseMultiplier(d >> s, N - s);if(multiplier.m < (Uint64(1) << N)) {std::cout << "    return muluh(n";if(s > 0) std::cout << " >> " << s;std::cout << ", " << multiplier.m << ")";if(multiplier.l > 0) std::cout << " >> " << multiplier.l;std::cout << ";n";} else {std::cout << "    Uint32 t = muluh(n, " << (multiplier.m - (Uint64(1) << N)) << ");n";std::cout << "    return (((n - t) >> 1) + t) >> " << (multiplier.l - 1) << ";n";}}}std::cout << "}n";
}int main() {for(Uint32 d : std::initializer_list<Uint32>{1, 3, 6, 7, 14, 31, 32, 641, 0x7FFFFFFF, 0x80000000})generateUnsignedDivision(d);
}

下面展示了该代码生成的各种情况的优化结果:

Uint32 div1(Uint32 n) {return n;
}
Uint32 div3(Uint32 n) {return muluh(n, 2863311531) >> 1;
}
Uint32 div6(Uint32 n) {return muluh(n, 2863311531) >> 2;
}
Uint32 div7(Uint32 n) {Uint32 t = muluh(n, 613566757);return (((n - t) >> 1) + t) >> 2;
}
Uint32 div14(Uint32 n) {return muluh(n >> 1, 2454267027) >> 2;
}
Uint32 div31(Uint32 n) {Uint32 t = muluh(n, 138547333);return (((n - t) >> 1) + t) >> 4;
}
Uint32 div32(Uint32 n) {return n >> 5;
}
Uint32 div641(Uint32 n) {return muluh(n, 6700417);
}
Uint32 div2147483647(Uint32 n) {Uint32 t = muluh(n, 3);return (((n - t) >> 1) + t) >> 30;
}
Uint32 div2147483648(Uint32 n) {return n >= 2147483648;
}


这回讲了无符号除法的优化,关于有符号除法,我们下次再说。

参考

  1. ^Torbjörn Granlund and Peter L. Montgomery. 1994. Division by invariant integers using multiplication. SIGPLAN Not. 29, 6 (June 1994), 61–72. https://dl.acm.org/doi/10.1145/773473.178249

无符号有符号乘法_【编译笔记】变量除以常量的优化(一)——无符号除法相关推荐

  1. aardio学习笔记-变量与常量

    变    量 定义:在程序运行过程中,用来存储数据值并且其值能被改变的对象称为变量. 要求: 1.变量名开始字符不能为数字. 2.变量名包含中文时,中文字符前面不能有字母或数字. 3.可以使用美元符号 ...

  2. 位向量 补码与无符号 加法与乘法 CSAPP学习笔记

    计算机中用位来表示整数,一种方式只能表示非负数,一种可以表示有符号数. 无符号数编码: 补码编码: 由上面的定义可以知道补码与无符号之间的对应关系(见下式),最高位为0时,补码与无符号表示是一样的,而 ...

  3. java 常量字符串过长_编译出错:对于常量池来说,字符串表示的UTF过长,那我想知道,JVM的常量池到底有多大?...

    输入缓冲说是8000个字符,和这有关吗? String内部是以char数组的形式存储,数组的长度是int类型,那么String允许的最大长度就是Integer.MAX_VALUE了,214748364 ...

  4. php 双引号 常量,php易错笔记-变量,常量,运算符

    变量 基本 $4site = 'not yet'; // 非法变量名:以数字开头 $_4site = '_4site'; // 合法变量名:以下划线开头 $i站点is = 'mansikka'; // ...

  5. python常量变量和对象_Python学习笔记——变量和常量

    一.变量 变量的概念基本上和初中代数的方程变量是一致的,只是在计算机程序中,变量不仅可以是数字,还可以是任意数据类型. 在Python中,不需要事先声明变量名和类型,直接赋值即可创建各种类型的对象变量 ...

  6. Verilog学习笔记——有符号数的乘法和加法

    有符号数的计算在 Verilog 中是一个很重要的问题(也很容易会被忽视),在使用 Verilog 语言编写 FIR 滤波器时,需要涉及到有符号数的加法和乘法,在之前的程序中我把所有的输入输出和中间信 ...

  7. 负数与正数相乘怎么算_负数乘法_正负符号_两个正负符号的规则_乘法表

    做乘法时: 例子 × 正正得正: 3 × 2 = 6 ×   负负得正 (−3)× (−2) = 6 × 负正得负: (−3)× 2 = −6 × 正负得负: 3 ×(−2) = −6 是真的,负负得 ...

  8. 有符号二进制数的乘法

    最近在阅读<深入理解计算机系统>讲到补码乘法,书上给了一个例子是三位无符号和补码的乘法表.其中两个负数的例子:3位二进制乘法结果一般需要6为二进制表达 带符号数 x=101=-3 和y=0 ...

  9. 假设我们在对有符号值使用补码运算的32位机器人运行代码。对于有符号值使用的是算术右移,而对于无符号值使用的是逻辑右移

    假设我们在对有符号值使用补码运算的32位机器人运行代码.对于有符号值使用的是算术右移,而对于无符号值使用的是逻辑右移.变量的声明和初始化如下: int x = foo(); //任意值 int y = ...

最新文章

  1. RunnableException与CheckedException
  2. Matlab与线性代数 -- 魔方矩阵
  3. 芯片如何储存信息_十四五规划之:芯片
  4. 关于Class之深入Class
  5. html录音并转为音频文件,HTML5音频API Web Audio
  6. netty worker线程数量_Dubbo线程模型
  7. jquery 样式获取设置值_[JQuery] jQuery选择器ID、CLASS、标签获取对象值、属性、设置css样式...
  8. 验证子串(信息学奥赛一本通-T1140)
  9. CTS ( 9)---CTS 源码分析
  10. 基于JAVA+Swing+MYSQL的工资管理系统
  11. 计算机证据的获取,计算机证据获取技术的研究与应用.pdf
  12. linux防止文件和目录被意外删除或修改
  13. 基于数字孪生高校可视化的综合运营管理平台
  14. ps在线版 Photoshop在线精简版-toolfk程序员在线工具网
  15. Elasticsearch 分布式搜索引擎 速学
  16. 让用户输入一个数,判断7的倍数
  17. 如何将PDF转成PPT?为什么转换后不能编辑
  18. html图片靠右浮动 文字左侧环绕,CSS 模拟float实现center文字左右环绕图片的效果...
  19. AEJoy —— 彻底搞懂 AE 各种 loop* 表达式【二】
  20. (一)Activiti 数据库25张表——流程历史记录表25(ACT_EVT_LOG)

热门文章

  1. matplotlib调整图例的位置
  2. Excel制作随机抽取名单
  3. TensorFlow报错run() got multiple values for argument 'feed_dict'
  4. C++学习之路(一)
  5. windows 下 Graphviz 安装及入门教程以及 PlantUML
  6. Struts2源码阅读(四)_DispatcherConfigurationProvider续
  7. php进程间通信 yoc_PHP 进程间通信各种通信方式间的优劣之分??
  8. google账号解除游戏绑定_成长守护平台解除实名认证 公众号解绑操作流程
  9. Python序列类型常用函数练习:enumerate() reversed() sorted() zip()
  10. 计算机专业性特有的道德要求,什么是通信科学技术人员职业道德的双重性?