java实现次方的运算_Java中对于位运算的优化以及运用与思考

引言

随着JDK的发展以及JIT的不断优化,我们很多时候都可以写读起来易读但是看上去性能不高的代码了,编译器会帮我们优化代码。之前大学里面学单片机的时候,由于内存以及处理器性能都极其有限(可能很多时候考虑内存的限制优先于处理器),所以很多时候,利用位运算来节约空间或者提高性能,那么这些优秀的思想,放到目前的Java中,是否还有必要这么做呢?我们逐一思考与验证下(其实这也是一个关于Premature optimization的界定的思考)

1. 乘法与左移位

左移一位,相当于乘以2,左移n位,相当于乘以2的n次方。

1 << 1 == 1 * 2 //true1 << n == 1 * pow(2, n) // truepublic int pow(int i, int n) {    assert n >= 0;    int result = 1;    for (int i = 0; i < n; i++) {        result *= i;    }    return result;}

看上去,移位应该比乘法性能快。那么JIT与JVM虚拟机是否做了一些优化呢?优化分为两部分,一个是编译器优化,另一个是处理器优化。我们先来看看字节码是否一致判断是否有编译优化,例如直接将乘以2优化成左移一位,来编写两个函数:

public void multiply2_1() {    int i = 1;    i = i << 1;}public void multiply2_2() {    int i = 1;    i *= 2;}

编译好之后,用javap -c来看下编译好的class文件,字节码是:

  public void multiply2_1();    Code:       0: iconst_1       1: istore_1       2: iload_1       3: iconst_1       4: ishl       5: istore_1       6: return  public void multiply2_2();    Code:       0: iconst_1       1: istore_1       2: iload_1       3: iconst_2       4: imul       5: istore_1       6: return

可以看出左移是ishl,乘法是imul,从字节码上看编译器并没有优化。那么在执行字节码转换成处理器命令是否会优化呢?是会优化的,在底层,乘法其实就是移位,但是并不是简单地左移

我们来使用jmh验证下,添加依赖:

org.openjdk.jmh    jmh-core    1.22org.openjdk.jmh    jmh-generator-annprocess    1.22site.ycsb    core    0.17.0

实现思路:

  1. 被乘数的选择:被乘数固定为1,或者是一个极小值或者极大值或者是稀疏值(转换成2进制很多位是0),测试结果没啥太大的参考意义,所以我们选择2的n次方减某一数字作为被乘数
  2. 乘数生成的性能损耗:乘数是2的随机n次方,生成这个的方式要一致,我们这里要测试的仅仅是移位还有乘法运算速度,和实现复杂度没有关系。 实现代码:
@Benchmark@Warmup(iterations = 0)@Measurement(iterations = 300)public void multiply2_n_shift_not_overflow(Generator generator) {    int result = 0;    int y = 0;    for (int j = 0; j < generator.divide.length; j++) {        //被乘数x为2^n - j        int x = generator.divide[j] - j;        int ri = generator.divide.length - j - 1;        y = generator.divide[ri];        result += x * y;        //为了和移位测试保持一致所以加上这一步        result += y;    }}@Benchmark@Warmup(iterations = 0)@Measurement(iterations = 300)public void multiply2_n_mul_not_overflow(Generator generator) {    int result = 0;    int y = 0;    for (int j = 0; j < generator.divide.length; j++) {        int x = generator.divide[j] - j;        int ri = generator.divide.length - j - 1;        //为了防止乘法多了读取导致性能差异,这里虽然没必要,也读取一下        y = generator.divide[ri];        result += x << ri;        //为了防止虚拟机优化代码将上面的给y赋值踢出循环,加上下面这一步        result += y;    }}

测试结果:

Benchmark                 Mode  Cnt         Score         Error  UnitsBitUtilTest.multiply2_n_mul_not_overflow    thrpt  300  35882831.296 ±  48869071.860  ops/sBitUtilTest.multiply2_n_shift_not_overflow  thrpt  300  59792368.115 ±  96267332.036  ops/s

可以看出,左移位相对于乘法还是有一定性能提升的

2. 除法和右移位

这个和乘法以及左移位是一样的.直接上测试代码:

@Benchmark@Warmup(iterations = 0)@Measurement(iterations = 300)public void divide2_1_1(Generator generator) {    int result = 0;    for (int j = 0; j < generator.divide.length; j++) {        int l = generator.divide[j];        result += Integer.MAX_VALUE / l;    }}@Benchmark@Warmup(iterations = 0)@Measurement(iterations = 300)public void divide2_1_2(Generator generator) {    int result = 0;    for (int j = 0; j < generator.divide.length; j++) {        int l = generator.divide[j];        result += Integer.MAX_VALUE >> j;    }}

结果:

Benchmark                                    Mode  Cnt         Score           Error  UnitsBitUtilTest.divide2_n_div                   thrpt  300  10219904.214 ±   5787618.125  ops/sBitUtilTest.divide2_1_shift                 thrpt  300  44536470.740 ± 113360206.643  ops/s

可以看出,右移位相对于除法还是有一定性能提升的

3. “取余”与“取与”运算

对于2的n次方取余,相当于对2的n次方减一取与运算,n为正整数。为什么呢?通过下图就能很容易理解:

十进制中,对于10的n次方取余,直观来看就是:

fbe2393145483e0b3a25693ed1b7cdbe.png

其实就是将最后n位取出,就是余数。 对于二进制,是一样的:

8566ba3af1ea79f016fb29bdd6f94d8f.png

这个运算相当于,对于n-1取与:

e38ffaa4ff794969ebc1b04cb16befd1.png

这个是一个很经典的位运算运用,广泛用于各种高性能框架。例如在生成缓存队列槽位的时候,一般生成2的n次方个槽位,因为这样在选择槽位的时候,就可以用取与代替取余;java中的ForkJoinPool的队列长度就是定为2的n次方;netty中的缓存池的叶子节点都是2的n次方,当然这也是因为是平衡二叉查找树算法的实现。

我们来看下性能会好多少:

@Benchmark@Warmup(iterations = 0)@Measurement(iterations = 300)public void mod2_n_1(Generator generator) {    int result = 0;    for (int j = 0; j < generator.divide.length; j++) {        int l = generator.divide[j];        result += Integer.MAX_VALUE % l;    }}@Benchmark@Warmup(iterations = 0)@Measurement(iterations = 300)public void mod2_n_2(Generator generator) {    int result = 0;    for (int j = 0; j < generator.divide.length; j++) {        int l = generator.divide[j];        result += Integer.MAX_VALUE & (l - 1);    }}

结果:

Benchmark                                    Mode  Cnt         Score           Error  UnitsBitUtilTest.mod2_n_1                        thrpt  300  10632698.855 ±   5843378.697  ops/sBitUtilTest.mod2_n_2                        thrpt  300  80339980.989 ±  21905820.262  ops/s

同时,我们从这里也可以引申出,判断一个数是否是2的n次方的方法,就是看这个数与这个数减一取与运算看是否是0,如果是,则是2的n次方,n为正整数

adf1c60b63d4d9182a99bceb6a817bda.png

进一步的,奇偶性判断就是看对2取余是否为0,那么就相当于对(2-1)=1取与

4. 求与数字最接近的2的n次方

这个广泛运用于各种API优化,上文中提到,2的n次方是一个好东西。我们在写框架的很多时候,想让用户传入一个必须是2的n次方的参数来初始化某个资源池,但这样不是那么灵活,我们可以通过用户传入的数字N,来找出不大于N的最大的2的n次方,或者是大于N的最小的2的N次方。

抽象为比较直观的理解就是,找一个数字最左边的1的左边一个1(大于N的最小的2的N次方),或者是最左边的1(小于N的最大的2的N次方),前提是这个数字本身不是2的n次方。

bc418c8279d89e1919f4d2389eb3f08d.png

那么,如何找呢?一种思路是,将这个数字最高位1之后的所有位都填上1,最后加一,就是大于N的最小的2的N次方。右移一位,就是小于N的最大的2的N次方。

如何填补呢?可以考虑按位或计算,我们知道除了0或0=0以外,其他的都是1. 我们现在有了最左面的1,右移一位,与原来按位或,就至少有了两位是1,再右移两位并按位或,则至少有四位为1。。。以此类推:

a09e03ab9bedf51c6c0eba5e7e511a2c.png

用代码表示是:

n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16;n += 1;  //大于N的最小的2的N次方n = n >>> 1; //小于N的最大的2的N次方

如果有兴趣,可以看一下Java的ForkJoinPool类的构造器,其中的WorkQueue大小,就是通过这样的转换得来的。

5. 交换两个数字

这个在单片机编程中经常会使用这个位运算性质:一个数字异或自己为零,一个数字异或0为自己本身。那么我们就可以利用这个性质交换两个数字。

假设有数字x,y。 我们有x^y^y = x^(y^y)= x^0 = x 还有x^y^y^x^y = 0^y = y 那么我们可以利用:

x = x ^ y;y = x ^ y; //代入后就是x^y^yx = x ^ y; //代入后就是x^y^y^x^y

这个方法虽然很巧妙,但是是一种时间换空间的方式; 我们常用的利用另一个变量实现交换是一种空间换时间的方式,来对比下性能:

@Benchmark@Warmup(iterations = 0)@Measurement(iterations = 300)public int swap_1() {    int x = Integer.MAX_VALUE, y = Integer.MAX_VALUE / 2;    int z = x;    x = y;    y = z;    return x + y;}@Benchmark@Warmup(iterations = 0)@Measurement(iterations = 300)public int swap_2() {    int x = Integer.MAX_VALUE, y = Integer.MAX_VALUE / 2;    x ^= y;    y ^= x;    x ^= y;    return x + y;}

结果:

Benchmark            Mode  Cnt          Score           Error  UnitsBitUtilTest.swap_1  thrpt  300  267787894.370 ± 559479133.393  ops/sBitUtilTest.swap_2  thrpt  300  265768807.925 ± 387039155.884  ops/s

测试来看,性能差异并不明显,利用位运算减少了空间占用,减少了GC,但是交换减少了cpu运算,但是GC同样是消耗cpu计算,所以,很难界定。目前还是利用中间变量交换的更常用,也更易读一些

6. bit状态位

我们为了节省空间,尝尝利用一个数字类型(例如long类型)作为状态数,每一位代表一个状态是true还是false。假设我们使用long类型,则一个状态数可以最多表示64个属性。代码上一般这么写:

public static class Test {    //如果你的field是会被并发修改访问,那么最好还是加上缓存行填充防止false sharing    @jdk.internal.vm.annotation.Contended    private long field;    private static final long SWITCH_1_MASK = 1;    private static final long SWITCH_2_MASK = 1 << 1;    private static final long SWITCH_3_MASK = 1 << 2;    public boolean isSwitch1On() {        return (field & SWITCH_1_MASK) == 1;    }    public void turnOnSwitch1() {        field |= SWITCH_1_MASK;    }    public void turnOffSwitch1() {        field &= ~SWITCH_1_MASK;    }}

这样能节省大量空间,在实际应用中,很多地方做了这种优化。最直接的例子就是,Java对象的对象头:

|-------------------------------------------------------|--------------------||                  Mark Word (32 bits)                  |       State        ||-------------------------------------------------------|--------------------|| identity_hashcode:25 | age:4 | biased_lock:1 | lock:2 |       Normal       ||-------------------------------------------------------|--------------------||  thread:23 | epoch:2 | age:4 | biased_lock:1 | lock:2 |       Biased       ||-------------------------------------------------------|--------------------||               ptr_to_lock_record:30          | lock:2 | Lightweight Locked ||-------------------------------------------------------|--------------------||               ptr_to_heavyweight_monitor:30  | lock:2 | Heavyweight Locked ||-------------------------------------------------------|--------------------||                                              | lock:2 |    Marked for GC   ||-------------------------------------------------------|--------------------|

7. 位计数

基于6,有时候我们想某个状态数里面,有多少个状态是true,就是计算这个状态数里面多少位是1.

比较朴素的方法就是:先判断n的奇偶性,为奇数时计数器增加1,然后将n右移一位,重复上面的步骤,直到移位完毕。

高效一点的方法通过:

  1. n & (n - 1) 可以移除最后一位1 (假设最后一位本来是0, 减一后必为1,0 & 1为 0, 最后一位本来是1,减一后必为0,0 & 1为 0)
  2. 移除了最后一位1之后,计数加1,如果结果不为零,则用结果继续第一步。
int n = Integer.MAX_VALUE;int count = 0;while(n != 0) {    n &= n -1;    count++;}

作者:zhxhash

转载自:https://my.oschina.net/u/3747772/blog/3155006

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

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

相关文章

dep指定版本 go_Go 1.12 版本的新特性

Go 1.12昨天&#xff0c;Go 官方发布 1.12 版本。本文介绍下 Go 1.12 版本变更的内容。Go 1.12 正式版发布了&#xff0c;距离上个正式发布版 Go 1.11 已经过去半年。跟往常一样&#xff0c;Go 1.12 保持了 兼容性承诺&#xff0c;预期所有 Go 程序会像之前一样正常编译。新版本…

信号与系统 chapter14 卷积积分的应用

卷积的时移特性 若有一个卷积&#xff1a; f(t)f1(t)∗f2(t)f(t)f_1(t)*f_2(t)f(t)f1​(t)∗f2​(t)&#xff0c;卷积右边的函数都发生了时移&#xff0c;分别为t1,t2t_1,t_2t1​,t2​&#xff0c;则有&#xff1a; 不要管怎么来&#xff0c;记下就完事了 例题&#xff1a; …

java null转换jason_Java笔记Java常量、变量

“要成为绝世高手&#xff0c;并非一朝一夕&#xff0c;除非是天生武学奇才&#xff0c;但是这种人…万中无一”——包租婆这道理放在Java语言学习上也一并受用。在编程方面有着天赋异禀的人毕竟是少数&#xff0c;我们大多数人想要从java语言小白进阶到高手&#xff0c;需要经…

通信系统中的多普勒频移

多普勒现象在通信系统中的表现 当终端在运动特别是在高速运动时&#xff0c;移动终端和基站接收端的信号频率会发生变化&#xff0c;其计算公式如下图所示&#xff1a; fdfc∗v∗cosθf_d\frac{f}{c}*v*cos\thetafd​cf​∗v∗cosθ θ\thetaθ为移动台移动方向与入射方向的夹…

mockito_Mockito – JAXB的RETURNS_DEEP_STUBS

mockito很抱歉没有写一段时间&#xff0c;但是我正忙于为DZone编写JBoss Drools Refcard&#xff0c;而且我正在写一本有关Mockito的书&#xff0c;因此我没有太多时间来写博客了…… 无论如何&#xff0c;最近在我当前的项目中&#xff0c;我对使用Mockito和JAXB结构进行单元…

常见扩频序列

扩频通信就要借助扩频序列 对扩频序列的要求如下&#xff1a; 具有尖锐的自相关特性有尽可能小的互相关特性&#xff0c;最好为0序列平衡&#xff0c;0与1的数量尽可能一样多在扩频序列族中有数目足够多的序列可供选用有尽可能大的序列复杂度 常见的几种扩频序列 PN序列 第一…

删除文件夹下所有的文件_VB删除文件和文件夹的方法

在VB编程中&#xff0c;我们如何删除一个指定的文件&#xff0c;或者文件夹呢&#xff1f;本文&#xff0c;介绍两种方法&#xff0c;详细的介绍如何实现删除文件和文件夹&#xff0c;并对两种方法的区别做一个说明。一、删除文件的方法方法1&#xff1a;使用kill方法删除文件语…

无线网络设计基础

移动电波的传播特点 受到地形影响因素大存在严重的多径衰落迹象存在固定通信中没有的阴影衰落存在相对运动引起的多普勒效应存在由时延散布引起的信号波形展宽 无线收发信设备、天馈线系统、无线电信道组成无线通信系统 自由空间传输损耗 理想条件下&#xff1a; Lbs32.4520…

GSM网络与CDMA网络话务量、基站容量相关计算

GSM网络与CDMA网络的区别 网络类型GSMCDMA编码方式频分多址和时分多址相结合方式码分多址覆盖面积较小较大容量小大话音质量较低支持软切换&#xff0c;和较软切换&#xff0c;使得用户在基站边缘通话时信号更加稳定 GSM规范中推荐使用的频道配置规范 GSM的频道配置 每个小区…

参数整定临界比例度实验_PID理解起来很难?系统讲解PID控制及参数调节,理论加实际才好!...

在实际工程中&#xff0c;应用最为广泛的调节器控制规律为比例、积分、微分控制&#xff0c;简称PID控制&#xff0c;又称PID调节。PID控制器问世至今以其结构简单、稳定性好、工作可靠、调整方便而成为工业控制的主要技术之一。PID调节控制是一个传统控制方法&#xff0c;它适…

apache jmeter_Apache Server和JMeter调试

apache jmeter我一直在使用JMeter为生产服务器生成负载以测试我的应用程序。 该测试计划具有13个以上的HTTP采样器以发出不同的请求&#xff0c;并具有一个正则表达式提取器以从响应中提取一些值。 此值在连续的HTTP Sampler中使用。 这个测试用例简单而直接。 最初&#xff0c…

4计算准确率_孩子计算总出错?4个好方法帮助低年级学生提高计算准确率!

低年级孩子&#xff0c;由于活泼好动&#xff0c;注意力不容易集中&#xff0c;思维容易被分散。表现在学业上&#xff0c;就会出现学习水平参差不齐的情况。而最主要的表现&#xff0c;就是计算能力的差异。据资深数学老师观察&#xff1a;成绩好的孩子&#xff0c;一般不只掌…

使用Speedion 3.0.17或更高版本轻松从事务中返回值

交易次数 在我以前的文章中&#xff0c;我写了关于如何使用Speedment轻松使用事务的方法&#xff0c;其中我们原子地更新了两个银行帐户。 众所周知&#xff0c;事务是一种将多个数据库操作组合到一个原子执行的单个操作中的方法。 但是事务不仅与更新数据库有关&#xff0c;而…

双代号网络图节点时间参数_管理和实务都考!快速学会单代号与双代号参数计算...

工程网络计划是二级建造师《建设工程施工管理》科目每年均会进行考核的高频考点&#xff0c;重点在双代号、单代号网络计划的概念及应用。主要题型为通过网络图或文字描述计算相关网络参数或确定关键线路&#xff0c;本篇就双代号、单代号网络计划相关参数的计算&#xff0c;以…

idea中maven执行install报错_IntelliJ IDEA Maven编译install时报错,无效的发行版:1.8

1.首先看java环境是否配置正确JAVA_HOME : C:\Program Files\Java\jdk1.8.0_92 //安装的Jdk路径PATH: %JAVA_HOME%\bin;按下"window"R 输入cmd 按下"ENTER" 输入java -version&#xff0c;如果出现了版本号等信息说明配置成功2.确认maven配…

gc可视化分析_GC内存可视化器教程–第一部分

gc可视化分析正如您从过去的文章中可能已经读到的那样&#xff0c;要获得的Java程序员的一项关键技能就是理解和评估JVM的运行状况的能力&#xff0c;例如Java堆内存占用量以及垃圾回收过程。 为了实现上述目标&#xff0c;所有JVM供应商&#xff08;Oracle&#xff0c;IBM等&…

go 变量大写_go语言如何将大写转小写

go语言将大写转小写的方法&#xff1a;首先创建一个go示例文件&#xff1b;然后定义一个字符串类型的变量&#xff1b;接着使用ToLower函数将大写的变量字符全部转成小写&#xff1b;最后使用print函数打印转换后的结果即可。本文操作环境&#xff1a;Windows7系统、Go1.11.2版…

spark减少内存消耗_将内存消耗减少20倍

spark减少内存消耗这将是另一个故事&#xff0c;与我们分享有关内存相关问题的最新经验。 该案例是从最近的客户支持案例中提取的&#xff0c;在该案例中&#xff0c;我们遇到了一个行为异常严重的应用程序&#xff0c;该应用程序因生产中的OutOfMemoryError消息而死。 在连接了…

运筹视角下,体系化学习机器学习算法原理的实践和总结

文章目录 引言目标设计目标实践文章汇总经验总结一则预告 引言 上两周总结了我在体系化学习运筹学基础知识方面的个人经验&#xff0c;看过那篇文章的人可能知道&#xff0c;今年我还花了很多时间学习机器学习中各种模型的算法原理。 在工业应用中&#xff0c;机器学习和运筹…

使用Logstash,JDBC将数据聚合并索引到Elasticsearch中

介绍 在我以前的帖子在这里和这里我展示了如何使用JDBC和Elasticsearch JDBC进口商库从SQL数据库索引数据到Elasticsearch。 在这里的第一篇文章中&#xff0c;我提到了使用导入程序库的一些缺点&#xff0c;这些缺点我已在此处复制&#xff1a; 不支持ES版本5及更高版本 嵌套…