(byte)1658385462>>16=-40,怎麼算的?

楊同學technotes發表於2022-12-26

正文

在 Github 專案mongo-java-driver有一個類ObjectId.java,它的作用是生成唯一 id 的,它的核心實現是下面這樣一段程式碼 [1]:

public void putToByteBuffer(final ByteBuffer buffer) {
    notNull("buffer", buffer);
    isTrueArgument("buffer.remaining() >=12", buffer.remaining() >= OBJECT_ID_LENGTH);

    buffer.put(int3(timestamp));
    buffer.put(int2(timestamp));
    buffer.put(int1(timestamp));
    buffer.put(int0(timestamp));
    buffer.put(int2(randomValue1));
    buffer.put(int1(randomValue1));
    buffer.put(int0(randomValue1));
    buffer.put(short1(randomValue2));
    buffer.put(short0(randomValue2));
    buffer.put(int2(counter));
    buffer.put(int1(counter));
    buffer.put(int0(counter));
}

上述程式碼中的int2()方法定義如下:

private static byte int2(int x) {
    return (byte) (x >> 16);
}

取當前時間戳(秒)1658385462,我們來測試一下該方法:

@Test
public void test() {
    System.out.println(int2(1658385462)); // -40
}

得到的結果是 -40。即:(byte) 1658385462 >> 16 = -40

這是怎麼算出來的?

計算過程

1、首先,計算機要將 1658385462 轉換為二進位制數。

因為 int 為 4 位元組 32 位,對應二進位制結果如下:

0110 0010 1101 1000 1111 0100 0011 0110

2、執行 >>16 運算。

運算結果是 0110 0010 1101 1000。

0110 0010 1101 1000 1111 0100 0011 0110 >> 16 = 0110 0010 1101 1000

3、因為計算機儲存補位,所以需將其轉為補位。

正數的補碼就是其本身,補碼是:0110 0010 1101 1000。

4、因為 byte 為 1 位元組 8 位,所以強制轉換時計算機只保留其後 8 位。

保留 8 位的結果是:1101 1000。

5、保留 8位後的數值仍然是補位,而要展示給使用者需轉換成原位。

補:1101 1000
反:1101 0111
原:1010 1000

6、最高位 1 表示負數,將 010 1000 轉換成十進位制數,則為 -40。

什麼是原碼、反碼、補碼?

原碼:原碼就是符號位加上真值的絕對值,即用第一位表示符號,其餘位表示值。

反碼:正數的反碼是其本身。負數的反碼是在其原碼的基礎上,符號位不變,其餘各位取反。

補碼:正數的補碼就是其本身。負數的補碼是在其原碼的基礎上,符號位不變,其餘各位取反,最後+1。

從原碼、反碼、補碼的表示方式不難看出,原碼才是人眼最直觀能看出值的表示方式,那麼為什麼還要有反碼和補碼呢?

答案是為了簡化計算機積體電路的設計。

我們人腦是可以辨別第一位是符號位的,在計算的時候我們會根據符號位,選擇對真值區域的加減。但是對於計算機,辨別“符號位”顯然會讓計算機的基礎電路設計變得十分複雜,於是人們想出了將符號位也參與運算的方法。

我們知道,根據運演算法則:減去一個正數等於加上一個負數,即:1-1 = 1 + (-1) = 0,所以機器可以只有加法而沒有減法,這樣計算機運算的設計就更簡單了。此外,由於現階段計算機 CPU 擅長做加法運算,CPU 硬體實現減法要複雜得多,而且運算效率很低,所以我們偷懶只討論加法運算。說不定以後發明了減法加速硬體,那就另當別論了。

為什麼要有反碼?

於是人們開始探索將符號位參與運算,並且只保留加法的方法。 首先來看原碼:計算十進位制的表示式:1-1=0。

1 - 1
= 1 + (-1) = [00000001]原 + [10000001]原
= [10000010]原
= -2

如果用原碼錶示,讓符號位也參與計算,顯然對於減法來說,結果是不正確的。這也就是為何計算機內部不使用原碼錶示一個數。

為了解決原碼做減法的問題,出現了反碼。

1 - 1
= 1 + (-1)
= [0000 0001]原 + [1000 0001]原
= [0000 0001]反 + [1111 1110]反
= [1111 1111]反
= [1000 0000]原
= -0

發現用反碼計算減法,結果的真值部分是正確的。

為什麼要有補碼?

用反碼計算減法,結果的真值部分是正確的。而唯一的問題其實就出現在“0”這個特殊的數值上。 雖然人們理解上 +0 和 -0 是一樣的,但是 0 帶符號是沒有任何意義的。 而且會有[0000 0000]原[1000 0000]原兩個編碼表示 0。

於是出現了補碼,為了解決 0 的符號以及 0 的兩個編碼問題:

1 - 1
= 1 + (-1)
= [0000 0001]原 + [1000 0001]原
= [0000 0001]補 + [1111 1111]補
= [0000 0000]補
= [0000 0000]原

這樣 0 用 [0000 0000] 表示,而以前出現問題的 -0 則不存在了。那另一個編碼 [1000 0000] 是否就棄用了呢?

(-1) + (-127)
= [1000 0001]原 + [1111 1111]原
= [1111 1111]補 + [1000 0001]補
= [1000 0000]補

-1-127 的結果應該是 -128,剛好 [1000 0000] 可以用來表示 -128。在用補碼運算的結果中,[1000 0000]補就是 -128。

但是注意: -128 並沒有原碼和反碼錶示。對 -128 的補碼[1000 0000]補算出來的原碼是[0000 0000]原,這顯然是不正確的。

使用補碼,不僅僅修復了 0 的符號以及存在兩個編碼的問題,而且還能夠多表示一個最低數。所以最終同樣是 8 位二進位制,使用原碼或反碼錶示的範圍為 [-127, +127],而使用補碼錶示的範圍為 [-128, 127]。

小結

我整理了本文知識消化鏈路,如下。

使用原碼計算減法的結果是錯誤的
-> 出現了反碼
-> 使用反碼計算的 0 有兩個,+0 和 -0
-> 出現了補碼

更多技術乾貨,敬請關注公眾號「楊同學technotes」,歡迎技術交流。

文中提及的連結

參考資料

相關文章