[英]Where can I find the world's fastest atof implementation?
我正在 IA32 上尋找一個極快的 atof() 實現,該實現針對 US-en 語言環境、ASCII 和非科學符號進行了優化。 Windows 多線程 CRT 在這里慘遭失敗,因為它在每次調用 isdigit() 時檢查區域設置更改。 我們目前最好的來自 perl + tcl 的 atof 實現中最好的,並且比 msvcrt.dll 的 atof 好一個數量級。 我想做得更好,但沒有想法。 BCD 相關的 x86 指令看起來很有希望,但我無法讓它超越 perl/tcl C 代碼。 任何 SO'ers 都可以挖掘到那里最好的鏈接嗎? 也歡迎非基於 x86 匯編的解決方案。
基於初步答案的澄清:
大約 2 ulp 的不准確性對於此應用程序來說沒有問題。
要轉換的數字將以 ascii 消息的形式通過網絡以小批量的形式到達,我們的應用程序需要以盡可能低的延遲來轉換它們。
你的精度要求是什么? 如果你真的需要它“正確”(總是得到最接近指定十進制的浮點值),可能很難擊敗標准庫版本(除了刪除語言環境支持,你已經完成了),因為這需要進行任意精度的算術運算。 如果您願意容忍一兩個 ulp 錯誤(並且比次正常的錯誤更多),那么 cruzer 提出的這種方法可以工作並且可能更快,但它絕對不會產生 <0.5ulp 的輸出。 您將在分別計算整數和小數部分時在精度方面做得更好,並在最后計算分數(例如,對於 12345.6789,將其計算為 12345 + 6789 / 10000.0,而不是 6*.1 + 7*.01 + 8 *.001 + 9*0.0001) 因為 0.1 是一個無理數的二進制分數,當你計算 0.1^n 時,誤差會迅速累積。 這也使您可以使用整數而不是浮點數進行大部分數學運算。
自 (IIRC) 286 以來,BCD 指令尚未在硬件中實現,現在只是微編碼。 它們不太可能具有特別高的性能。
我剛剛完成編碼的這個實現的運行速度是我桌面上內置的“atof”的兩倍。 它在 2 秒內轉換了 1024*1024*39 個數字輸入,與我系統的標准 gnu 'atof' 相比需要 4 秒。 (包括設置時間和獲取內存等等)。
更新:抱歉,我必須撤銷兩倍快的索賠。 如果您要轉換的內容已經在字符串中,則速度會更快,但是如果您將硬編碼字符串文字傳遞給它,則它與 atof 大致相同。 但是,我將把它留在這里,因為可能通過對 ragel 文件和狀態機進行一些調整,您可能能夠為特定目的生成更快的代碼。
https://github.com/matiu2/yajp
您感興趣的文件是:
https://github.com/matiu2/yajp/blob/master/tests/test_number.cpp
https://github.com/matiu2/yajp/blob/master/number.hpp
您也可能對進行轉換的狀態機感興趣:
在我看來,您想(手動)構建相當於每個狀態處理第 N 個輸入數字或指數數字的狀態機; 這個狀態機的形狀像一棵樹(沒有循環!)。 目標是盡可能地進行整數運算,並且(顯然)在狀態中隱式地記住狀態變量(“前導減號”、“位置 3 處的小數點”),以避免分配、存儲和以后獲取/測試這些值. 僅在輸入字符上使用簡單的舊“if”語句實現狀態機(因此您的樹將成為一組嵌套的 if)。 對緩沖區字符的內聯訪問; 您不希望對getchar
的函數調用減慢您的速度。
可以簡單地抑制前導零; 你可能需要一個循環來處理長得可笑的前導零序列。 無需將累加器歸零或乘以 10 即可收集第一個非零數字。 前 4-9 位非零數字(對於 16 位或 32 位整數)可以通過整數乘以常數值 10 來收集(大多數編譯器將其轉換為幾次移位和加法)。 [最重要的是:在找到非零數字之前,零數字不需要任何工作,然后需要對 N 個連續零乘以 10^N; 您可以將所有這些連接到狀態機]。 根據您機器的字長,可以使用 32 位或 64 位乘法來收集前 4-9 位之后的數字。 由於您不關心准確性,因此您可以在收集 32 或 64 位值后簡單地忽略數字; 根據您的應用程序對這些數字的實際操作,我猜想當您有一些固定數量的非零數字時,您實際上可以停止。 在數字字符串中找到的小數點只會導致狀態機樹中的一個分支。 該分支知道該點的隱含位置,因此稍后如何適當地按 10 的冪進行縮放。 如果你不喜歡這段代碼的大小,你可以努力組合一些狀態機子樹。
[最重要的是:將整數和小數部分保留為單獨的(小)整數。 這將需要在最后進行額外的浮點運算來組合整數和小數部分,可能不值得]。
[頂部:將數字對的 2 個字符收集成 16 位值,查找 16 位值。 這避免了為了內存訪問而交易的寄存器中的乘法,這可能不是現代機器的勝利]。
遇到“E”時,將指數作為整數收集,如上; 在預先計算的乘數表中查找准確的預先計算/縮放的十的冪(如果指數中存在“-”符號,則為倒數)並乘以收集的尾數。 (永遠不要做浮動除法)。 由於每個指數收集例程位於樹的不同分支(葉)中,因此它必須通過偏移十個索引的冪來調整小數點的明顯或實際位置。
[最重要的是:如果您知道數字的字符線性存儲在緩沖區中並且不跨越緩沖區邊界,則可以避免ptr++
的成本。 在沿着樹枝的第 k 個狀態中,您可以將第 k 個字符作為*(start+k)
。 一個好的編譯器通常可以在尋址模式的索引偏移中隱藏“...+k”。]
如果做得好,這個方案對每個非零位執行大約一個便宜的乘加,一個尾數轉換為浮點數,以及一個浮點乘法以按指數和小數點位置縮放結果。
我沒有實現上述。 我已經用循環實現了它的版本,它們非常快。
我已經實現了一些你可能會覺得有用的東西。 與 atof 相比,它大約快 x5,如果與__forceinline
大約快 x10。 另一件好事是它與 crt 實現具有完全相同的算法。 當然它也有一些缺點:
__forceinline bool float_scan(const wchar_t* wcs, float* val)
{
int hdr=0;
while (wcs[hdr]==L' ')
hdr++;
int cur=hdr;
bool negative=false;
bool has_sign=false;
if (wcs[cur]==L'+' || wcs[cur]==L'-')
{
if (wcs[cur]==L'-')
negative=true;
has_sign=true;
cur++;
}
else
has_sign=false;
int quot_digs=0;
int frac_digs=0;
bool full=false;
wchar_t period=0;
int binexp=0;
int decexp=0;
unsigned long value=0;
while (wcs[cur]>=L'0' && wcs[cur]<=L'9')
{
if (!full)
{
if (value>=0x19999999 && wcs[cur]-L'0'>5 || value>0x19999999)
{
full=true;
decexp++;
}
else
value=value*10+wcs[cur]-L'0';
}
else
decexp++;
quot_digs++;
cur++;
}
if (wcs[cur]==L'.' || wcs[cur]==L',')
{
period=wcs[cur];
cur++;
while (wcs[cur]>=L'0' && wcs[cur]<=L'9')
{
if (!full)
{
if (value>=0x19999999 && wcs[cur]-L'0'>5 || value>0x19999999)
full=true;
else
{
decexp--;
value=value*10+wcs[cur]-L'0';
}
}
frac_digs++;
cur++;
}
}
if (!quot_digs && !frac_digs)
return false;
wchar_t exp_char=0;
int decexp2=0; // explicit exponent
bool exp_negative=false;
bool has_expsign=false;
int exp_digs=0;
// even if value is 0, we still need to eat exponent chars
if (wcs[cur]==L'e' || wcs[cur]==L'E')
{
exp_char=wcs[cur];
cur++;
if (wcs[cur]==L'+' || wcs[cur]==L'-')
{
has_expsign=true;
if (wcs[cur]=='-')
exp_negative=true;
cur++;
}
while (wcs[cur]>=L'0' && wcs[cur]<=L'9')
{
if (decexp2>=0x19999999)
return false;
decexp2=10*decexp2+wcs[cur]-L'0';
exp_digs++;
cur++;
}
if (exp_negative)
decexp-=decexp2;
else
decexp+=decexp2;
}
// end of wcs scan, cur contains value's tail
if (value)
{
while (value<=0x19999999)
{
decexp--;
value=value*10;
}
if (decexp)
{
// ensure 1bit space for mul by something lower than 2.0
if (value&0x80000000)
{
value>>=1;
binexp++;
}
if (decexp>308 || decexp<-307)
return false;
// convert exp from 10 to 2 (using FPU)
int E;
double v=pow(10.0,decexp);
double m=frexp(v,&E);
m=2.0*m;
E--;
value=(unsigned long)floor(value*m);
binexp+=E;
}
binexp+=23; // rebase exponent to 23bits of mantisa
// so the value is: +/- VALUE * pow(2,BINEXP);
// (normalize manthisa to 24bits, update exponent)
while (value&0xFE000000)
{
value>>=1;
binexp++;
}
if (value&0x01000000)
{
if (value&1)
value++;
value>>=1;
binexp++;
if (value&0x01000000)
{
value>>=1;
binexp++;
}
}
while (!(value&0x00800000))
{
value<<=1;
binexp--;
}
if (binexp<-127)
{
// underflow
value=0;
binexp=-127;
}
else
if (binexp>128)
return false;
//exclude "implicit 1"
value&=0x007FFFFF;
// encode exponent
unsigned long exponent=(binexp+127)<<23;
value |= exponent;
}
// encode sign
unsigned long sign=negative<<31;
value |= sign;
if (val)
{
*(unsigned long*)val=value;
}
return true;
}
我記得我們有一個 Winforms 應用程序,它在解析一些數據交換文件時執行得很慢,我們都認為這是 db 服務器抖動,但我們聰明的老板實際上發現瓶頸在於將解析的字符串轉換為小數點!
最簡單的方法是對字符串中的每個數字(字符)進行循環,保持一個運行總數,將總數乘以 10,然后加上下一個數字的值。 繼續這樣做,直到到達字符串的末尾或遇到一個點。 如果您遇到一個點,請將整數部分與小數部分分開,然后使用一個乘數將每個數字除以 10。 繼續添加它們。
示例:123.456
運行總數= 0,加1(現在是1)運行總數= 1 * 10 = 10,加2(現在是12)運行總數= 12 * 10 = 120,加3(現在是123)遇到一個點,准備小數部分乘數 = 0.1,乘以 4,得到 0.4,添加到運行總數,使 123.4 乘數 = 0.1 / 10 = 0.01,乘以 5,得到 0.05,添加到運行總數,使 123.45 乘數 = 0.01 / 10 = 0。乘以 6,得到 0.006,加上運行總數,得到 123.456
當然,測試一個數字的正確性以及負數會使它變得更加復雜。 但是,如果您可以“假設”輸入是正確的,則可以使代碼更簡單、更快捷。
您是否考慮過讓 GPU 來完成這項工作? 如果您可以將字符串加載到 GPU 內存中並讓它處理所有這些字符串,您可能會發現一個很好的算法,它的運行速度比您的處理器快得多。
或者,在 FPGA 中進行 - 有可用於制作任意協處理器的 FPGA PCI-E 板。 使用 DMA 將 FPGA 指向包含要轉換的字符串數組的內存部分,讓它快速通過它們,將轉換后的值留在后面。
你看過四核處理器嗎? 在大多數情況下,真正的瓶頸是內存訪問無論如何......
-亞當
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.