日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程语言 > java >内容正文

java

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

發(fā)布時(shí)間:2023/12/2 java 35 豆豆
生活随笔 收集整理的這篇文章主要介紹了 java实现次方的运算_Java中对于位运算的优化以及运用与思考 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

引言

隨著JDK的發(fā)展以及JIT的不斷優(yōu)化,我們很多時(shí)候都可以寫讀起來易讀但是看上去性能不高的代碼了,編譯器會(huì)幫我們優(yōu)化代碼。之前大學(xué)里面學(xué)單片機(jī)的時(shí)候,由于內(nèi)存以及處理器性能都極其有限(可能很多時(shí)候考慮內(nèi)存的限制優(yōu)先于處理器),所以很多時(shí)候,利用位運(yùn)算來節(jié)約空間或者提高性能,那么這些優(yōu)秀的思想,放到目前的Java中,是否還有必要這么做呢?我們逐一思考與驗(yàn)證下(其實(shí)這也是一個(gè)關(guān)于Premature optimization的界定的思考)

1. 乘法與左移位

左移一位,相當(dāng)于乘以2,左移n位,相當(dāng)于乘以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;}

看上去,移位應(yīng)該比乘法性能快。那么JIT與JVM虛擬機(jī)是否做了一些優(yōu)化呢?優(yōu)化分為兩部分,一個(gè)是編譯器優(yōu)化,另一個(gè)是處理器優(yōu)化。我們先來看看字節(jié)碼是否一致判斷是否有編譯優(yōu)化,例如直接將乘以2優(yōu)化成左移一位,來編寫兩個(gè)函數(shù):

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

編譯好之后,用javap -c來看下編譯好的class文件,字節(jié)碼是:

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,從字節(jié)碼上看編譯器并沒有優(yōu)化。那么在執(zhí)行字節(jié)碼轉(zhuǎn)換成處理器命令是否會(huì)優(yōu)化呢?是會(huì)優(yōu)化的,在底層,乘法其實(shí)就是移位,但是并不是簡單地左移

我們來使用jmh驗(yàn)證下,添加依賴:

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

實(shí)現(xiàn)思路:

  • 被乘數(shù)的選擇:被乘數(shù)固定為1,或者是一個(gè)極小值或者極大值或者是稀疏值(轉(zhuǎn)換成2進(jìn)制很多位是0),測試結(jié)果沒啥太大的參考意義,所以我們選擇2的n次方減某一數(shù)字作為被乘數(shù)
  • 乘數(shù)生成的性能損耗:乘數(shù)是2的隨機(jī)n次方,生成這個(gè)的方式要一致,我們這里要測試的僅僅是移位還有乘法運(yùn)算速度,和實(shí)現(xiàn)復(fù)雜度沒有關(guān)系。 實(shí)現(xià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++) { //被乘數(shù)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; //為了防止乘法多了讀取導(dǎo)致性能差異,這里雖然沒必要,也讀取一下 y = generator.divide[ri]; result += x << ri; //為了防止虛擬機(jī)優(yōu)化代碼將上面的給y賦值踢出循環(huán),加上下面這一步 result += y; }}

    測試結(jié)果:

    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. 除法和右移位

    這個(gè)和乘法以及左移位是一樣的.直接上測試代碼:

    @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; }}

    結(jié)果:

    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. “取余”與“取與”運(yùn)算

    對于2的n次方取余,相當(dāng)于對2的n次方減一取與運(yùn)算,n為正整數(shù)。為什么呢?通過下圖就能很容易理解:

    十進(jìn)制中,對于10的n次方取余,直觀來看就是:

    其實(shí)就是將最后n位取出,就是余數(shù)。 對于二進(jìn)制,是一樣的:

    這個(gè)運(yùn)算相當(dāng)于,對于n-1取與:

    這個(gè)是一個(gè)很經(jīng)典的位運(yùn)算運(yùn)用,廣泛用于各種高性能框架。例如在生成緩存隊(duì)列槽位的時(shí)候,一般生成2的n次方個(gè)槽位,因?yàn)檫@樣在選擇槽位的時(shí)候,就可以用取與代替取余;java中的ForkJoinPool的隊(duì)列長度就是定為2的n次方;netty中的緩存池的葉子節(jié)點(diǎn)都是2的n次方,當(dāng)然這也是因?yàn)槭瞧胶舛娌檎覙渌惴ǖ膶?shí)現(xiàn)。

    我們來看下性能會(huì)好多少:

    @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); }}

    結(jié)果:

    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

    同時(shí),我們從這里也可以引申出,判斷一個(gè)數(shù)是否是2的n次方的方法,就是看這個(gè)數(shù)與這個(gè)數(shù)減一取與運(yùn)算看是否是0,如果是,則是2的n次方,n為正整數(shù)。

    進(jìn)一步的,奇偶性判斷就是看對2取余是否為0,那么就相當(dāng)于對(2-1)=1取與

    4. 求與數(shù)字最接近的2的n次方

    這個(gè)廣泛運(yùn)用于各種API優(yōu)化,上文中提到,2的n次方是一個(gè)好東西。我們在寫框架的很多時(shí)候,想讓用戶傳入一個(gè)必須是2的n次方的參數(shù)來初始化某個(gè)資源池,但這樣不是那么靈活,我們可以通過用戶傳入的數(shù)字N,來找出不大于N的最大的2的n次方,或者是大于N的最小的2的N次方。

    抽象為比較直觀的理解就是,找一個(gè)數(shù)字最左邊的1的左邊一個(gè)1(大于N的最小的2的N次方),或者是最左邊的1(小于N的最大的2的N次方),前提是這個(gè)數(shù)字本身不是2的n次方。

    那么,如何找呢?一種思路是,將這個(gè)數(shù)字最高位1之后的所有位都填上1,最后加一,就是大于N的最小的2的N次方。右移一位,就是小于N的最大的2的N次方。

    如何填補(bǔ)呢?可以考慮按位或計(jì)算,我們知道除了0或0=0以外,其他的都是1. 我們現(xiàn)在有了最左面的1,右移一位,與原來按位或,就至少有了兩位是1,再右移兩位并按位或,則至少有四位為1。。。以此類推:

    用代碼表示是:

    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類的構(gòu)造器,其中的WorkQueue大小,就是通過這樣的轉(zhuǎn)換得來的。

    5. 交換兩個(gè)數(shù)字

    這個(gè)在單片機(jī)編程中經(jīng)常會(huì)使用這個(gè)位運(yùn)算性質(zhì):一個(gè)數(shù)字異或自己為零,一個(gè)數(shù)字異或0為自己本身。那么我們就可以利用這個(gè)性質(zhì)交換兩個(gè)數(shù)字。

    假設(shè)有數(shù)字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

    這個(gè)方法雖然很巧妙,但是是一種時(shí)間換空間的方式; 我們常用的利用另一個(gè)變量實(shí)現(xiàn)交換是一種空間換時(shí)間的方式,來對比下性能:

    @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;}

    結(jié)果:

    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

    測試來看,性能差異并不明顯,利用位運(yùn)算減少了空間占用,減少了GC,但是交換減少了cpu運(yùn)算,但是GC同樣是消耗cpu計(jì)算,所以,很難界定。目前還是利用中間變量交換的更常用,也更易讀一些

    6. bit狀態(tài)位

    我們?yōu)榱斯?jié)省空間,嘗嘗利用一個(gè)數(shù)字類型(例如long類型)作為狀態(tài)數(shù),每一位代表一個(gè)狀態(tài)是true還是false。假設(shè)我們使用long類型,則一個(gè)狀態(tài)數(shù)可以最多表示64個(gè)屬性。代碼上一般這么寫:

    public static class Test { //如果你的field是會(huì)被并發(fā)修改訪問,那么最好還是加上緩存行填充防止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; }}

    這樣能節(jié)省大量空間,在實(shí)際應(yīng)用中,很多地方做了這種優(yōu)化。最直接的例子就是,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. 位計(jì)數(shù)

    基于6,有時(shí)候我們想某個(gè)狀態(tài)數(shù)里面,有多少個(gè)狀態(tài)是true,就是計(jì)算這個(gè)狀態(tài)數(shù)里面多少位是1.

    比較樸素的方法就是:先判斷n的奇偶性,為奇數(shù)時(shí)計(jì)數(shù)器增加1,然后將n右移一位,重復(fù)上面的步驟,直到移位完畢。

    高效一點(diǎn)的方法通過:

  • n & (n - 1) 可以移除最后一位1 (假設(shè)最后一位本來是0, 減一后必為1,0 & 1為 0, 最后一位本來是1,減一后必為0,0 & 1為 0)
  • 移除了最后一位1之后,計(jì)數(shù)加1,如果結(jié)果不為零,則用結(jié)果繼續(xù)第一步。
  • int n = Integer.MAX_VALUE;int count = 0;while(n != 0) { n &= n -1; count++;}

    作者:zhxhash

    轉(zhuǎn)載自:https://my.oschina.net/u/3747772/blog/3155006

    總結(jié)

    以上是生活随笔為你收集整理的java实现次方的运算_Java中对于位运算的优化以及运用与思考的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。