使用libdivide加速整数除法运算
在x86和ARM平台上,整数除法是相对较慢的操作。不巧的是除法在日常开发中使用频率并不低,而且还有一些其他常用的运算依赖于除法操作,比如取模。因此频繁的除法操作很容易成为程序的性能瓶颈,尤其是在一些数值计算程序里。
人们当然也想了很多办法优化,比如在除数是2的幂的时候,除法可以用速度更快的位运算来替换。比较新的编译器都会自动进行这类优化。然而不是所有的除数都是2的幂,也不是所有表达式里的除数在编译期间可知,因此我们还需要一些别的手段。

libdivide就是这样一种整数除法的优化手段,它不仅能应用前面提到的位运算优化,它还可以在运行时根据除数和被除数的特性选择速度最快的算法来模拟除法操作,最有意义的地方在于如果硬件平台支持SIMD指令,它还会在条件允许下尽量使用SSE2/AVX2/AVX256/NEON等SIMD指令做优化,重复发挥了现代cpu的性能优势。按照项目文档的说法,在有SIMD的加持下,64位整数的除法运算最大可以提升10倍。
libdivide是个头文件库,这意味着只需要简单包含一下它提供的头文件就可以使用了,不需要额外的编译和安装。同时它的使用方法也很简单,库做了运算符重载,只要初始化一下它需要的对象就可以了:
int64_t a = 1000;
libdivide::divider<int64_t> fast_d(10);
int64_t result = a / fast_d;
a /= fast_d;
c语言没有提供运算符重载的功能,因此得用libdivide_s64_gen等包装函数来完成相同的操作。
简单来说它几乎可以平替项目中的除法运算,不过在这之前我们得先检验下它承诺的性能提升是不是确有其事。
libdivide在测试用例里自带了一个性能测试程序,但这个程序的输出比较抽象,测试的场景也不够全面,因此我重新写了三个场景的场景做测试。
三个场景分别是除数未知、除数已知但是2的幂以及除数已知但不是2的幂。覆盖的情况是编译器做不了优化、编译器可以优化成位运算以及编译器能优化但不能直接用位运算进行替换这三种。
由于c++的编译期计算太强大,为了避免编译期计算搅局影响结果,测试数据中的大部分都是随机生成的,理论上性能测试中应该尽量减少这种随机生成的数据,但这里我们别无他法,而且说到底也是“评估”一下库的大致性能,不需要那么精确。测试用例不长,因此我就全搬上来了:
// 场景1,除数未知,为了模拟除数未知所以除数也用了随机数
void bench_div(benchmark::State &stat)
{
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<int64_t> dis(1, 114515);
std::vector<int64_t> v;
for (int i = 0; i < 10; ++i) {
v.push_back(dis(gen));
}
int64_t d = dis(gen);
for (auto _ : stat) {
for (auto n : v) {
benchmark::DoNotOptimize(n/=d);
}
}
}
BENCHMARK(bench_div);
void bench_libdiv(benchmark::State &stat)
{
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<int64_t> dis(1, 114515);
std::vector<int64_t> v;
for (int i = 0; i < 10; ++i) {
v.push_back(dis(gen));
}
libdivide::divider<int64_t> fast_d(dis(gen));
for (auto _ : stat) {
for (auto n : v) {
benchmark::DoNotOptimize(n/=fast_d);
}
}
}
BENCHMARK(bench_libdiv);
// 场景2,除数的4,2的2次幂
void bench_div4(benchmark::State &stat)
{
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<uint64_t> dis(1, 114515);
std::vector<uint64_t> v;
for (int i = 0; i < 10; ++i) {
v.push_back(dis(gen));
}
uint64_t d = 4;
for (auto _ : stat) {
for (auto n : v) {
benchmark::DoNotOptimize(n>>=d);
}
}
}
BENCHMARK(bench_div4);
void bench_libdiv4(benchmark::State &stat)
{
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<uint64_t> dis(1, 114515);
std::vector<uint64_t> v;
for (int i = 0; i < 10; ++i) {
v.push_back(dis(gen));
}
uint64_t d = 4;
libdivide::divider<uint64_t> fast_d(d);
for (auto _ : stat) {
for (auto n : v) {
benchmark::DoNotOptimize(n/=fast_d);
}
}
}
BENCHMARK(bench_libdiv4);
// 场景3,除数已知但不是2的幂,特地选了一个素数23
void bench_div_const(benchmark::State &stat)
{
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<int64_t> dis(1, 114515);
std::vector<int64_t> v;
for (int i = 0; i < 10; ++i) {
v.push_back(dis(gen));
}
int64_t d = 23;
for (auto _ : stat) {
for (auto n : v) {
benchmark::DoNotOptimize(n/=d);
}
}
}
BENCHMARK(bench_div_const);
void bench_libdiv_const(benchmark::State &stat)
{
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<int64_t> dis(1, 114515);
std::vector<int64_t> v;
for (int i = 0; i < 10; ++i) {
v.push_back(dis(gen));
}
libdivide::divider<int64_t> fast_d(23);
for (auto _ : stat) {
for (auto n : v) {
benchmark::DoNotOptimize(n/=fast_d);
}
}
}
BENCHMARK(bench_libdiv_const);
BENCHMARK_MAIN();
测试内容是连续除十个随机生成的被除数,现代cpu性能还是很强悍的,如果只测除一次的情况,那么会得到一堆0.X纳秒的结果,那样对比不够明显,也容易引入统计误差和噪音。
测试运行也分两部分,一是使用-O2优化级别进行测试,在这个级别下编译器会采用比较保守的优化策略,并且只应用少量的SIMD指令;另一个是用-O3 -march=native进行优化,在这个级别下编译器会最大限度优化程序性能并且尽可能利用当前cpu上所有可用的指令(包括SIMD)进行优化。
先来看看老机器上(10代i5台式机)的结果:

下面是开启native之后的结果:

结果符合预期,在除数未知的情形下libdivide性能提升了8倍左右,除数已知且是2的幂的时候两者差不多,只有第三种情形下libdivide稍慢与直接除,原因大概是因为编译器也做了和libdivide类似的优化,但libdivide还需要额外探测除数的性质以及需要多几次函数调用,因此性能上稍慢了一些。
最大化利用SIMD结果类似,情形3下的差距缩小了很多。
然后我们看看在更新的机器上的表现(14代i7):

不启用最高级别优化时结果与老机器类似,但性能差距缩小了。

最大程度利用SIMD后现在情形3变快,但场景2又稍显落后了。在场景1中的提升也只有5倍左右。
总体来说在libdivide宣称的场景下,性能提升确实很可观,但还没到1个数量级这么夸张,不过我的测试环境都没有avx512支持,对于支持这个指令集的cpu来说也许性能还能再提升一些最终达到文档里说的10倍。在其他场景下libdivide的优势并不明显,所以追求极致性能的时候不是很建议在非场景1的情况下使用这个库。
总结
如果整数除法成为了性能瓶颈的话,可以尝试使用libdivide。这里总结下优缺点。
优点:
- 使用方便,只需要导入头文件
- 在除数未知的情况下能获得显著的性能提升
- 能利用SIMD,充分释放现代cpu性能
缺点:
- 只适用于除数未知的情况下
- 且除数要固定,因为频繁创建销毁
libdivide::divider对象要付出额外的代价,会导致优化效果打折甚至负优化 libdivide::divider对象比整数要多占用一个字节,尽管这个对象是栈分配的,但对空间消耗比较敏感的程序可能需要谨慎使用,尤其是账面是虽然只多一字节,但遇到需要内存对齐的时候可能占用就要翻倍了。
总得来说libdivide还是很值得一试的库,但任何优化都要有性能测试做依据。
使用libdivide加速整数除法运算的更多相关文章
- Java 整数间的除法运算如何保留所有小数位?
1.情景展示 double d = 1/10; System.out.println(d); 返回的结果居然是0.0!这是怎么回事儿? 2.原因分析 第一步:你会发现用运算结果也可以用int类型接 ...
- java 除法运算只保留整数位的3种方式
1.情景展示 根据提供的毫秒数进行除法运算,如果将毫秒数转换成小时,小时数不为0,则只取整数位,依此类推... 2.情况分析 可以使用3个函数实现 Math.floor(num) 只保留整数位 ...
- Numpy 基本除法运算和模运算
基本算术运算符+.-和*隐式关联着通用函数add.subtract和multiply 在数组的除法运算中涉及三个通用函数divide.true_divide和floor_division,以及两个对应 ...
- sql server 中进行除法运算时,如何得到结果是小数形式呢?
我们正常进行除法运算时,sql默认是返回一个四舍五入的数 比如12除以5,17除以3 --算法1:返回结果:2 需要的是2.40 ) as 结果1 --算法2:返回结果:5 需要的是5.67 ) as ...
- 编译器是如何实现32位整型的常量整数除法优化的?[C/C++]
引子 在我之前的一篇文章[ ThoughtWorks代码挑战——FizzBuzzWhizz游戏 通用高速版(C/C++ & C#) ]里曾经提到过编译器在处理除数为常数的除法时,是有优化的,今 ...
- BigDecimal除法运算出现java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result的解决办法
BigDecimal除法运算出现java.lang.ArithmeticException: Non-terminating decimal expansion; no exact represent ...
- ruby 除法运算
在Ruby中根据运算对象的值的不同进行不同的操作.除法运算符"/"的两边同为Interger对象时运算符进行整除运算,其中任意一方为Float对象时进行实数的除法运算. 7 / 2 ...
- C/C++整数除法以及保留小数位的问题
题目描述 Given two postive integers A and B, please calculate the maximum integer C that C*B≤A, and the ...
- FPGA中的除法运算及初识AXI总线
FPGA中的硬件逻辑与软件程序的区别,相信大家在做除法运算时会有深入体会.硬件逻辑实现的除法运算会占用较多的资源,电路结构复杂,且通常无法在一个时钟周期内完成.因此FPGA实现除法运算并不是一个&qu ...
- SQL0419N 十进制除法运算无效,因为结果将有一个负小数位。 SQLSTATE=42911
select case when sum(qty_sold*u.um03/u.um08) <> 0 then decimal(coalesce(sum(d.amt_sold_with_ta ...
随机推荐
- QT5笔记: 22. 自定义代理
代理作用:在界面发生编辑时可以指定编辑所用的组件,可以沟通Model和View 自定义代理需要继承的基类和需要实现的方法 使用步骤: 继承QStyledItemDelegate,实现上面的四个方法 在 ...
- 离线环境安装nodejs及npm库i5ting_toc(超详细,手把手教学一通百通)
一.离线环境先安装nodejs 1.在可联网的电脑上下载特定版本的 Node.js: 访问 Node.js 官方下载页面(https://nodejs.org/download/release/) ...
- Java - 高射炮打蚊子(第二弹)
题记部分 01 || 面试题 001 || 什么是JVM JVM(Java虚拟机)是Java程序运行的环境,它是一个抽象的计算机,包括指令集.寄存器集.堆栈.垃圾回收等.JVM屏蔽了与具体操作系统平台 ...
- K8s - 容器编排引擎Kubernetes
什么是Kubernetes? 背景 Kubernetes 是开源的容器集群管理项目,诞生于2014年,由Google公司发起 前身Borg系统在Google内部应用了十几年,积累了大量来自生产环境的实 ...
- 添加xxx.so到环境变量里
点击查看代码 libxxx.so 文件位于 /usr/local/lib 目录下,你可以按照以下步骤操作: 创建配置文件: echo "/usr/local/lib" | sudo ...
- netcore后台服务慎用BackgroundService
在 .NET Core 开发中,BackgroundService 是一个非常方便的后台任务运行方式,但它并不适用于所有场景. BackgroundService 一时爽,并发火葬场. Backgro ...
- 强!10.8K star!推荐一款用于威胁预防、安全检测的开源监控平台,功能非常强大!!
在当今数字化时代,网络安全威胁日益严峻,企业和组织对于高效.强大的安全解决方案的需求也愈发迫切. 今天给大家分享一个专注于安全监控的开源项目:Wazuh,正是应对这一挑战的有力武器,专门用于帮助个人和 ...
- Selenium 项目搭建过程
Selenium 环境搭建多多少少大概有4.5次了,每次都有东西遗忘,整理下大概的过程: 安装python,下载exe文件 C:\Users\lic\AppData\Local\Programs\Py ...
- Ubuntu如何下载nvidia驱动和Cuda Toolkit
Ubuntu如何下载nvidia驱动和Cuda Toolkit 前言 手快不小心把 nvidia 的某个东西删除了,现在不得不全部卸载后再重新安装了. 我再也不敢在不确认内容的情况下,确认删除了 ...
- 《视觉SLAM十四讲》第13讲 设计SLAM系统 回环检测线程的实现
<视觉SLAM十四讲>第13讲 设计SLAM系统 回环检测线程的实现 这个学期看完了高翔老师的<视觉SLAM十四讲>,学到了很多,首先是对计算机视觉的基本知识有了一个更加全面系 ...