一個有趣的實驗:用0.1f 替換 0,效能提升 7 倍!
點選關注上方“視學演算法”,設為“置頂或星標”,第一時間送達技術乾貨。
本文來源:http://cenalulu.github.io/linux/about-denormalized-float-number
一個有趣的實驗
本文從一個有趣而詭異的實驗開始,最早這個例子是 Stackoverflow上的一個問題:
https://stackoverflow.com/questions/9314534/why-does-changing-0-1f-to-0-slow-down-performance-by-10x
為了提高可讀性,對程式碼稍作修改,簡化成了以下兩段程式碼:
#include
#include
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
#include
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 會先加後減同樣一個數值,照理說這兩段程式碼的作用和效率應該是完全一樣的,當然也是沒有任何邏輯意義的。
假設現在我告訴你:其中一段程式碼的效率要比另一段慢 7 倍。
想必讀者會認為一定是 y += 0.1f
的那段慢,畢竟它和 y += 0
相比看上去要多一些運算。
但是,實驗結果,卻出乎意料, y += 0
的那段程式碼比 y += 0.1f
足足慢了7倍。
世界觀被顛覆了有木有?
博主是在自己的Macbook Pro上進行的測試,有興趣的讀者也可以在自己的筆記本上試試。(只要是支援SSE2指令集的CPU都會有相似的結果)。
shell> g++ code1.c -o test1
shell> g++ code2.c -o test2
shell> time ./test1
real 0m1.490s
user 0m1.483s
sys 0m0.003s
shell> time ./test2
real 0m9.895s
user 0m9.871s
sys 0m0.009s
本著知其然還要知其所以然的態度,博主做了一個詳盡的分析和思路整理過程。也希望讀者能夠從0開始解釋這個詭異現象的原因。
複習浮點數的二進位制轉換
現在讓我們複習大學計算機基礎課程,如果你熟練掌握了浮點數向二進位制表示式轉換的方法,那麼你可以跳過這節。
我們先來看下浮點數二進位制表達的三個組成部分。
三個主要成分是:
Sign(1bit):表示浮點數是正數還是負數。0表示正數,1表示負數。-
Exponent(8bits):指數部分。類似於科學技術法中的M*10^N中的N,只不過這裡是以2為底數而不是10。需要注意的是,這部分中是以2^7-1即127,也即01111111代表2^0,轉換時需要根據127作偏移調整。
Mantissa(23bits):基數部分。浮點數具體數值的實際表示。
下面我們來看個實際例子來解釋下轉換過程。
Step 1 改寫整數部分 以數值5.2為例。先不考慮指數部分,我們先單純的將十進位制數改寫成二進位制。整數部分很簡單,5.即101.。
Step 2 改寫小數部分 小數部分我們相當於拆成是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 規格化 現在我們已經有了這麼一串二進位制101.00110011001100110011。然後我們要將它規格化,也叫Normalize。
其實原理很簡單就是保證小數點前只有一個bit。於是我們就得到了以下表示:1.0100110011001100110011 * 2^2。
到此為止我們已經把改寫工作完成,接下來就是要把bit填充到三個組成部分中去了。
Step 4 填充 指數部分(Exponent):之前說過需要以127作為偏移量調整。因此2的2次方,指數部分偏移成2+127即129,表示成10000001填入。
整數部分(Mantissa):除了簡單的填入外,需要特別解釋的地方是1.010011中的整數部分1在填充時被捨去了。因為規格化後的數值整部部分總是為1。
那大家可能有疑問了,省略整數部分後豈不是1.010011和0.010011就混淆了麼?
其實並不會,如果你仔細看下後者:會發現他並不是一個規格化的二進位制,可以改寫成1.0011 * 2^-2。
所以省略小數點前的一個bit不會造成任何兩個浮點數的混淆。
具體填充後的結果見下圖
什麼是Denormalized Number
瞭解完浮點數的表達以後,不難看出浮點數的精度和指數範圍有很大關係。最低不能低過2^-7-1最高不能高過2^8-1(其中剔除了指數部分全0和全1的特殊情況)
如果超出表達範圍那麼不得不捨棄末尾的那些小數,我們成為overflow和underflow,甚至有時捨棄都無法表示
例如當我們要表示一個:1.00001111*2^-7這樣的超小數值的時候就無法用規格化數值表示,如果不想點其他辦法的話,CPU內部就只能把它當做0來處理。
那麼,這樣做有什麼問題呢?最顯然易見的一種副作用就是:當多次做低精度浮點數捨棄的後,就會出現除數為0的exception,導致異常。
當然精度失準嚴重起來也可以要人命,以下這個事件摘自wikipedia:
On 25 February 1991, a loss of significance in a MIM-104 Patriot missile battery prevented it intercepting an incoming Scud missile in Dhahran, Saudi Arabia, contributing to the death of 28 soldiers from the U.S. Army’s 14th Quartermaster Detachment.[25] See also: Failure at Dhahran
於是乎就出現了Denormalized Number(後稱非規格化浮點)。他和規格浮點的區別在於,規格浮點約定小數點前一位預設是1。而非規格浮點約定小數點前一位可以為0,這樣小數精度就相當於多了最多2^22範圍。
但是,精度的提升是有代價的。由於CPU硬體只支援,或者預設對一個32bit的二進位制使用規格化解碼。因此需要支援32bit非規格數值的轉碼和計算的話,需要額外的編碼標識,也就是需要額外的硬體或者軟體層面的支援。
以下是wiki上的兩端摘抄,說明了非規格化計算的效率非常低。一般來說,由軟體對非規格化浮點數進行處理將帶來極大的效能損失,而由硬體處理的情況會稍好一些,但在多數現代處理器上這樣的操作仍是緩慢的。
極端情況下,規格化浮點數操作可能比硬體支援的非規格化浮點數操作快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.
如果要解釋為什麼有如此大的效能損耗,那就要需要涉及電路設計了,超出了博主的知識範圍。當然萬能的wiki也是有答案的,有興趣的讀者可以自行查閱。
回到實驗
總上面的分析中我們得出了以下結論:
浮點數表示範圍有限,精度受限於指數和底數部分的長度,超過精度的小數部分將會被捨棄(underflow)
為了表示更高精度的浮點數,出現了非規格化浮點數,但是他的計算成本非常高。
於是我們就可以發現通過幾十上百次的迴圈後,y中存放的數值無限接近於零。CPU將他表示為精度更高的非規格化浮點。
而當y+0.1f時為了保留跟重要的底數部分,之後無限接近0(也即y之前存的數值)被捨棄,當y-0.1f後,y又退化為了規格化浮點數。並且之後的每次y*x和y/z時,CPU都執行的是規劃化浮點運算。
而當y+0,由於加上0值後的y仍然可以被表示為非規格化浮點,因此整個迴圈的四次運算中CPU都會使用非規格浮點計算,效率就大大降低了。
其他
當然,也有在程式內部也是有辦法控制非規範化浮點的使用的。在相關程式的上下文中加上fesetenv(FE_DFL_DISABLE_SSE_DENORMS_ENV);就可以迫使CPU放棄使用非規範化浮點計算,提高效能。
我們用這種辦法修改上面實驗中的程式碼後,y+=0的效率就和y+=0.1f就一樣了。甚至還比y+=0.1f更快了些,世界觀又端正了不是麼:) 修改後的程式碼如下。
#include
#include
#include
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;
}
- END -
如果看到這裡,說明你喜歡這篇文章,請轉發、點贊。掃描下方二維碼或者微信搜尋「perfect_iscas」,新增好友後即可獲得10套程式設計師全棧課程+1000套PPT和簡歷模板,向我私聊「進群」二字即可進入高質量交流群。
↓掃描二維碼進群↓
喜歡文章,點個在看
相關文章
- T-One 社群版排程引擎替換至 runnerV2,效能提升 6.8 倍
- 用 100 行程式碼提升 10 倍的效能行程
- 用100行程式碼提升10倍的效能行程
- Golang pprof 效能調優實戰,效能提升 3 倍!Golang
- 如何把 MySQL 備份驗證效能提升 10 倍MySql
- 這兒有一個使你網頁效能提升10倍的工具網頁
- 記一次提升18倍的效能優化優化
- github-abertschi/graalphp:一個基於GraalVM的高效PHP實現,比傳統PHP7或HHVM效能提升接近10倍!GithubPHPLVM
- Java 中的5個程式碼效能提升技巧,最高提升近10倍Java
- 效能竟然再提升一倍!Redis的一個例項有多快?- keydb.devRedisdev
- 用一個效能提升了666倍的小案例說明在TiDB中正確使用索引的重要性TiDB索引
- 換個角度提升APP效能和質量APP
- 帶你深入 Dart 解析一個有趣的引用和編譯實驗Dart編譯
- 從0搭建一個實用的MVVM框架MVVM框架
- 幫你提升Python執行效能的7 個習慣Python
- js replace替換字串,同時替換多個方法JS字串
- gohook 一個支援執行時替換 golang 函式的庫實現HookGolang函式
- GTest(基於YApi)介面研發效能提升10倍 實戰API
- Android Hilt實戰初體驗: Dagger替換成HiltAndroid
- Excel 多個變數替換 實際值Excel變數
- MS SQL 替換欄位中的 u00a0SQL
- 7-15 字串替換 (6分)字串
- 【Linux Ops】如何替換 libstdc++ 提升 GLIBCXX 版本LinuxC++
- 一個基於 SourceGenerator 生成 從 dbReader轉換為 class 資料的效能測試實驗
- 百萬商品查詢,效能提升了10倍
- 幾行程式碼提升Pandas效能150倍行程
- Nacos 2.0 正式釋出,效能提升 10 倍!!
- 一些有趣的B+樹優化實驗優化
- 簡單一招竟把nginx伺服器效能提升50倍Nginx伺服器
- Nacos 2.0 正式釋出,效能提升了 10 倍!!
- 我是如何將一個老系統的kafka消費者服務的效能提升近百倍的Kafka
- 【前端詞典】分享 8 個有趣且實用的 API前端API
- unity 統一替換shaderUnity
- 讓你的 webpack sass 和 css 處理效能 10 倍提升WebCSS
- 雲原生時代的“精益實踐”:企業效能提升10倍“殺手鐧”
- KubeSphere 助力提升研發效能的應用實踐分享
- 隨手打造一個可以替換全站字串的nginx映象(docker)字串NginxDocker
- 實現最簡單的模板替換