數與計算機 (編碼、原碼、反碼、補碼、移碼、IEEE 754、定點數、浮點數)

#PS:要轉載請註明出處,本人版權所有

#PS:這個只是 《 我自己 》理解,如果和你的

#原則相沖突,請諒解,勿噴
測試環境:
ubuntu 18.04
Linux 4.15.0-54-generic #58-Ubuntu SMP Mon Jun 24 10:55:24 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux

起因

有些時候,在測試深度學習的模型的時候,特別是模型出問題,或者其他亂七八糟的原因,你會發現某些層的某些特徵會出現INF和NAN(特別是轉換模型)。說實話,這兩個一個是無窮大,一個是不是數字,我們都知道這個的意思,但是怎麼出現的,可能都忘了。

我如果沒有記錯的話,數在計算機中的表示是來至於《計算機組成原理》,其中介紹了很多很多的有趣的原理。但是我出校後,這些理論知識很少和實際聯繫起來,趁着這次機會,我準備結合我們平常寫的代碼,理一理計算機中數的這個問題。

符號

在計算機裏面,除了指令,就是數值或者符號(統一的說就是數值,因爲數值和符號有映射關係,這就是符號編碼),我也不知道這樣說對不對。
本文主要是說的是數值。對於符號來說,符號涉及到編碼問題,有興趣的可以去了解了解,我們常見的編碼就是ascii,utf-8, unicode,gbk,big5等等,這些編碼涉及到符號顯示的問題。

數值

數值大致有兩類表示法,一類是定點數,一類是浮點數。
計算機中,對於整數來說,就是定點數表示法(這裏對於整數的說法我也不太確定對不對,相關資料也沒有查到明確的說法,但是我理解的是整數就是定點數表示法中,小數點在最右邊),對於實數來說,就是浮點數表示法。

整數

在計算機中,在說明整數的定點數表示之前,還得說幾個書上的理論:
原碼:原碼就是十進制轉換爲二進制。
反碼:原碼取反。
補碼:反碼+1。
(注意:如果是符號數,反碼和補碼運算不影響符號位。)

在計算機中,正整數的補碼反碼原碼一樣,負整數的補碼是原碼取反加1。

對於整數來說,可以分爲有符號還是無符號的整數。

無符號數,是正整數,所以在計算機中是補碼錶示,且就是其十進制轉換爲二進制。

有符號整數,如果是正整數,計算機中補碼錶示,且就是其十進制轉換爲二進制(補碼原碼一樣),如果是負整數,在計算機中表示,且就是其十進制轉換爲二進制,取補碼。

實數

在計算機中,在說明浮點數表示法之前,還得說兩個理論:
移碼:N-M轉換爲二進制,M爲偏移數。

二進制浮點數:整數部分直接轉換爲二進制(除2逆序取餘),小數部分逼近求和(乘2正序取整)。(更詳細的百度隨便找個教程即可,我這裏只是簡單寫一下)

規範化二進制浮點數:小數點前只有一個1。

IEEE 754:IEEE根據一些歷史因素,定製的大部分通用的浮點數表示方法。以單精度浮點數爲例,31位表示符號位,23-30位表示exponent(偏移數是M,指數爲E.),0-22表示base(底數爲B),表示的浮點數爲:B*(2^(E-M))

IEEE 754有很多特殊值,也有一些溢出規則和約等於規則,一般來說,除非你要做科學運算,平常你是遇不到的,簡單瞭解一下即可。
IEEE 754規定的特殊值:
在這裏插入圖片描述
在這裏插入圖片描述
注意:其實浮點數還有其他的一些異常計算及表示,詳細的請查看ieee 754 chapter7

實例分析

上面扯了半天,大家都看煩了,其實都是一些書本上的知識整理。
下面是實例源文件。

#include <cstring>
int main(int argc , char * argv[]){

//integer
	char A = 0xF1;//A = -15; size(A) = 1;mem=[1]111 0001(complement); mem=[1]000 1111(true form)
	short B = 0xF111;//B= -3823; size(B) = 2; mem = [1]111 0001 / 0001 0001 (complement); mem=[1]000 1110 / 1110 1111(true form)
	int C = 0xF1111111;//C = -250539759; size(C) = 4; mem = [1]111 0001 / 0001 0001 / 0001 0001 / 0001 0001 (complement); mem=[1]000 1110 / 1110 1110 / 1110 1110 / 1110 1111 (true form)
	long D = 0xF1111111;//D = -250539759(size(D)=4), 4044427537(size(D)=8); size(D) = 4; mem = [1]111 0001 / 0001 0001 / 0001 0001 / 0001 0001 (complement); mem=[1]000 1110 / 1110 1110 / 1110 1110 / 1110 1111 (true form)(Sizeof(D) may be 4 or 8, it decided by compiler)
	long long E = 0xF111111111111111;//E = -1076060070966390511; size(E) = 8; mem = [1]111 0001 / 0001 0001 / 0001 0001 / 0001 0001 / 0001 0001 / 0001 0001 / 0001 0001 / 0001 0001 (complement); mem=[1]000 1110 / 1110 1110 / 1110 1110 / 1110 1110 / 1110 1110 / 1110 1110 / 1110 1110 / 1110 1111 (true form)

	//overflow int
	int C1 = 0xF1111111FF;//drop highest byte(0xF1), it decided by compiler

	//overflow long long
	long long E1 = 0xF111111111111111FF;//drop highest byte(0xF1), it decided by compiler



//float, IEEE 754.
/*	
single-precision float
bits: [31] is signed bit, (30~23) is exponent, {22~0} is base

double-precision float
bits: [63] is signed bit, (62~52) is exponent, {51~0} is base
*/
	float F = -10;//size(F)=4, mem=[1](100 0001 / 0){010 0000 / 0000 0000 / 0000 0000}, [] is signed bit. () is exponent, {} is normalized base.(single-precision)
	double G = 10;//size(G)=8, mem=[0](100 0000 / 0010) {0100 / 0000 0000 / 0000 0000 / 0000 0000 / 0000 0000 / 0000 0000 / 0000 0000}, [] is signed bit. () is exponent, {} is normalized base.(double-precision)

	int tmp_buf = 0x00800001;
	float F_normalized_min = 0;
	memcpy(&F_normalized_min, &tmp_buf, sizeof(F_normalized_min));//size(F)=4
	float F_normalized_zero = F_normalized_min - 1; //-1


	float F_normalized_max = 0;
	tmp_buf = 0x7F7FFFFF;
	memcpy(&F_normalized_max, &tmp_buf, sizeof(F_normalized_max));//size(F)=4
	float F_normalized_infinity = F_normalized_max * F_normalized_max; //inf
	
	float F_normalized_nan = F_normalized_infinity / F_normalized_infinity;//nan

	//some fun value
	float F_fun_01 = 0.1;
	float F_fun_02 = 0.2;
	float F_fun_03 = 0.3;
	float F_fun_04 = 0.4;
	float F_fun_05 = 0.5;
	float F_fun_06 = 0.6;
	float F_fun_07 = 0.7;
	float F_fun_08 = 0.8;
	float F_fun_09 = 0.9;

	float F_denormalized_min = 0;
	tmp_buf = 0x00000001;
	memcpy(&F_denormalized_min, &tmp_buf, sizeof(F_denormalized_min));//size(F)=4
	float F_denormalized_max = 0;
	tmp_buf = 0x007FFFFF;
	memcpy(&F_denormalized_max, &tmp_buf, sizeof(F_denormalized_max));//size(F)=4




	return 0;
}
整數分析

有符號負整數:
在這裏插入圖片描述
在這裏插入圖片描述
(注意,x86,小端)

無符號整數、有符號正整數,就是直接10進制轉換爲二進制。

浮點數分析

規範化浮點數:
(數值:規範化浮點數最小正數)
在這裏插入圖片描述
在這裏插入圖片描述
(數值:規範化浮點數最大正數)
在這裏插入圖片描述
在這裏插入圖片描述
(數值:非規範化浮點數最小正數)
在這裏插入圖片描述
在這裏插入圖片描述
(數值:非規範化浮點數最大正數)
在這裏插入圖片描述
在這裏插入圖片描述
浮點數異常計算:
(數值:NAN)
在這裏插入圖片描述
在這裏插入圖片描述
(數值:INF)
在這裏插入圖片描述
在這裏插入圖片描述

一些有趣的浮點數

看了上邊後,計算機關於浮點數的的存儲其實是很離散的(很不靠譜),也就是說,很多浮點數計算機根本表示不出來(計算機只能夠存儲,實數數軸上極少部分的數),爲什麼呢?如果你要是瞭解了上面關於10進制浮點數轉2進制浮點數,那麼你可能已經猜到了原因。

在這裏插入圖片描述

在這裏插入圖片描述

下面我以0.1位例,分析一下,爲啥會出現這樣的問題。
在這裏插入圖片描述
0.1在計算機中表示爲:
mem=[0](011 1101/ 1){100 1100/ 1100 1100/ 1100 1100}, [] is signed bit. () is exponent, {} is normalized base.(single-precision)

E=123
M=127
B=1.100 1100/ 1100 1100/ 1100 1100

F_fun_01 = B*2(^-4) = 0.0001100 1100/ 1100 1100/ 1100 1100 = 2^(-4) + 2^(-5) + 2^(-8) + 2^(-9) + 2^(-12) + 2^(-13) + 2^(-16) + 2^(-17) + 2^(-20) + 2^(-21) + 2^(-24) + 2^(-25)

正是因爲在內存中表示的是這樣的,所以這裏打印出來的值看到不是0.1,而是0.1+。那麼大家可能會疑惑,如果0.1都有誤差,那計算的時候,不是炸了嗎?其實不然,還記得c語言中一句話嗎?float的精度爲小數點後6位,爲啥是6位,而不是10位,8位呢?其實原因就是來至於這裏,計算機中,某些小數位後雖然還有值,但是不是有效的,但是這些值影響數值的舍進(類似與四捨五入的約等於,建議大致瞭解,知道有這個事情即可)。

總結

計算機裏面,數的表示,就浮點數最難,但是只需要瞭解了大致的原理,你就會覺得非常簡單。
其實對於計算機來說,數值的表示很弱的,離表示整個數軸差的遠。
在計算機裏面,數值有很多邊界條件,比如溢出、異常運算、異常值,只是我們平常很少遇到,所以遺忘了。
同時也可以說明,其實計算機僅僅是個機器,只會冰冷的加載指令和數,並執行,只是我們的前輩們爲我們做了很多事情,隔離了很多細節和底層,讓我們覺得這些東西可有可無,極大的便利人們使用計算機。這樣有好處也有壞處。
#PS:請尊重原創,不喜勿噴

#PS:要轉載請註明出處,本人版權所有.

有問題請留言,看到後我會第一時間回覆

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