本博客已經(jīng)遷移至:本文從一個(gè)有趣而又令人意外的實(shí)驗(yàn)展開(kāi),介紹一些關(guān)于浮點(diǎn)數(shù)你應(yīng)該知道的基礎(chǔ)知識(shí)
http://cenalulu.github.io/
為了更好的體驗(yàn),請(qǐng)通過(guò)此鏈接閱讀:http://cenalulu.github.io/linux/about-denormalized-float-number/
一個(gè)有趣的實(shí)驗(yàn)文章歡迎轉(zhuǎn)載,但轉(zhuǎn)載時(shí)請(qǐng)保留本段文字,并置于文章的頂部作者:盧鈞軼(cenalulu)本文原文地址:
本文從一個(gè)有趣而詭異的實(shí)驗(yàn)開(kāi)始。最早這個(gè)例子博主是從 Stackoverflow上的一個(gè)問(wèn)題中看到的。為了提高可讀性,博主這里做了改寫(xiě),簡(jiǎn)化成了以下兩段代碼:
#include <iostream>#include <string>using namespace std;int main() { const float x=1.1; const float z=1.123; float y=x; for(int j=0;j<90000000;j++) { y*=x; y/=z; y+=0.1f; y-=0.1f; } return 0;}
#include <iostream>#include <string>using namespace std;int main() { const float x=1.1; const float z=1.123; float y=x; for(int j=0;j<90000000;j++) { y*=x; y/=z; y+=0; y-=0; } return 0;}
上面兩段代碼的唯一差別就是第一段代碼中y+=0.1f
,而第二段代碼中是y+=0
。由于y會(huì)先加后減同樣一個(gè)數(shù)值,照理說(shuō)這兩段代碼的作用和效率應(yīng)該是完全一樣的,當(dāng)然也是沒(méi)有任何邏輯意義的。假設(shè)現(xiàn)在我告訴你:其中一段代碼的效率要比另一段慢7倍。想必讀者會(huì)認(rèn)為一定是y+=0.1f
的那段慢,畢竟它和y+=0
相比看上去要多一些運(yùn)算。但是,實(shí)驗(yàn)結(jié)果,卻出乎意料, y+=0
的那段代碼比y+=0.1f
足足慢了7倍。{: style="color: red" } 。世界觀被顛覆了有木有?博主是在自己的Macbook PRo上進(jìn)行的測(cè)試,有興趣的讀者也可以在自己的筆記本上試試。(只要是支持SSE2指令集的CPU都會(huì)有相似的結(jié)果)。
shell> g++ code1.c -o test1shell> g++ code2.c -o test2shell> time ./test1real 0m1.490suser 0m1.483ssys 0m0.003sshell>?time ./test2real 0m9.895suser 0m9.871ssys 0m0.009s
當(dāng)然 原文中的投票最高的回答解釋的非常好,但博主第一次看的時(shí)候是一頭霧水,因?yàn)榇蟛糠只A(chǔ)知識(shí)已經(jīng)還給大學(xué)老師了。所以,本著知其然還要知其所以然的態(tài)度,博主做了一個(gè)詳盡的分析和思路整理過(guò)程。也希望讀者能夠從0開(kāi)始解釋這個(gè)詭異現(xiàn)象的原因。
復(fù)習(xí)浮點(diǎn)數(shù)的二進(jìn)制轉(zhuǎn)換現(xiàn)在讓我們復(fù)習(xí)大學(xué)計(jì)算機(jī)基礎(chǔ)課程。如果你熟練掌握了浮點(diǎn)數(shù)向二進(jìn)制表達(dá)式轉(zhuǎn)換的方法,那么你可以跳過(guò)這節(jié)。我們先來(lái)看下浮點(diǎn)數(shù)二進(jìn)制表達(dá)的三個(gè)組成部分。
三個(gè)主要成分是:
M*10^N
中的N,只不過(guò)這里是以2為底數(shù)而不是10。需要注意的是,這部分中是以2^7-1
即127
,也即01111111
代表2^0
,轉(zhuǎn)換時(shí)需要根據(jù)127作偏移調(diào)整。下面我們來(lái)看個(gè)實(shí)際例子來(lái)解釋下轉(zhuǎn)換過(guò)程。Step 1 改寫(xiě)整數(shù)部分以數(shù)值5.2
為例。先不考慮指數(shù)部分,我們先單純的將十進(jìn)制數(shù)改寫(xiě)成二進(jìn)制。整數(shù)部分很簡(jiǎn)單,5.
即101.
。
Step 2 改寫(xiě)小數(shù)部分小數(shù)部分我們相當(dāng)于拆成是2^-1
一直到2^-N
的和。例如:0.2 = 0.125+0.0625+0.007825+0.00390625
即2^-3+2^-4+2^-7+2^-8....
,也即.00110011001100110011
Step 3 規(guī)格化現(xiàn)在我們已經(jīng)有了這么一串二進(jìn)制101.00110011001100110011
。然后我們要將它規(guī)格化,也叫Normalize。其實(shí)原理很簡(jiǎn)單就是保證小數(shù)點(diǎn)前只有一個(gè)bit。于是我們就得到了以下表示:1.0100110011001100110011 * 2^2
。到此為止我們已經(jīng)把改寫(xiě)工作完成,接下來(lái)就是要把bit填充到三個(gè)組成部分中去了。
Step 4 填充指數(shù)部分(Exponent):之前說(shuō)過(guò)需要以127作為偏移量調(diào)整。因此2的2次方,指數(shù)部分偏移成2+127即129,表示成10000001
填入。整數(shù)部分(Mantissa):除了簡(jiǎn)單的填入外,需要特別解釋的地方是1.010011
中的整數(shù)部分1在填充時(shí)被舍去了。因?yàn)橐?guī)格化后的數(shù)值整部部分總是為1。那大家可能有疑問(wèn)了,省略整數(shù)部分后豈不是1.010011
和0.010011
就混淆了么?其實(shí)并不會(huì),如果你仔細(xì)看下后者:會(huì)發(fā)現(xiàn)他并不是一個(gè)規(guī)格化的二進(jìn)制,可以改寫(xiě)成1.0011 * 2^-2
。所以省略小數(shù)點(diǎn)前的一個(gè)bit不會(huì)造成任何兩個(gè)浮點(diǎn)數(shù)的混淆。具體填充后的結(jié)果見(jiàn)下圖
練習(xí):如果想考驗(yàn)自己是否充分理解這節(jié)內(nèi)容的話,可以隨便寫(xiě)一個(gè)浮點(diǎn)數(shù)嘗試轉(zhuǎn)換。通過(guò) 浮點(diǎn)二進(jìn)制轉(zhuǎn)換工具可以驗(yàn)證答案。
什么是Denormalized Number了解完浮點(diǎn)數(shù)的表達(dá)以后,不難看出浮點(diǎn)數(shù)的精度和指數(shù)范圍有很大關(guān)系。最低不能低過(guò)2^-7-1
最高不能高過(guò)2^8-1
(其中剔除了指數(shù)部分全0喝全1的特殊情況)。那么當(dāng)我們要表示一個(gè)例如:1.00001111*2^-7
這樣的超小數(shù)值的時(shí)候就無(wú)法用規(guī)格化數(shù)值表示,只能用0來(lái)代替。那么,這樣做有什么問(wèn)題呢?最容易理解的一種副作用就是:當(dāng)多次做低精度浮點(diǎn)數(shù)舍棄的時(shí)候,就會(huì)出現(xiàn)除數(shù)為0的exception,導(dǎo)致異常。
于是乎就出現(xiàn)了Denormalized Number
(后稱(chēng)非規(guī)格化浮點(diǎn))。他和規(guī)格浮點(diǎn)的區(qū)別在于,規(guī)格浮點(diǎn)約定小數(shù)點(diǎn)前一位默認(rèn)是1。而非規(guī)格浮點(diǎn)約定小數(shù)點(diǎn)前一位可以為0,這樣小數(shù)精度就相當(dāng)于多了最多2^22
范圍。
但是,精度的提升是有代價(jià)的。由于CPU硬件只支持,或者默認(rèn)對(duì)一個(gè)32bit的二進(jìn)制使用規(guī)格化解碼。因此需要支持32bit非規(guī)格數(shù)值的轉(zhuǎn)碼和計(jì)算的話,需要額外的編碼標(biāo)識(shí),也就是需要額外的硬件或者軟件層面的支持。以下是wiki上的兩端摘抄,說(shuō)明了非規(guī)格化計(jì)算的效率非常低。> 一般來(lái)說(shuō),由軟件對(duì)非規(guī)格化浮點(diǎn)數(shù)進(jìn)行處理將帶來(lái)極大的性能損失,而由硬件處理的情況會(huì)稍好一些,但在多數(shù)現(xiàn)代處理器上這樣的操作仍是緩慢的。極端情況下,規(guī)格化浮點(diǎn)數(shù)操作可能比硬件支持的非規(guī)格化浮點(diǎn)數(shù)操作快100倍。
For example when using NVIDIA's CUDA platform, on gaming cards, calculations with double precision take 3 to 24 times longer to complete than calculations using single precision.
如果要解釋為什么有如此大的性能損耗,那就要需要涉及電路設(shè)計(jì)了,超出了博主的知識(shí)范圍。當(dāng)然萬(wàn)能的wiki也是有答案的,有興趣的讀者可以自行查閱。
回到實(shí)驗(yàn)總上面的分析中我們得出了以下結(jié)論:
于是我們就可以發(fā)現(xiàn)通過(guò)幾十上百次的循環(huán)后,y中存放的數(shù)值無(wú)限接近于零。CPU將他表示為精度更高的非規(guī)格化浮點(diǎn)。而當(dāng)y+0.1f
時(shí)為了保留跟重要的底數(shù)部分,之后無(wú)限接近0(也即y之前存的數(shù)值)被舍棄,當(dāng)y-0.1f
后,y又退化為了規(guī)格化浮點(diǎn)數(shù)。并且之后的每次y*x
和y/z
時(shí),CPU都執(zhí)行的是規(guī)劃化浮點(diǎn)運(yùn)算。而當(dāng)y+0
,由于加上0值后的y仍然可以被表示為非規(guī)格化浮點(diǎn),因此整個(gè)循環(huán)的四次運(yùn)算中CPU都會(huì)使用非規(guī)格浮點(diǎn)計(jì)算,效率就大大降低了。
當(dāng)然,也有在程序內(nèi)部也是有辦法控制非規(guī)范化浮點(diǎn)的使用的。在相關(guān)程序的上下文中加上fesetenv(FE_DFL_DISABLE_SSE_DENORMS_ENV);
就可以迫使CPU放棄使用非規(guī)范化浮點(diǎn)計(jì)算,提高性能。我們用這種辦法修改上面實(shí)驗(yàn)中的代碼后,y+=0
的效率就和y+=0.1f
就一樣了。甚至還比y+=0.1f
更快了些,世界觀又端正了不是么:) 修改后的代碼如下
#include <iostream>#include <string>#include <fenv.h>using namespace std;int main() { fesetenv(FE_DFL_DISABLE_SSE_DENORMS_ENV); const float x=1.1; const float z=1.123; float y=x; for(int j=0;j<90000000;j++) { y*=x; y/=z; y+=0; y-=0; } return 0;}
Reference什么是非規(guī)格化浮點(diǎn)數(shù)Why does changing 0.1f to 0 slow down performance by 10x?IEEE floating pointFloating pointDenormal number
新聞熱點(diǎn)
疑難解答
圖片精選