之前在看 lua 源碼的時候,,看到一處浮點數(shù)轉(zhuǎn)整數(shù)的方法,當(dāng)時確實嚇我一跳,,后來在網(wǎng)上搜索了才知道浮點數(shù)原來還有這么神奇的地方,,我看到一篇喜歡的文章,翻譯一下(英文一般還請見諒),,大家要閑著沒事可以看看,,先貼出 lua 中的轉(zhuǎn)換方法。
/*
@@ lua_number2int is a macro to convert lua_Number to int.
@@ lua_number2integer is a macro to convert lua_Number to lua_Integer.
** CHANGE them if you know a faster way to convert a lua_Number to
** int (with any rounding method and without throwing errors) in your
** system. In Pentium machines, a naive typecast from double to int
** in C is extremely slow, so any alternative is worth trying.
*/
// 這是這個文件最trick的地方,,把一個double數(shù)轉(zhuǎn)換成long,,用到了神奇的數(shù)字6755399441055744.0
/* On a Pentium, resort to a trick */
#if defined(LUA_NUMBER_DOUBLE) && !defined(LUA_ANSI) && !defined(__SSE2__) && (defined(__i386) || defined (_M_IX86) || defined(__i386__))
union luai_Cast { double l_d; long l_l; };
#define lua_number2int(i,d) { volatile union luai_Cast u; u.l_d = (d) + 6755399441055744.0; (i) = u.l_l; }
#define lua_number2integer(i,n) lua_number2int(i, n)
/* this option always works, but may be slow */
#else
#define lua_number2int(i,d) ((i)=(int)(d))
#define lua_number2integer(i,d) ((i)=(lua_Integer)(d))
#endif
上面用到了一個神奇的數(shù)字 6755399441055744.0,通過把一個 double 類型的數(shù)加上這個數(shù)字再直接拿來用就是整型了,。這個真心讓我驚奇,。于是我馬上在我的 vs 里寫了個測試了一下,結(jié)果同樣令我心驚:
可以看到我用 8.75 作為測試的 double 數(shù),,結(jié)果經(jīng)過 lua 這個宏轉(zhuǎn)換的結(jié)果是 9,,其實當(dāng)我把數(shù)改成 8.45 時,結(jié)果是 8,,正好是轉(zhuǎn)換成整數(shù)的結(jié)果,。那么 6755399441055744.0 這個神奇的數(shù)字到底是哪里來的呢? 我打開計算器看了一下,,發(fā)現(xiàn)這個 6755399441055744.0 是 1.5 * 2^52,,既然是這么個特殊的數(shù),我們知道在 IEEE 754 里規(guī)定雙精度浮點數(shù)是 64 位,,而其中符號位占 1 位,,指數(shù)部分是 11 位,,那么尾數(shù)部分就是 52 位,那么這個 1.5 * 2^52 會不會跟這個有關(guān)呢,?網(wǎng)上一搜看到篇文章,,這里翻譯了一下,有興趣了解的朋友可以看看,,我這里把文章的意思總結(jié)一下,,大體意思就是說在做浮點數(shù)加減法的時候有這么幾個步驟: 1、對階 (就是使兩個浮點數(shù)的兩個指數(shù)部分相同,,規(guī)定小數(shù)向大數(shù)對齊,,因為如果是大數(shù)向小數(shù)對齊,那么就要左移大數(shù)的尾數(shù)部分,,就有可能會丟失大數(shù)的最高有效位,;小數(shù)向大數(shù)對齊是右移小數(shù)的尾數(shù)部分,而丟失小數(shù)的那點精度對結(jié)果來說是無關(guān)緊要的) 2,、尾數(shù)相加 (為什么是相加而不是相加減,,因為減法被轉(zhuǎn)成加法做了,這個應(yīng)該好理解,,計算機(jī)里只有累加器) 3,、對結(jié)果規(guī)格化 (這個可能比較生疏,其實也很好理解,,就像我們十進(jìn)制的科學(xué)記數(shù)法一樣,,我們規(guī)定尾數(shù)為 [1, 10) 之間的數(shù),二進(jìn)制里面我們?nèi)绻矓?shù)不是 1 開頭,我們就移動成 1 開頭,,然后省去存 1,,比如計算結(jié)果尾數(shù)為 0010,那么我就把尾數(shù)左移 3 位得到 1000,, 然后省去 1,,只存 000,當(dāng)然這個過程指數(shù)也會變) 4,、舍入處理 (這個不多說,,就那個意思)
下面來說這個 6755399441055744.0 的來歷,我們利用的就是“對階”的過程,,如果我們把一個雙精度浮點數(shù)加上 1.5 * 2^52,,因為雙精度浮點數(shù)的尾數(shù)部分正好是 52 位,那么在進(jìn)行對階的時候,,我們想要轉(zhuǎn)換的浮點數(shù)如果比 1.5 * 2^52 小,,那么就會向 1.5 * 2^52 對齊,對齊的方法我上面也說了,, 就是右移小數(shù)的尾數(shù),,而這里需要右移 52-目標(biāo)數(shù)的指數(shù)位數(shù),正好把目標(biāo)數(shù)的所有小數(shù)部分全部移掉了,,我們這里忽略舍入過程,,只理解這個數(shù)的來源。那如果目標(biāo)數(shù)比 1.5 * 2^52 大呢,?呵呵,,不用比 6755399441055744.0 大就會失敗,為什么呢,?因為我們這里的目的本來就是把 double 轉(zhuǎn)換成 long,,而 long 的范圍是 (32位數(shù))-2147483648 ~ 2147483647,遠(yuǎn)沒有到 6755399441055744.0 這么大,,所以這個轉(zhuǎn)換的隱含條件還包括了一個 long 的范圍在這里,。其實根據(jù)這個方法看,只要求 long 的范圍在 2^52 的規(guī)模以下就行,。 為了更好的理解這個過程,,我們來看一下這個轉(zhuǎn)換的二進(jìn)制的過程(這里的二進(jìn)制表示沒有對指數(shù)域做偏置,在標(biāo)準(zhǔn) IEEE 中會進(jìn)行偏置,,想了解什么是偏置可以看看下面我翻譯的那篇文章或者參考 IEEE 754 標(biāo)準(zhǔn)):
了解了浮點數(shù)的各種特性之后,,你就可以隨心所欲了,比如馬上就可以知道單精度浮點數(shù)也可以有這么一個數(shù)字,,對,,單精度浮點數(shù)的這個數(shù)字是 1.5 * 2^23。當(dāng)然,,你還可以設(shè)計出非常有意思的一些轉(zhuǎn)換,,不過總體還是根據(jù)浮點數(shù)的結(jié)構(gòu)來。 比如這個地方的 magic number 不一定必須是 1.5 * 2^52 ,,看懂了我上面這張圖的人應(yīng)該都能看出來,,其實這個神奇數(shù)字是有限的,他要滿足2個條件,,一個是它必須是一個大于 1 * 2 ^ 52 的數(shù),,同時必須要小于 1 * 2 ^ 53。這樣才能保證正好把目標(biāo)數(shù)的尾數(shù)部分全部移除,;另一個條件是這個數(shù)的尾數(shù)部分必須沒有用到 double 類型的低 32 位,,因為這樣才能保證最后做 long 轉(zhuǎn)換的時候取到的正好是目標(biāo)數(shù)的整數(shù)部分,否則就亂了,。這里還有一個隱含條件我沒有說,,就是要保證這個 magic number 尾數(shù)部分 32 位之前的那些位必須至少要有一個 1,因為這樣才能保證當(dāng)目標(biāo)數(shù)是負(fù)數(shù)的運算正確性,,這個我就不解釋了,,你仔細(xì)分析一下,,像我上面那張圖一樣手動做一下負(fù)數(shù)的取整操作就明白了,如果還不明白也可以看我下面翻譯的文章,,里面雖然說到了,,但是沒有解釋清楚,我還是建議你手動做一下會理解更加深刻,。那么這樣一來,,我就可以羅列出所有跟 6755399441055744.0 一樣的 magic number了。下面我列了一些供大家參考: ( 1 + 2 ^ ( -20 ) ) * 2 ^ 52 ( 1 + 2 ^ ( -19 ) ) * 2 ^ 52 ………… ( 1 + 2 ^ ( -1 ) ) * 2 ^ 52 —— 這個就是 6755399441055744.0 ( 1 + 2 ^ ( -20 ) + 2 ^ ( -19 ) ) * 2 ^ 52 …………
準(zhǔn)確來說,,這樣的魔法數(shù)字總共有 2 ^ 20 - 1,,唯一排除的一個就是尾數(shù)部分前 20 位全部為 0 的情況,這種情況也僅僅只是因為對負(fù)數(shù)不成立罷了,。你完全可以寫一個簡單的程序把所有這些數(shù)都列舉出來,,這樣一來,這個神奇的魔法數(shù)字也顯得不那么特殊了,。歡迎大家和我討論,。 Lua :luaconf.h
Let’s Get to the (Floating) Point 來源:http:///images/f/fb/Gdmfp.pdf 作者: Chris Hecker
引子: 問:浮點數(shù)運算生來就是邪惡的嗎? 答:也許不是,,只要現(xiàn)代計算機(jī)的芯片中指令的時序是可以被信任的,。 問:但是,他們可以被信任嗎,? 當(dāng)我坐下來寫這篇文章的時候,,這將會是我們那史詩般的“透視校正的紋理貼圖”系列最后一次被安裝了。鑒于此,,我意識到我需要去寫一篇真正關(guān)于浮點數(shù)優(yōu)化的文章,,來作為我們在“透視校正的紋理貼圖”中所做優(yōu)化的結(jié)尾。所以,,我們首先將學(xué)到一些非??岬募记桑⒃谙乱粋€話題中把他們應(yīng)用到我們的紋理貼圖系統(tǒng)中去,。事實上,,這些技術(shù)適用于任何在混合了浮點數(shù)運算和整數(shù)運算的現(xiàn)代處理器上運行的高性能系統(tǒng)。當(dāng)然,,這些技術(shù)是否適合于所有的游戲,,而無論它們是否使用了貼圖技術(shù),這個就仁者見仁智者見智了,。 真實的故事 幾年前,,你不能用“一個混合了浮點數(shù)和整數(shù)運算的應(yīng)用”來描述一個游戲軟件,因為那時沒有游戲使用浮點數(shù)。事實上,,自從個人計算機(jī)出現(xiàn)以來,,浮點數(shù)一直都是慢的象征。當(dāng)時想玩游戲,,你必須去買一個浮點數(shù)的協(xié)處理器,,然后自己動手把它安裝在主板上。這并非是游戲開發(fā)者不愿意使用真正的浮點數(shù)運算,,在當(dāng)時,PC 機(jī)處理整數(shù)運算已經(jīng)有一大堆麻煩了,,更別談去處理浮點數(shù)了,。 我們沒有大量的篇幅來回憶整數(shù)運算是如何過渡到定點數(shù)運算,最后到浮點數(shù)運算的(舉個簡單的例子大家就明白了,,比如 Bresenham 直線算法,,這里我就做個總結(jié):在很長一段時間里,游戲開發(fā)者只在使用高級語言設(shè)計算法原型的時候使用浮點數(shù)運算,。一旦原型寫好了,,程序員為了提高運算速度在真正的實現(xiàn)代碼里通常把浮點數(shù)轉(zhuǎn)換成定點數(shù)。近年來,,我們能夠看到,,浮點數(shù)運算的速度已經(jīng)追上了整數(shù)和定點數(shù),甚至在某些方面還超過了它們,。 為什么會這樣呢,,讓我們來看一下那些被游戲開發(fā)人員使用最多的算術(shù)運算(加減乘、以及希望極少使用的除法)對于定點數(shù),、整數(shù),、浮點數(shù)有什么不同周期時序。表1 顯示了在第三代 Intel 處理器上,、PowerPC 604,、一個現(xiàn)代 MIPS 這三種平臺上不同的周期時間。我們從中可以看出加減法浮點數(shù)操作的期望確實比整數(shù)更快(386,,如果不集成浮點數(shù)元算單元的話其實不算一個高級處理器,,落后其他處理器太多了;處于過渡期的 486 表現(xiàn)中等),。 當(dāng)然了,,純粹看數(shù)字也說明不了全部的情況。比如這個表并沒有說明,,盡管多個周期肯定比單個周期的指令要慢,,比如整數(shù)的加法。但是你在執(zhí)行一條長時間的浮點數(shù)操作的同時,經(jīng)常會同時執(zhí)行其他的整數(shù)運算指令,。關(guān)于這個周期重疊的問題不同的處理器情況也不一樣,,但是對于一些耗時非常長的指令,比如浮點數(shù)的除法,,經(jīng)常是整個執(zhí)行周期和整數(shù)操作指令的執(zhí)行(甚至是浮點數(shù)操作指令)都重疊而不是只有一些,。與之相反,在當(dāng)前的 Intel 處理器上一條耗時很長的整數(shù)指令執(zhí)行時,,卻無法同時執(zhí)行別的指令,,其他的處理器一般也都有所限制。 還有一點需要注意,,就是浮點數(shù)操作并不像表中顯示的那樣快,,因為在進(jìn)行操作之前,還需要加載這些操作數(shù)到浮點數(shù)運算單元中去,,然而浮點數(shù)的加載和存儲通常來說比整數(shù)要慢一些,。更糟糕的是,把浮點數(shù)轉(zhuǎn)換成整數(shù)的指令通常更加慢,。事實上,,盡管浮點數(shù)指令的運算時間其實比同類型的整數(shù)操作要快,但 486 上浮點數(shù)加載,、存儲,、轉(zhuǎn)換的開銷已經(jīng)讓我們有理由為了速度而使用定點數(shù)了。 但是今天,,我們在這篇文章里涉及到的技巧和技術(shù)將會給是浮點數(shù)運算以及定點數(shù)運算帶來無可匹敵的速度上的優(yōu)勢,。 如果它不是浮點數(shù),不要修復(fù)它 跟往常一樣,,我將假設(shè)在討論這個話題之前你已經(jīng)知道了定點數(shù)是如何運作的了,。從數(shù)學(xué)上講,一個定點數(shù)是一個實數(shù)乘以一個固定規(guī)模的正整數(shù),,然后移除結(jié)果的小數(shù)部分獲得的,。這個整數(shù)的規(guī)模有一部分是原實數(shù)的小數(shù)部分編碼得到的最小有效位。這也是為什么這么多年來定點數(shù)作為實數(shù)系統(tǒng)的一個選擇,。一旦我們確定了范圍,,那么我就可以在只有極少數(shù)需要格外處理的情況下使用更快的整數(shù)操作。但是定點數(shù)在大多數(shù)情況下,,有很多問題和一堆特殊情況需要處理,,你必須非常小心的避免上溢和下溢的發(fā)生。而且這些以前所謂的更快速的整數(shù)操作在現(xiàn)在也沒有浮點數(shù)操作快了,。 浮點數(shù)運算是一件輕松的事情,,它背后的主要思想是犧牲一些精度來換取數(shù)的范圍。(意思就是說比如 64 位的浮點數(shù),精度有 52 位,,指數(shù)部分有 11 位,,還有一位符號位) 現(xiàn)在,讓我們忘掉浮點數(shù),,來想象一下我們有一個很巨大的二進(jìn)制定點數(shù),,它有很多位整數(shù)部分和小數(shù)部分。比如有 1000 位整數(shù)部分,,和 1000 位小數(shù)部分,,所以我們可以表示一個數(shù)大到 2^1000 小到 -2^1000。這個假想的定點數(shù)格式有一個巨大的范圍和一個非常高的精度,,范圍的意思是能夠表示的最大的數(shù)和最小的數(shù)的比值,,精度的意思是有效數(shù)字的位數(shù)。所以,,我們能夠處理在我們模擬星系的系統(tǒng)中那些難以置信的大數(shù),,在保持星體間距離的準(zhǔn)確性的同時我們也能夠計算亞原子的半徑,。 但是,,大多數(shù)應(yīng)用并非是處處都需要這么高的精度。很多應(yīng)用更多的是需要范圍來表示巨大的值,,比如星球之間的距離,;或者是極小的值,比如原子之間的距離,。但是通常不需要表示確切的數(shù)在處理兩個不同規(guī)模的數(shù)的時候,。 浮點數(shù)利用了這個精度和范圍在使用上的差異,在存儲的時候僅僅用了很少的幾位就記錄下了很大的范圍(事實上甚至比我們假想的 2000 位定點數(shù)還要大),。浮點數(shù)通過把實數(shù)的指數(shù)和尾數(shù)分開存儲來達(dá)到這個效果,。就好像科學(xué)表示法一樣,一個數(shù)比如 2.345*10^35 的尾數(shù)是 2.345,,指數(shù)是 35(有時你也會看到不同的表示方式,,但是他們都是等價的)。這個數(shù)只有 4 位精度,,但是它的范圍確實非常的大,。 表示數(shù)值的精度同樣是一件重要的事情。當(dāng)指數(shù)是 35 時,,改變第一位數(shù)字將按照 10^35 的規(guī)模來改變這個數(shù),,但是當(dāng)指數(shù)為 0 時,改變第一位數(shù)字僅僅只按 1 的規(guī)模來改變,。這個方法讓你在處理“埃米”級別(納米的十分之一)的規(guī)模的時候能夠獲得“埃米”級別的精確度,,對于星系規(guī)模來說也是一樣。 IEEE 浮點數(shù)標(biāo)準(zhǔn)規(guī)定了浮點數(shù)的表示方法以及如何操作浮點數(shù)。其中 IEEE 規(guī)定的浮點數(shù)格式中我們關(guān)心的只有兩種:單精度浮點數(shù)和雙精度浮點數(shù),。它們使用相同的轉(zhuǎn)換方程來把二進(jìn)制浮點數(shù)表示變成實數(shù)表示,。你會發(fā)現(xiàn)它就像是一個二進(jìn)制的科學(xué)記數(shù)法:
單精和雙精這兩種格式唯一不同的地方是上面這個格式中一些值的范圍。所以我們接下來會按照順序指出每個部分的不同點,。 我們從上面格式的右手側(cè)開始,。尾數(shù)表示方法(1.mantissa)在第一次看的時候略顯奇怪,但是當(dāng)你意識到它是一個二進(jìn)制數(shù)的時候時候就會覺得它清晰了一點,。你同時也應(yīng)該意識到這是一個規(guī)范的二進(jìn)制數(shù),。“規(guī)范”在科學(xué)表示法里意思是尾數(shù)是大于等于 1 且小于 10(換句話說是一個不包括 0 的一位數(shù)),。我們上面那個科學(xué)表示法的例子 2.345*10^35 就是規(guī)范的,,而 234500*10^30 就不規(guī)范。 一個規(guī)范的二進(jìn)制數(shù)意思就是它會移動它的數(shù)位直到最高位為 1,。仔細(xì)想想這個概念的意思,。(001011 - > 1011)如果這個二進(jìn)制數(shù)有前導(dǎo) 0,那么我們能夠用指數(shù)的一部分來表示它們,,就像我們在十進(jìn)制科學(xué)記數(shù)法中所做的一樣,。因為最高位始終為 1,所以我們能夠免去存儲這個 1,,把它作為我們這種表示法的一種默認(rèn)值就可以了,。就像十進(jìn)制科學(xué)記數(shù)法中保持尾數(shù)部分在 1 到 10 之間一樣,二進(jìn)制浮點數(shù)的尾數(shù)在 1 到 2 之間(大于等于 1 小于 2),。 接下來,,二進(jìn)制指數(shù)的小數(shù)點是向左移還是向右移取決于指數(shù)是正還是負(fù)。這跟十進(jìn)制科學(xué)記數(shù)法類似,,當(dāng)指數(shù)的小數(shù)點移動的時候,,就補(bǔ) 0。指數(shù)域位數(shù)相同的浮點數(shù)比定點數(shù)有范圍表示上的優(yōu)勢,。其實定點數(shù)有一個隱含的小數(shù)點而它的所有位數(shù)其實都是指數(shù),,浮點數(shù)通過保存指數(shù)域來移動二進(jìn)制小數(shù)點(這也就是浮點的意思)。如果用 8 位來保存指數(shù),,那么一個 32 位數(shù)能夠表示的范圍就是 2^127 到 2^-127,,而最好的定點數(shù)僅僅能表示 2^32 到 0 和 2^16 到 2^-16 或者是 0 到 2^-32,而且還不能同時表示這兩個范圍,。當(dāng)然,,天下沒有免費的午餐,32 位定點數(shù)比浮點數(shù)的精度更高,,因為定點數(shù)有 32 位有效數(shù)字,,而浮點數(shù)去掉指數(shù)部分后只有 24 位有效數(shù)字,。 你會注意到 IEEE 標(biāo)準(zhǔn)中指數(shù)表達(dá)式實際上是一種指數(shù)的偏置。偏置的目的是使指數(shù)域的值始終為正,。換句話說,,假設(shè)一個 8 位的指數(shù),那么它偏置的值就是 127,,如果一個沒有偏置的指數(shù)是 -126那么偏置后的指數(shù)為 1,。同樣的道理,一個偏置后指數(shù)域的值為 254,,那么沒有偏置的指數(shù)就是 127,。全 0 和全 1 的指數(shù)值保留做無窮和 0 的指數(shù)值,當(dāng)然我們沒有足夠的值去對應(yīng)所有的特殊數(shù),。 最后,,符號位(sign bit)的意思是這個數(shù)為正還是為負(fù)。不同于二進(jìn)制補(bǔ)碼的表示法,,浮點數(shù)的正負(fù)僅僅由符號位決定,。我們后面會談到這樣做的目的。表2 顯示了單精度浮點數(shù)和雙精度浮點數(shù)在 IEEE 規(guī)定的格式位數(shù),,F(xiàn)igure 1 顯示了他們的是什么樣的,,最高有效位就是符號位。
一個例子 讓我們來看一個例子,,把一個十進(jìn)制數(shù)轉(zhuǎn)換成一個單精度二進(jìn)制浮點數(shù),。我將用 8.75 舉例,,因為它比較簡單,,甚至可以用手寫來轉(zhuǎn)換,但是它也能夠展現(xiàn)一些復(fù)雜的地方,。首先,,我們將它變成一個二進(jìn)制定點數(shù)—— 1000.11(小數(shù)的二進(jìn)制轉(zhuǎn)換方法),小數(shù)部分 .75 就是 2^-1 + 2^-2,,所以是 .11,。然后,我們把這個二進(jìn)制數(shù)左移 3 位使它規(guī)格化,,也就是 1.00011 * 2^3,。最后,我們通過給指數(shù)加 127 來偏置指數(shù),,那么指數(shù)就是 130(10000010),。那么我們的浮點數(shù)尾數(shù)部分就是 1.00011,當(dāng)然,,前置 1 是隱式的,。我們的數(shù)是正的,,所以符號位是 0。所以 8.75 最后的二進(jìn)制浮點數(shù)的樣子就如 Figure 2 所示,,好了,,現(xiàn)在我們已經(jīng)熟悉了浮點數(shù)以及它的表示方法,接下來,,我們可以學(xué)一些技巧了,。
轉(zhuǎn)換 毫不夸張的說,我注意到在某些處理器上浮點數(shù)轉(zhuǎn)換成整數(shù)相當(dāng)?shù)穆?。在奔騰處理器上,,把浮點數(shù)轉(zhuǎn)換成整數(shù)存下來的指令“FIST”(FIST 指令轉(zhuǎn)換和舍入在堆棧頂 ST(0) 的源值為符號整數(shù)并復(fù)制它至規(guī)定的 16 位或 32 位內(nèi)存單元。源可以是任一浮點數(shù)據(jù)類型,,包括單精度,、雙精度或擴(kuò)展的雙精度浮點值?!?/span>《64位微處理器應(yīng)用編程》)需要 6 個 CPU 周期,,而乘法運算都僅僅只需要 3 個周期,這樣你就明白我說的慢是什么意思了,。更糟糕的是,,F(xiàn)IST 指令同時占據(jù)浮點數(shù)通道和整數(shù)通道,所以其他的指令也沒有辦法執(zhí)行,。但是這里還有個選擇,,那就是我們動用我們學(xué)過的浮點數(shù)知識來寫一個我們自己版本的 FIST,并且只需要用到普通的浮點數(shù)加法,。 在做浮點數(shù)加法之前,,CPU 需要對齊二進(jìn)制小數(shù)點,小數(shù)點不對齊是沒辦法把尾數(shù)相加的,。對齊的方法通常是左移較小的數(shù)的小數(shù)點,。舉例來說,如果我們想把兩個用科學(xué)記數(shù)法表示的十進(jìn)制數(shù) 2.345 * 10^35 和 1.0 * 10^32 相加,,我們把較小的數(shù)的小數(shù)點左移 3 位來使他們達(dá)到同一個級別,。然后再相加:2.345*10^35 + 0.001*10^35 = 2.346*10^35。二進(jìn)制數(shù)跟這個道理是一樣的,。 我們能夠利用這種對齊移動的方式來把浮點數(shù)位數(shù)表示弄的跟整數(shù)位數(shù)表示的一樣,,然后我們就可以像普通整數(shù)那樣加了。理解這個的關(guān)鍵在于我們想要的整數(shù)已經(jīng)存在于浮點數(shù)的位數(shù)中了,,但是它被規(guī)范化了,,所以移動它尾數(shù)部分的前導(dǎo)位,比如 8.75,,就像 Figure 2 顯示的那樣,。整數(shù)部分 8 是隱式的那一位加上尾數(shù)部分前面 3 個 0 組成的,。尾數(shù)部分接著的 11 是二進(jìn)制 .75 的小數(shù)部分,等著轉(zhuǎn)換成一個定點數(shù),。 想象一下當(dāng)你把 2 個浮點數(shù)相加時,,比如 2^8 = 256 加到 8.75,如 Figure 3,。為了做這個加法,,因為指數(shù)不相同,所以 CPU 會左移 5 位 8.75 的二進(jìn)制小數(shù)點(8 - 3 = 5),。加法把 256 隱含的那一位加到了小數(shù)點對齊之后的 8.75 中,,然后當(dāng)結(jié)果做了規(guī)范化之后,在 256 中隱含的那一位仍然是隱含的,,所以 8.75 就下移了,。你在 Figure 3 的結(jié)果尾數(shù)部分可以看到 8.75。那當(dāng)我加 233 時呢,,或者加跟尾數(shù)的寬度一樣大的數(shù)呢,?就像你想的那樣,8.75 的尾數(shù)會移動 23 - 3 = 20 位,,只留下1000,,這個的值是 8(因為 .75 被我們移除掉了,單精度浮點數(shù)的尾數(shù)只有 23 位,,雖然這個時候舍入模式就會生效,,但是為了簡化,我們這里假設(shè)舍入為 0),。如果我們蓋住符號位和指數(shù)部分,,直接讀尾數(shù)部分,那么我們就得到了把 8.75 轉(zhuǎn)換成整數(shù)的結(jié)果 8,。
這個技巧只對正數(shù)有效,,如果你嘗試把一個負(fù)數(shù)轉(zhuǎn)換過來就會發(fā)現(xiàn)出錯了,。你可以通過自己手動操作移位來看看為什么會出錯,。我發(fā)現(xiàn)兩個正數(shù)相減比一正數(shù)一負(fù)數(shù)相加要容易。比如計算 2^23 + (-8.75),,而 2^23 - 8.75 要容易一些,。這是由于符號位的表示方法導(dǎo)致的(用一張紙一支筆你可以看得更加清楚)。所以,,當(dāng)你在做減法的對齊的時候,,8.75 減去一個大的尾數(shù),如果所有位都為 0 了,,就會從那個隱含的位借一,。這個最初看起來不錯,,尾數(shù)是正確值 -8.75,但是執(zhí)行規(guī)范化步驟之后,,整個數(shù)就亂了,,因為我們從隱含位借了 1,那么這個隱含位就不再是最高有效位了,,所以規(guī)范化的操作移動整個數(shù)之后,,這個數(shù)就亂了。 但是,,等等,,好像并非全亂了。我們需要的其實只是一位尾數(shù)讓我們可以借位罷了,,這樣我們那個隱含位仍然保留并正確移動,。我們可以通過乘以 1.5 來實現(xiàn)這個多出的一位,1.5 在二進(jìn)制中是 1.1,,那么第一個 1 就成為隱含位,,第二個 1 就變成了尾數(shù)的最高有效位拿來借位。這樣,,無論是正數(shù)還是負(fù)數(shù)的規(guī)范化操作都能夠正確的進(jìn)行了,。負(fù)數(shù)就在最高位加 1 來完成二進(jìn)制補(bǔ)碼的表示。 當(dāng)你僅處理一兩個正數(shù)和負(fù)數(shù)時,,計算掩碼已經(jīng)夠麻煩了,,更別談你想要透明的進(jìn)行這種處理,計算如何使用掩碼對于高位數(shù)來說非常緩慢,。但是,,這里也有一個小技巧。如果你把這個浮點數(shù)減去我們剛剛用上面轉(zhuǎn)換成整數(shù)方法得到的那個整數(shù),,那么就會正確的移除掉所有的高位,,正數(shù)就置 0,負(fù)數(shù)就置 1,。 你會發(fā)現(xiàn)這個技巧只使用到了 32 位中的一部分,,因為還有指數(shù)部分和符號位。并且當(dāng)我們使用“1.5”的小技巧時,,我們還會失去額外的一位來確保正數(shù)和負(fù)數(shù)同樣適用,。但是,我們可以通過使用一個雙精度數(shù)來作為暫時的轉(zhuǎn)換這樣就可以避免范圍問題和掩碼問題,。如果我們做加法時把我們的數(shù)當(dāng)作一個雙精度類型(確保在做整數(shù)轉(zhuǎn)換時加的值是 1.5 * 2^52)并且只讀取最低的 32 位有效數(shù)字作為整數(shù),,我們就得到了一個 32 位精度的數(shù)并且不需要使用掩碼。因為指數(shù)部分和符號位移到了雙精度值的第二個 32 位中,。 總之,,在做整數(shù)轉(zhuǎn)換時我們可以通過加一個大數(shù)來控制怎么去移動對齊,。我們一直移位直到只保留整數(shù)部分,或者可以移動一定值保留一定的小數(shù)精度,。 使用這些技巧看起來確實麻煩,,但是在一些處理器上,像 X86 系列,,這會帶來很大的不同,。在奔騰處理器上使用 FIST 指令,你需要 6 個 CPU 周期,,在此之間你無法執(zhí)行任何其他的指令,。但是使用這個加法的技巧,你就可以節(jié)省出 3 個周期來進(jìn)行別的整數(shù)指令的運行,。你同時能夠控制有多少位小數(shù)精度而不是一味的全部轉(zhuǎn)換成整數(shù),。 你的觀點呢? 在我做總結(jié)之前,,我想告訴你一些別的技巧,,來供你做判斷做不做這些改變。對指數(shù)做偏置是因為這樣在做兩個數(shù)的比較的時候更加方便,。指數(shù)永遠(yuǎn)是正的,,這樣一個大數(shù)比一個小數(shù)要大在比較指數(shù)部分的時候就已經(jīng)確定了。符號位的存在導(dǎo)致比較更加麻煩,,但是單符號值卻沒有問題,。當(dāng)然,你可以移除符號位來取得浮點數(shù)的絕對值,。 我已經(jīng)非常接近最酷的技巧了,,這個技巧就是在做坑爹的除法的時候重疊整數(shù)指令的運行。但是我沒有深入探討這個話題,,這個就要等到下一次了,。
最后作者推薦了一些讀物: The Art of Computer Programming, Vol. 2: Seminumerical Algorithms (Addison-Wesley, 1981) by D.Knuth Graphics Gems series from AP Professional
|