1. 程式人生 > >【演算法】一個比系統自帶的sqrt函式快四倍的sqrt

【演算法】一個比系統自帶的sqrt函式快四倍的sqrt



看完之後,感覺碼程式碼的能力不如數學好。演算法還是很重要的!再加上程式設計師的創造性,一定會有更好的會出現。

  我們平時經常會有一些資料運算的操作,需要呼叫sqrt,exp,abs等函式,那麼時候你有沒有想過:這個些函式系統是如何實現的?就拿最常用的sqrt函式來說吧,系統怎麼來實現這個經常呼叫的函式呢?

  雖然有可能你平時沒有想過這個問題,不過正所謂是“臨陣磨槍,不快也光”,你“眉頭一皺,計上心來”,這個不是太簡單了嘛,用二分的方法,在一個區間中,每次拿中間數的平方來試驗,如果大了,就再試左區間的中間數;如果小了,就再拿右區間的中間數來試。比如求sqrt(16)的結果,你先試(0+16)/2=8,8*8=64,64比16大,然後就向左移,試(0+8)/2=4,4*4=16剛好,你得到了正確的結果sqrt(16)=4。然後你三下五除二就把程式寫出來了:

float SqrtByBisection(float n) //用二分法 
{ 
    if(n<0) //小於0的按照你需要的處理 
        return n; 
    float mid,last; 
    float low,up; 
    low=0, up=n; 
    mid=(low+up)/2; 
    do
    {
        if(mid*mid>n)
            up=mid; 
        else 
            low=mid;
        last=mid;
        mid=(up+low)/2; 
    }
while(abs(mid-last) > eps);//精度控制 return mid; }

  然後看看和系統函式效能和精度的差別(其中時間單位不是秒也不是毫秒,而是CPU Tick,不管單位是什麼,統一了就有可比性) 

  從圖中可以看出,二分法和系統的方法結果上完全相同,但是效能上整整差了幾百倍。為什麼會有這麼大的區別呢?難道系統有什麼更好的辦法?難道。。。。哦,對了,回憶下我們曾經的高數課,曾經老師教過我們“牛頓迭代法快速尋找平方根”,或者這種方法可以幫助我們,具體步驟如下:

求出根號a的近似值:首先隨便猜一個近似值x,然後不斷令x等於x和a/x的平均數,迭代個六七次後x的值就已經相當精確了。
例如,我想求根號2等於多少。假如我猜測的結果為4,雖然錯的離譜,但你可以看到使用牛頓迭代法後這個值很快就趨近於根號2了:
(4 + 2/4) / 2 = 2.25
(2.25 + 2/2.25) / 2 = 1.56944..
(1.56944.. + 2/1.56944..) / 2 = 1.42189..
(1.42189.. + 2/1.42189..) / 2 = 1.41423..
....


這種演算法的原理很簡單,我們僅僅是不斷用(x,f(x))的切線來逼近方程x^2-a=0的根。根號a實際上就是x^2-a=0的一個正實根,這個函式的導數是2x。也就是說,函式上任一點(x,f(x))處的切線斜率是2x。那麼,x-f(x)/(2x)就是一個比x更接近的近似值。代入 f(x)=x^2-a得到x-(x^2-a)/(2x),也就是(x+a/x)/2。

  相關的程式碼如下:

float SqrtByNewton(float x)
{
    float val = x; //最終
    float last; //儲存上一個計算的值
    do
    {
        last = val;
        val =(val + x/val) / 2;
    }while(abs(val-last) > eps);
    return val;
}

  然後我們再來看下效能測試:

  哇塞,效能提高了很多,可是和系統函式相比,還是有這麼大差距,這是為什麼呀?想啊想啊,想了很久仍然百思不得其解。突然有一天,我在網上看到一個神奇的方法,於是就有了今天的這篇文章,廢話不多說,看程式碼先:

float InvSqrt(float x)
{
    float xhalf = 0.5f*x;
    int i = *(int*)&x; // get bits for floating VALUE 
    i = 0x5f375a86- (i>>1); // gives initial guess y0
    x = *(float*)&i; // convert bits BACK to float
    x = x*(1.5f-xhalf*x*x); // Newton step, repeating increases accuracy
    x = x*(1.5f-xhalf*x*x); // Newton step, repeating increases accuracy
    x = x*(1.5f-xhalf*x*x); // Newton step, repeating increases accuracy
    return 1/x;
}

  然後我們最後一次來看下效能測試:

  這次真的是質變了,結果竟然比系統的還要好。。。哥真的是震驚了!!!哥吐血了!!!一個函式引發了血案!!!血案,血案。。。

  到現在你是不是還不明白那個“鬼函式”,到底為什麼速度那麼快嗎?不急,先看看下面的故事吧:

Quake-III Arena (雷神之錘3)是90年代的經典遊戲之一。該系列的遊戲不但畫面和內容不錯,而且即使計算機配置低,也能極其流暢地執行。這要歸功於它3D引擎的開發者約翰·卡馬克(John Carmack)。事實上早在90年代初DOS時代,只要能在PC上搞個小動畫都能讓人驚歎一番的時候,John Carmack就推出了石破天驚的Castle Wolfstein,然後再接再勵,doom, doomII, Quake...每次都把3-D技術推到極致。他的3D引擎程式碼資極度高效,幾乎是在壓榨PC機的每條運算指令。當初MS的Direct3D也得聽取他的意見,修改了不少API。

最近,QUAKE的開發商ID SOFTWARE 遵守GPL協議,公開了QUAKE-III的原始碼,讓世人有幸目睹Carmack傳奇的3D引擎的原碼。這是QUAKE-III原始碼的下載地址:
http://www.fileshack.com/file.x?fid=7547

我們知道,越底層的函式,呼叫越頻繁。3D引擎歸根到底還是數學運算。那麼找到最底層的數學運算函式(在game/code/q_math.c), 必然是精心編寫的。裡面有很多有趣的函式,很多都令人驚奇,估計我們幾年時間都學不完。在game/code/q_math.c裡發現了這樣一段程式碼。它的作用是將一個數開平方並取倒,經測試這段程式碼比(float)(1.0/sqrt(x))快4倍:

float Q_rsqrt( float number )
{
    long i;
    float x2, y;
    const float threehalfs = 1.5F;
    x2 = number * 0.5F;
    y   = number;
    i   = * ( long * ) &y;   // evil floating point bit level hacking
    i   = 0x5f3759df - ( i >> 1 ); // what the fuck?
    y   = * ( float * ) &i;
    y   = y * ( threehalfs - ( x2 * y * y ) ); // 1st iteration
    // y   = y * ( threehalfs - ( x2 * y * y ) ); // 2nd iteration, this can be removed

    #ifndef Q3_VM
    #ifdef __linux__
         assert( !isnan(y) ); // bk010122 - FPE?
    #endif
    #endif
    return y;
}  

函式返回1/sqrt(x),這個函式在影象處理中比sqrt(x)更有用。 

注意到這個函式只用了一次疊代!(其實就是根本沒用疊代,直接運算)。編譯,實驗,這個函式不僅工作的很好,而且比標準的sqrt()函式快4倍!要知道,編譯器自帶的函式,可是經過嚴格仔細的彙編優化的啊!

這個簡潔的函式,最核心,也是最讓人費解的,就是標註了“what the fuck?”的一句:

 i = 0x5f3759df - ( i >> 1 );

再加上:

y  = y * ( threehalfs - ( x2 * y * y ) ); 

兩句話就完成了開方運算!而且注意到,核心那句是定點移位運算,速度極快!特別在很多沒有乘法指令的RISC結構CPU上,這樣做是極其高效的。

演算法的原理其實不復雜,就是牛頓迭代法,用x-f(x)/f'(x)來不斷的逼近f(x)=a的根。

沒錯,一般的求平方根都是這麼迴圈迭代算的,但是卡馬克(quake3作者)真正牛B的地方是他選擇了一個神祕的常數 0x5f3759df 來計算那個猜測值,就是我們加註釋的那一行,那一行算出的值非常接近1/sqrt(n),這樣我們只需要2次牛頓迭代就可以達到我們所需要的精度。好吧如果這個還不算NB,接著看:

普渡大學的數學家Chris Lomont看了以後覺得有趣,決定要研究一下卡馬克弄出來的這個猜測值有什麼奧祕。Lomont也是個牛人,在精心研究之後從理論上也推匯出一個最佳猜測值,和卡馬克的數字非常接近,0x5f37642f。卡馬克真牛,他是外星人嗎?

傳奇並沒有在這裡結束。Lomont計算出結果以後非常滿意,於是拿自己計算出的起始值和卡馬克的神祕數字做比賽,看看誰的數字能夠更快更精確的求得平方根。結果是卡馬克贏了... 誰也不知道卡馬克是怎麼找到這個數字的。

最後Lomont怒了,採用暴力方法一個數字一個數字試過來,終於找到一個比卡馬克數字要好上那麼一丁點的數字,雖然實際上這兩個數字所產生的結果非常近似,這個暴力得出的數字是0x5f375a86。

參考:<IEEE Standard 754 for Binary Floating-Point Arithmetic><FAST INVERSE SQUARE ROOT>

最後,給出最精簡的1/sqrt()函式:

float InvSqrt(float x)
{
    float xhalf = 0.5f*x;
    int i = *(int*)&x; // get bits for floating VALUE 
    i = 0x5f375a86- (i>>1); // gives initial guess y0
    x = *(float*)&i; // convert bits BACK to float
    x = x*(1.5f-xhalf*x*x); // Newton step, repeating increases accuracy
    return x;
}  

大家可以嘗試在PC機、51、AVR、430、ARM、上面編譯並實驗,驚訝一下它的工作效率。

前兩天有一則新聞,大意是說 Ryszard Sommefeldt 很久以前看到這麼樣的一段 code (可能出自 Quake III 的 source code):

float InvSqrt (float x) 
{
    float xhalf = 0.5f*x;
    int i = *(int*)&x;
    i = 0x5f3759df - (i>>1);
    x = *(float*)&i;
    x = x*(1.5f - xhalf*x*x);
    return x;
}

他一看之下驚為天人,想要拜見這位前輩高人,但是一路追尋下去卻一直找不到人;同時間也有其他人在找,雖然也沒找到出處,但是 Chris Lomont 寫了一篇論文 (in PDF) 解析這段 code 的演算法 (用的是 Newton’s Method,牛頓法;比較重要的是後半段講到怎麼找出神奇的 0x5f3759df 的)。 

PS. 這個 function 之所以重要,是因為求“開根號倒數”這個動作在 3D 運算 (向量運算的部份) 裡面常常會用到,如果你用最原始的 sqrt() 然後再倒數的話,速度比上面的這個版本大概慢了四倍吧… XD
PS2. 在他們追尋的過程中,有人提到一份叫做 MIT HACKMEM 的檔案,這是 1970 年代的 MIT 強者們做的一些筆記 (hack memo),大部份是 algorithm,有些 code 是 PDP-10 asm 寫的,另外有少數是 C code (有人整理了一份列表)

相關推薦

演算法一個系統sqrt函式sqrt

 看完之後,感覺碼程式碼的能力不如數學好。演算法還是很重要的!再加上程式設計師的創造性,一定會有更好的會出現。   我們平時經常會有一些資料運算的操作,需要呼叫sqrt,exp,abs等函式,那麼時候你有沒有想過:這個些函式系統是如何實現的?就拿最常用的sqrt函式

一個隊列類的實現(delphi的速度70)(線程安全版本)

initial unlock use for in 線程 self. private aps 自帶 unit sfContnrs; interface {$DEFINE MULTI_THREAD_QUEUE} //線程安全版本,如果不需要線程安全,請註釋掉此行代碼 {

JavaSpring和Tomcat的連線池實現資料庫操作

@[toc] 前言 前面我們已經用Spring和傳統的Jdbc實現資料庫操作、Spring和JdbcTemplate實現資料庫操作。但是這些都是基於直連的資料來源進行的,現在我們將介紹基於連線池的資料來源進行資料庫操作。前面幾個步驟都相同。 建立資料庫 首先建立我們的資料庫(這裡我使用的是Mysql)

演算法蟻群系統中的細節辨析

1. 引數B很敏感,從1-500不等,需要簡單調參 B的取值:1、500、250、二分法。 2. 引數zeta的設定 在TSP問題中(最小化):zeta_(r, s)=1/d_(r,s) 在0-1KP問題中(最大化):zeta_i = vi/wi 3. 引數delt

Java實現MD5演算法過程,並利用MD5函式進行對比校驗

文章目錄 一、環境說明 二、演算法原理概述 三、程式設計 資料結構 重要模組步驟 四、執行結果 一、環境說明 作業系統:window10 程式語言:Java (J

其他Windows系統的DOS視窗

寫在前面:   整理自網路   記錄學習過程,方便複習   說明 DOS全稱為Disk Operating System,意思是“磁碟作業系統” DOS是個人計算機上的一類作業系統,windows之前的計算機作業系統,現在仍在使用 DOS視窗是Win

其他Windows系統的DOS窗口

tex bsp rowspan 寫在前面 -s 大型機 width edi 使用 寫在前面:   整理自網絡   記錄學習過程,方便復習 說明 DOS全稱為Disk Operating System,意思是“磁盤操作系統” DOS是個人計算機上的一類操作

原創windows 7 通過系統工具進行MD5 SHA1 SHA256 校驗oracle 19c 安裝文件

home windows oracle pan sha linu color 示例 md5 windows 7 通過系統自帶工具進行MD5 SHA1 SHA256 校驗oracle 19c 安裝文件 工具:certutil 用法:certutil -hashfil

小程序獲取微信 的 收貨地址獲取和整理

code blog itl ucc success span .info toa pan 1、wx.chooseAddress(OBJECT) if(wx.chooseAddress){ wx.chooseAddress({ success: function (r

系統Python2.7安裝演算法包setuptools-40.1.1.zip

租戶業務需求安裝setuptools-40.1.1.zip 1、上傳setuptools-40.1.1.zip包 2、解壓 # unzip setuptools-40.1.1.zip 3、安裝 # cd setuptools-40.1.1/  # python setup.py insta

WMI應用(一個系統的測試WMI語句的工具)

1. 開始-執行-輸入:wbemtest 回車 2. 單擊"連線", 輸入:root\cimv2 回車; 或者ROOT\SecurityCenter  3. 單擊"查詢", 輸入: SELECT * FROM Win32_Process 應用; 或者SELECT * FROM A

一個普通車民對比亞迪的評價——感覺亞迪這兩年進步還可以,果斷轉載了

分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow 也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!        

QT一個基於QT的簡單的工資管理系統

1、資料採用儲存到本地,檔案中有幾處涉及到地址的地方需要改成自己的,做的第一版練手程式,未作嚴格的巨集定義。 2、檔案操作為C++的檔案操作,基本只涉及c++ 3、只作為簡單的qt小程式。 4、還有一些bug 由於現在重新裝了系統,所以已經沒有了執行環境。   原始

演算法C++用連結串列實現一個箱子排序附原始碼詳解

01 箱子排序 1.1 什麼是分配排序? 分配排序的基本思想:排序過程無須比較關鍵字,而是通過"分配"和"收集"過程來實現排序.它們的時間複雜度可達到線性階:O(n)。 1.2 什麼是箱子排序? 箱子排序是分配排序的一種,箱子排序也稱桶排序(Bucket Sort),其基本思想是:設定若干個箱子,依次掃描待

演算法計算數字k在0到n中的出現的次數,k可能是0~9的一個

思路: 遍歷每一個數,同時每一個數都遍歷每一位,遍歷每一位的方法是不斷地整除10,直到整除為0。 #include <stdio.h> int digitCounts(int k, int n) { int sum=0; for(int i=0;i<=n;i++)

演算法判斷一個字串的所有字元是否全部不同 java程式碼實現

package com.billkang.algorithm; import java.util.HashSet; import java.util.Set; /** * * @author K

演算法給定兩個字串,確定一個字串重新排列後能否變成另一個字串(java實現)

分析:兩個字串的長度都不相同時,不可能是變位詞 package com.billkang.algorithm; import java.util.Arrays; /** * 給定兩個字串,確

應用演算法資訊流-推薦系統的去重策略

聊兩個問題,它們看似和推薦系統沒有必然關係,但實際上, 在你構建自己的推薦系統的時候,不可避免地會遇到這兩個問題。 去重是剛需 在推薦系統中,有一個剛需就是去重,那麼說在哪些地方有去重的需求呢? 主要是在兩個地方:一個是內容源去重,另一個是不重複給使用者推薦。

演算法將n個有序集合合併成一個新的有序集合

import java.util.*;public class Main {public static void main(String[] args) {List<List<Integer>> lists = new ArrayList<>

iOS一個完整的簡單的呼叫系統相機和相簿設定頭像

1.Xcode8,iOS10的許可權設定(不設定會崩潰): 找到專案的info.plist檔案,右鍵open As,以Source Code的形式開啟,將以下程式碼新增進去: 相機許可權設定: <key>NSCameraUsageDescription</