在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。这里总结下优缺点。

优点:

  1. 使用方便,只需要导入头文件
  2. 在除数未知的情况下能获得显著的性能提升
  3. 能利用SIMD,充分释放现代cpu性能

缺点:

  1. 只适用于除数未知的情况下
  2. 且除数要固定,因为频繁创建销毁libdivide::divider对象要付出额外的代价,会导致优化效果打折甚至负优化
  3. libdivide::divider对象比整数要多占用一个字节,尽管这个对象是栈分配的,但对空间消耗比较敏感的程序可能需要谨慎使用,尤其是账面是虽然只多一字节,但遇到需要内存对齐的时候可能占用就要翻倍了。

总得来说libdivide还是很值得一试的库,但任何优化都要有性能测试做依据。

使用libdivide加速整数除法运算的更多相关文章

  1. Java 整数间的除法运算如何保留所有小数位?

      1.情景展示 double d = 1/10; System.out.println(d); 返回的结果居然是0.0!这是怎么回事儿? 2.原因分析 第一步:你会发现用运算结果也可以用int类型接 ...

  2. java 除法运算只保留整数位的3种方式

      1.情景展示 根据提供的毫秒数进行除法运算,如果将毫秒数转换成小时,小时数不为0,则只取整数位,依此类推... 2.情况分析 可以使用3个函数实现 Math.floor(num)  只保留整数位 ...

  3. Numpy 基本除法运算和模运算

    基本算术运算符+.-和*隐式关联着通用函数add.subtract和multiply 在数组的除法运算中涉及三个通用函数divide.true_divide和floor_division,以及两个对应 ...

  4. sql server 中进行除法运算时,如何得到结果是小数形式呢?

    我们正常进行除法运算时,sql默认是返回一个四舍五入的数 比如12除以5,17除以3 --算法1:返回结果:2 需要的是2.40 ) as 结果1 --算法2:返回结果:5 需要的是5.67 ) as ...

  5. 编译器是如何实现32位整型的常量整数除法优化的?[C/C++]

    引子 在我之前的一篇文章[ ThoughtWorks代码挑战——FizzBuzzWhizz游戏 通用高速版(C/C++ & C#) ]里曾经提到过编译器在处理除数为常数的除法时,是有优化的,今 ...

  6. BigDecimal除法运算出现java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result的解决办法

    BigDecimal除法运算出现java.lang.ArithmeticException: Non-terminating decimal expansion; no exact represent ...

  7. ruby 除法运算

    在Ruby中根据运算对象的值的不同进行不同的操作.除法运算符"/"的两边同为Interger对象时运算符进行整除运算,其中任意一方为Float对象时进行实数的除法运算. 7 / 2 ...

  8. C/C++整数除法以及保留小数位的问题

    题目描述 Given two postive integers A and B,  please calculate the maximum integer C that C*B≤A, and the ...

  9. FPGA中的除法运算及初识AXI总线

    FPGA中的硬件逻辑与软件程序的区别,相信大家在做除法运算时会有深入体会.硬件逻辑实现的除法运算会占用较多的资源,电路结构复杂,且通常无法在一个时钟周期内完成.因此FPGA实现除法运算并不是一个&qu ...

  10. SQL0419N 十进制除法运算无效,因为结果将有一个负小数位。 SQLSTATE=42911

    select case when sum(qty_sold*u.um03/u.um08) <> 0 then decimal(coalesce(sum(d.amt_sold_with_ta ...

随机推荐

  1. 使用FishSpeech进行语音合成推理

    部署 部署FishSpeech,优先参考github官方(https://speech.fish.audio/zh/). 注意:此网站可能需要FQ才能访问.   个人为Windows电脑,使用Wind ...

  2. Typecho 如何开启外链转内链

    把博客中的外部链接转换为网站内链,据说有利于搜索引擎收录.该插件主要由 benzBrake 大佬 编写,同时支持转换文章和评论中的链接. 上传插件 下载 Master Branch Code 后上传到 ...

  3. JavaGUI - [04] BoxLayout

    题记部分 一.简介   为了简化开发,Swing引入了一个新的布局管理器:BoxLayout.BoxLayout可以在垂直和水平两个方向上摆放GUI组件,BoxLayout提供了如下一个简单的构造器: ...

  4. Shell - [11] 开源Apache Zookeeper集群启停脚本

    一.集群角色部署 当前有Zookeeper集群如下 主机名 ctos79-01 ctos79-02 ctos79-03 Zookeeper ○ ○ ○ 二.脚本使用 三.脚本内容 #!/bin/bas ...

  5. Hadoop - 两个Namenode都是standby状态怎么处理

    在任意一个standby的NN节点执行 再次访问 ctos01:9870页面

  6. 读论文-基于会话的推荐系统综述(A survey on session-based recommender systems)

    前言 今天读的论文是一篇于2021年发表于"ACM Computing Surveys (CSUR)"的论文,文章写到,推荐系统在信息过载时代和数字化经济中非常重要.基于会话的推荐 ...

  7. 跨平台Windows和Linux(银河麒麟)操作系统OCR识别应用

    1 运行效果 代码下载链接: https://pan.baidu.com/s/1NUfLTjk6kzXJKsaH7yo4qA?pwd=rk5c 提取码: rk5c. 在银河麒麟桌面操作系统V10(SP ...

  8. excel怎么根据数值做进度条

    开始->条件格式->数据条

  9. 【MIPS】经典指令块集锦

    Directives声明变量值存储 容易将数据段地址和地址上的内容搞混 .data fibs: .space 48 # allocate 12 * 4 = 48 Byte memory, store ...

  10. Web前端入门第 7 问:HTML 标签不闭合、乱闭合、只有闭合标签有没有什么问题?

    HTML 标签语法遵循层级嵌套的树形结构,如果写出来的代码不是树形结构,浏览器会怎么渲染? 注意:以下截图都来源于 Chrome 浏览器,不同浏览器可能会产生不同的渲染结果. 先看正常代码 <s ...