位運算的優化與應用

一.簡介

隨着JDK的發展以及JIT的不斷優化,語法糖越來越豐富了,程序用了太多了看似高級的用法,易讀性提高很多,那麼效率呢?很多時候計算可以轉換位運算,提高性能和節約空間,很多組件都用到了,比如HashMap、BitSet、ProtocolBuf等等,本文驗證一些位運算的用法。

二.基礎

2.1 位運算符

Java整型數據類型有:byte、char、short、int、long。一個字節佔8位。

數據類型 所佔位數
byte 8
boolean 8
short 16
int 32
long 64
float 32
double 64
char 16

計算機中二進制最高符號位"0"正好,"1"爲負號。計算機中只能存在二進制,所以計算需要用二進制的補碼進行計算。

  • 正數的原碼,補碼,反碼都是自己本身。
  • 負數的原碼、補碼(反碼+1)、反碼(原碼(除符號位外)每位取反)。

舉個例子 int i=-10;

把十進制轉換爲二進制:

原碼:10000000 00000000 00000000 00001010

反碼:11111111 11111111 11111111 11110101

補碼:11111111 11111111 11111111 11110110

補碼的意義:

  • 使符號位能與有效值部分一起參與運算,從而簡化運算規則。
  • 使減法運算轉換爲加法運算,進一步簡化計算機中運算器的線路設計。

位運算符用來對二進制操作,共有七類運算符,如下:

符號 含義
& 按位與
| 按位或
^ 按位異或
~ 按位取反
>> 右移
<< 左移
>>> 無符號右移動
  • 左移( << ) 整體左移,右邊空出位補零,左邊位捨棄 (-4 << 1 = -8)
  • 右移( >> ) 整體右移,左邊空出位補零或補1(負數補1,整數補0),右邊位捨棄 (-4 >> 1 = -2)
  • 無符號右移( >>> )同>>,但不管正數還是負數都左邊位都補0 (-4 >>> 1 = 2147483646)
  • 與( & )每一位進行比較,兩位都爲1,結果爲1,否則爲0(-4 & 1 = 0)
  • 或( | )每一位進行比較,兩位有一位是1,結果就是1(-4 | 1 = -3)
  • 非( ~ ) 每一位進行比較,按位取反(符號位也要取反)(~ -4 = 3)
  • 異或( ^ )每一位進行比較,相同爲0,不同爲1(^ -4 = -3)

2.2 *與<<

乘法運算,可以換算成位運算實現

a * (2^n) 等價於 a << n

看上去,位運算應該是位運算性能快,那麼根據字節碼可以查看下,優化是從什麼時候開始?是編譯器優化,還是從處理器優化,根據下面示例驗證下:

public void multi1(){
    int i =1;
    i = i << 1;
}
public void multi2(){
    int i = 1;
    i *=2;
}

編譯好之後,用javap -c來查看class文件,字節碼:

Compiled from "BitTest.java"
public class com.algorithm.base.bitoperation.BitTest {
  public com.algorithm.base.bitoperation.BitTest();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public void multi1();
    Code:
       0: iconst_1
       1: istore_1
       2: iload_1
       3: iconst_1
       4: ishl
       5: istore_1
       6: return

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

可以看出左移是ishl,乘法是imul,從字節碼上看編譯器並沒有優化。那麼在執行字節碼轉換成處理器命令是否會優化呢?是會優化的,在底層,乘法其實就是移位,但是並不是簡單地左移。

處理器

從效率上看,使用移位指令有更高的效率,因爲移位指令佔2個機器週期,而乘除法指令佔4個機器週期。從硬件上看,移位對硬件更容易實現,所以會用移位,移1位就乘2,這種乘法當然考慮移位了。

示例:

兩個64位的數按位與 和 一個64位的數右移32位 哪個操作快些?

移位快,只有一次尋址,邏輯運算和寫操作,按位與需要兩次尋址,一次邏輯運算和一次寫。

2.3 /與>>

除法運算,可以換算成位運算實現

a/(2^n) 等價於 a >> n

java中 << >> >>>都是針對補碼來進行的。

2.4 %與&

取模運算,可以換算成位運算實現

a % (2^n) 等價於 a &(2^n-1)

示例:

a=25,n=3

a 二進制

a % 8 = 1

a &(7) = 1

25 二進制 11001

7 二進制 011

進一步可以判斷int類型變量a是奇數還是偶數

a & 1 = 0 偶數

a & 1 = 1 奇數

這個是一個很經典的位運算運用,廣泛用於各種高性能框架。例如在生成緩存隊列槽位的時候,一般生成2的n次方個槽位,因爲這樣在選擇槽位的時候,就可以用取與代替取餘;java中的ForkJoinPool的隊列長度就是定爲2的n次方;netty中的緩存池的葉子節點都是2的n次方,當然這也是因爲是平衡二叉查找樹算法的實現。

2.5 交換兩個數字

a ^= b
b ^= a
a ^= b

示例:
a = 11 二進制 111

b = 10 二進制 110

a = a ^ b 001

b = b^ a 111

a = a ^ b 110

2.6 絕對值

絕對值的定義:

正數的絕對值是它本身,負數的絕對值是它相反數,0的絕對值還是0。

任何有理數的絕對值都是非負數,也就是說任何有理數的絕對值都大於0。

int i;
int sign = i >> 31 //取符號位
(i+sign)^ sign  

2.7 bit狀態位

在一個系統中,用戶一般有查詢(Select)、新增(Insert)、修改(Update)、刪除(Selete)四種權限,四種權限有多種組合方式,也就是有16中不同的權限狀態(2的4次方)。每一位代表一個狀態是true還是false。

public class NewPermission {
	// 是否允許查詢,二進制第1位,0表示否,1表示是
	public static final int ALLOW_SELECT = 1 << 0; // 0001
	
	// 是否允許新增,二進制第2位,0表示否,1表示是
	public static final int ALLOW_INSERT = 1 << 1; // 0010
	
	// 是否允許修改,二進制第3位,0表示否,1表示是
	public static final int ALLOW_UPDATE = 1 << 2; // 0100
	
	// 是否允許刪除,二進制第4位,0表示否,1表示是
	public static final int ALLOW_DELETE = 1 << 3; // 1000
	
	// 存儲目前的權限狀態
	private int flag;
 
	/**
	 *  重新設置權限
	 */
	public void setPermission(int permission) {
		flag = permission;
	}
 
	/**
	 *  添加一項或多項權限
	 */
	public void enable(int permission) {
		flag |= permission;
	}
	
	/**
	 *  刪除一項或多項權限
	 */
	public void disable(int permission) {
		flag &= ~permission;
	}
	
	/**
	 *  是否擁某些權限
	 */
	public boolean isAllow(int permission) {
		return (flag & permission) == permission;
	}
	
	/**
	 *  是否禁用了某些權限
	 */
	public boolean isNotAllow(int permission) {
		return (flag & permission) == 0;
	}
	
	/**
	 *  是否僅僅擁有某些權限
	 */
	public boolean isOnlyAllow(int permission) {
		return flag == permission;
	}
}

這樣做節省空間,例如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

判斷有多少個狀態true,就是計算這個狀態裏面有多少位是1。

比較樸素的方法的:先判斷n的奇偶性,爲奇數計數器加1,然後將n右移1位,重複上面的步驟

int n = Integer.MAX_VALUE;
int count = 0;
while (n!=0){
    if((n&1)==1) count++;
    n= n>>1;
}

高效的方法:

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

2.8 初始化算法

這個廣泛用於各種組件的初始化操作,2^n初始化資源池。

比如HashMap:

/**
 * Returns a power of two size for the given target capacity.
 * 不考慮最大容量情況下,返回大於輸入參數且最近的2的整數次冪的數。
 * 比如10,則返回16
 */
static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

在這裏插入圖片描述

2.9 hashcode

在Java語言,判斷對象相等,需要重寫hashcode和equals方法,例如在hashMap中hash算法

 static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

使用位移16和異或爲了防止,後續計算節點位置,出現衝突,打亂順序。

三.總結

還有很多應用方式,繼續研究,對於工程來說少就是指數級的多。

參考:

https://zhuanlan.zhihu.com/p/101723848

https://blog.csdn.net/ko_tin/article/details/52830535?depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-1&utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-1

https://blog.csdn.net/zhangyong01245/article/details/103400375

https://blog.csdn.net/tonysong111073/article/details/79480495

https://blog.csdn.net/hhy107107/article/details/82801780?depth_1-utm_source=distribute.pc_relevant.none-task-blog-OPENSEARCH-2&utm_source=distribute.pc_relevant.none-task-blog-OPENSEARCH-2

https://blog.csdn.net/javazejian/article/details/51181320?depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-2&utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-2

https://blog.csdn.net/Hk_john/article/details/69942784

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章