我正在IA32上寻找一种非常快速的atof()实现,针对US-en语言环境,ASCII和非科学表示法进行了优化。 Windows多线程CRT在这里惨败,因为它检查每次调用isdigit()时的语言环境变化。我们目前最好的是源自perl + tcl的最佳实现,并且优于msvcrt.dll的atof一个数量级。我想做得更好,但我没有想法。与BCD相关的x86指令似乎很有希望,但我无法让它超越perl / tcl C代码。任何SO'ers都可以找到最好的链接吗?非x86基于组件的解决方案也是受欢迎的。
根据初步答案澄清:
对于这种应用,~2 ulp的不准确性是合适的 要转换的数字将通过网络小批量地到达ascii消息,我们的应用程序需要以尽可能低的延迟转换它们。
答案 0 :(得分:10)
您的准确度要求是多少?如果你确实需要它“正确”(总是得到最接近指定十进制的浮点值),它可能很难超越标准库版本(除了删除你已经完成的语言环境支持),因为这需要进行任意精度算术。如果你愿意容忍一个或两个误差(并且超过次正规误),那么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指令尚未在硬件中实现,现在简单地进行了微编码。它们不太可能表现得特别高。
答案 1 :(得分:5)
我刚完成编码的这个实现运行速度是我桌面上内置'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
您也可能对进行转换的状态机感兴趣:
答案 2 :(得分:3)
我已经实现了一些你可能会觉得有用的东西。
与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;
}
答案 3 :(得分:3)
在我看来,你想要(手动)构建一个状态机,每个状态处理第N个输入数字或指数数字;这个状态机的形状就像一棵树(没有环!)。目标是尽可能地进行整数运算,并且(显然)隐含地记住状态变量(“前导减号”,“位置3的小数点”),以避免分配,存储以及稍后获取/测试这些值。仅在输入字符上使用普通的旧“if”语句实现状态机(因此您的树将成为一组嵌套的ifs)。内联访问缓冲区字符;你不希望函数调用getchar
来减慢你的速度。
可以简单地抑制前导零;你可能需要一个循环来处理可笑的长前导零序列。可以收集第一个非零数字而不归零累加器或乘以10。前4到9个非零数字(对于16位或32位整数)可以通过整数乘法乘以常数值10来收集(由大多数编译器转换为几个移位并添加)。 [在顶部:零位数不需要任何工作,直到找到非零数字,然后需要乘以10 ^ N的N个连续零;你可以将所有这些连接到状态机]。可以使用32或64位乘法收集第一个4-9之后的数字,具体取决于机器的字大小。由于您不关心准确性,因此您可以在收集32或64位值后忽略数字;我猜你可以根据你的应用程序实际对这些数字做的事情,在你有一些固定数量的非零数字时实际停止。在数字字符串中找到的小数点只会导致状态机树中的分支。该分支知道该点的隐含位置,因此后来如何适当地按10的幂进行缩放。通过努力,如果您不喜欢此代码的大小,您可以组合一些状态机子树。
[在顶部:将整数和小数部分保持为单独的(小)整数。这将需要在末尾进行额外的浮点运算以组合整数和分数部分,可能不值得。]
[在顶部:将数字对的2个字符收集到16位值中,查找16位值。 这避免了在存储器访问的交易寄存器中的倍增,可能不是现代机器上的胜利]。
遇到“E”时,将指数收集为如上所示的整数;在预先计算的乘数表(如果指数中出现“ - ”符号的倒数)中准确查找预计算/缩放的10次幂,并乘以收集的尾数。 (不要做浮动划分)。由于每个指数收集例程都在树的不同分支(叶)中,因此必须通过抵消十个索引的幂来调整小数点的表观或实际位置。
[在顶部:如果您知道数字的字符线性存储在缓冲区中并且不跨越缓冲区边界,则可以避免ptr++
的成本。在沿树分支的第k个状态中,您可以访问第k个字符*(start+k)
。一个好的编译器通常可以在寻址模式中隐藏索引偏移中的“... + k”。]
如果做得好,这个方案大约对每个非零数字进行一次廉价的乘法加法,一次加入到尾数的浮点数,并且一次浮动乘以用小数点的指数和位置来缩放结果。
我没有实现上述内容。我用循环实现了它的版本,它们非常快。
答案 4 :(得分:1)
我记得我们有一个Winforms应用程序在解析一些数据交换文件时表现得这么慢,我们都认为这是数据库服务器颠簸,但是我们的聪明老板实际上发现瓶颈在于调用转换了将字符串解析为小数!
最简单的方法是循环字符串中的每个数字(字符),保持运行总计,将总数乘以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 multipiler = 0.01 / 10 = 0.001,乘以6,得到0.006,加上运行总计,得123.456
当然,测试一个数字的正确性和负数会使它更复杂。但是如果你能“假设”输入是正确的,你可以使代码更简单,更快。
答案 5 :(得分:0)
您是否考虑过让GPU完成这项工作?如果你可以将字符串加载到GPU内存并让它们处理它们,你可能会发现一个比你的处理器运行速度快得多的好算法。
或者,在FPGA中执行 - 可以使用FPGA PCI-E板来制作任意协处理器。使用DMA将FPGA指向包含您要转换的字符串数组的内存部分,并让它们通过它们保留转换后的值。
您是否看过四核处理器?大多数情况下的真正瓶颈是内存访问......
- 亚当