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

注:本文中的算法来自于 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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/506410.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

left join 效率_人力资源HR的人才测评工具,极大提高招聘效率

作为一个HR小白&#xff0c;打交道最多的就是简历&#xff0c;领导谈的最多的就是提高工作效率&#xff0c;其实这个概念对于我来说还是挺抽象的&#xff0c;经过向前辈的取经&#xff0c;人力资源如何提高效率&#xff0c;做了个小小的总结。首先我们要明白我们的难点有哪些&a…

windows media player 9_openmeetings(开源视频会议系统)的详细安装步骤 (windows版)

一、开源视频会议系统openmeetings的简介&#xff1a;OpenMeetings是一个多语言可定制的视频会议和协作系统。它支持音频、视频&#xff0c;能让你查看每个与会者的桌面。OpenMeetings还包含一个白板&#xff0c;通过白板可以导入各种格式的图片和涂鸦。它是基于OpenLaszlo’s的…

Java实例化后自动执行_Java的实例化顺序(程序执行顺序)

加载/执行顺序&#xff1a;牢记一点&#xff1a;静态和非静态分开处理使用到静态加载时&#xff0c;静态又分为&#xff1a; 静态变量&#xff0c; 静态代码块&#xff0c; 其中加载顺序是按照类中书写的先后顺序加载的非静态加载顺序&#xff1a; 按照非静态书写顺序加载/执行…

为什么java中floatda正确_为什么cast to float在java中产生正确的结果?

文档没有特别好地解释,但Double.toString(double)基本上在它产生的输出中执行一些舍入. Double.toString算法在整个Java SE中使用,包括例如System.out的PrintStream.println(double).文档说明了这一点&#xff1a;How many digits must be printed for the fractional part of …

war包怎么解压_渣渣辉表情包下载-渣渣辉抖音表情包动态图下载

详情介绍渣渣辉表情包怎么制作&#xff1f;渣渣辉表情包gif在线生成工具应用安装资源推荐给大家&#xff0c;这是一款最近抖音上相当火爆的趣味聊天必备神器。这里各种各样的动态表情包应有尽有&#xff0c;你喜欢的表情包这里都有。感兴趣的小伙伴赶紧下载体验吧&#xff01;关…

不足补0 java_Java Android 开发数字不足位数前面补0

import java.text.decimalformat;public void changecolor(view view) {decimalformat decimalformat new decimalformat("000");//获取随机数对象&#xff0c;产生三个随机数值(rgb值)random x new random();int red x.nextint(256);string sred decimalformat.…

python数据分析_使用Python进行数据分析

麦金尼编写的《使用Python进行数据分析》是最经典的数据分析教材&#xff0c;本专栏主要应用视频讲解的方式&#xff0c;讲授本书核心思想&#xff0c;并提供本教材的所有代码和代码点评。注意&#xff1a;视频网址必须用电脑台式机打开&#xff01;手机访问不到视频。注意&…

java.policy无法修改_如何配置Policy文件进行Java安全策略的设置

中国人最喜欢访问的网站只要注册ofo就送你10块钱&#xff0c;还等什么&#xff0c;快来注册吧Java语言具有完善的安全框架&#xff0c;从编程语言、编译器、解释程序到Java虚拟机&#xff0c;都能确保Java系统不被恶意的代码或敌对的编译器暗中破坏&#xff0c;它们能够保证Jav…

百度原创度在线检测_资深自媒体作者:做自媒体没有这款“原创度检测”软件是不行的...

目前市面上除了今日头条自媒体平台外&#xff0c;好友好多好多其它的自媒体平台&#xff0c;比如百度的百家号、360的快船号、腾讯的企鹅号等等&#xff0c;各种各样的自媒体平台&#xff0c;如此多的自媒体平台大家每天写作的资源和话题也就那么多&#xff0c;免不了有些人就会…

如何看懂串口通讯协议_一文看懂PLC的通讯方式——AB系统(一)

写在前面一直以来&#xff0c;PLC跟其他设备的通讯方式都是自动化工程师入门学习的难点和要点。说它难&#xff0c;因为这里面牵扯到了数据通讯的一些知识&#xff0c;大多数从事PLC工作的人员都是从电气、电子、自动化、机电等相关专业出身的&#xff0c;可能上学时候也只是简…

linux mysql 实战_linux实用实战

1、编译安装搭建wordpress软件环境&#xff1a;apr-1.6.2.tar.gzphp-7.1.10.tar.xz http://php.net/mariadb-10.2.8-linux-x86_64.tar.gz http://mariadb.org/wordpress-4.8.1-zh_CN.tar.gz https://cn.wordpress.org/1 、源码编译安装Httpd2.4(1)安装包组和包yum groupinstall…

java不需要返回数据时_从Java方法返回时,BigDecimal不保持实际值

我正在用Java制作货币转换应用程序.其他一些很棒的StackOverflowians给了我建议读取BigDecimal,目的是替换double来解决任何精度问题.我有两个方法系统;它从起始货币转换为美元,然后将美元价值转换为目标货币.请注意,我的转化率存储如下&#xff1a;// Conversion Rates - STAR…

openstack架构详解图_英特尔顶级技术专家合力缔造精品:Linux开源网络全栈详解...

日常水开篇自1991年诞生起&#xff0c;Linux已经走过了接近三十年。Linux早已没有了问世时的稚气&#xff0c;正在各个领域展示自己成熟的魅力。以Linux为基础&#xff0c;也衍生出了各种开源生态&#xff0c;例如网络和存储。而生态离不开形形色色的开源项目&#xff0c;在人人…

java多次点击时事件_click事件的累加绑定,绑定一次点击事件,执行多次

我的github(PS:希望star):https://github.com/thWinterSun/v-admin最近做项目为一个添加按钮绑定点击事件&#xff0c;很简单的一个事情&#xff0c;于是我按照通常做法找到元素&#xff0c;使用jquery的on()方法为元素绑定了点击事件&#xff0c;点击同时发送请求。完成后看效…

基于matlab的fisher线性判别及感知器判别_基于嵌入表示的网络实体对齐方法进展概述...

网络实体对齐是指给定两个网络&#xff0c;把两个网络中等价的实体合并。实体对齐在很多领域都有重要应用&#xff0c;比如&#xff0c;跨平台社交网络的用户对齐可以用于用户画像、用户兴趣挖掘&#xff0c;跨语言知识图谱的实体对齐可以辅助机器翻译、跨语言信息检索。传统的…

回调java 简书_Java接口回调机制详解【转】

一、回调的含义和用途1. 什么是回调&#xff1f;一般来说&#xff0c;模块之间都存在一定的调用关系&#xff0c;从调用方式上看&#xff0c;可以分为三类&#xff1a;同步调用、异步调用和回调。同步调用是一种阻塞式调用&#xff0c;即在函数A的函数体里通过书写函数B的函数名…

mfc指示灯报警显示_奔驰车辆常用指示灯功能讲解

点击箭头处“蓝色字”&#xff0c;关注我们哦&#xff01;&#xff01;今天将为大家解析车辆仪表盘指示灯功能&#xff0c;当然&#xff0c;我们为大家带来的是奔驰车型的指示灯解读。奔驰在指示灯上的设计是什么风格呢&#xff1f;下面我们就来带大家详细解读。疲劳驾驶报警指…

java无锁消费者框架_无锁并行框架多生产者多消费者模型

下面看一下多生产多消费者的模式&#xff0c;下面的代码是模拟100个生产者&#xff0c;每个生产者生产100个事件&#xff0c;然后有3个消费者&#xff0c;同时进行消费&#xff0c;共消费1W个事件&#xff0c;下面看一下代码&#xff1a;这边new出了3个消费者&#xff0c;并把消…

win10无法连接到这个网络_电脑无法连接网络怎么办?(有线网络篇)

哈喽&#xff01;各位小伙伴大家好呀&#xff01;好久不见&#xff0c;近期真的太多小伙伴咨询网络问题了&#xff0c;没办法&#xff0c;虽然以前写过相关文章&#xff0c;断网、网络卡、慢、掉线的常规解决方法不过可能还不够细致&#xff0c;本篇就尽量把所有的问题都提出解…

LeetCode反转链表java_Leetcode 反转链表系列 图解详细过程

对于一个程序猿来说&#xff0c;数据结构和算法的重要性就不用我多说了吧&#xff0c;算法题已然成了现在大厂笔试面试的重头戏&#xff0c;废话少说&#xff0c;Leetcode 刷起来呀。说起刷 Leetcode&#xff0c;我建议你按 tag 刷&#xff0c;不然只能像无头苍蝇&#xff0c;东…