我在哪里可以找到世界上最快的实施方案?

我正在寻找IA32上极其快速的atof()实现,针对US-en语言环境,ASCII和非科学表示法进行了优化。 WindowsmultithreadingCRT在这里惨败,因为它检查每次调用isdigit()时的语言环境变化。 我们目前最好的是源自perl + tcl的最佳实现,并且优于msvcrt.dll的atof一个数量级。 我想做得更好,但我没有想法。 与BCD相关的x86指令看起来很有希望,但我无法让它超越perl / tcl C代码。 任何SO的人都可以找到最好的链接吗? 也欢迎基于非x86组件的解决方案。

根据初步答案澄清:

对于该应用,~2 ulp的不准确性是好的。
要转换的数字将通过网络小批量地到达ascii消息,我们的应用程序需要以尽可能低的延迟转换它们。

您的准确度要求是多少? 如果你真的需要它“正确”(总是得到最接近指定十进制的浮点值),它可能很难超越标准库版本(除了删除你已经完成的语言环境支持),因为这需要进行任意精度算术。 如果你愿意容忍一个或两个误差(并且超过次正规误),那么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

您也可能对进行转换的状态机感兴趣:

数字解析状态机图

我已经实现了一些你可能会觉得有用的东西。 与atof相比,它的速度大约快x5,如果与__forceinline大约快x10。 另一个好处是它接缝与crt实现具有完全相同的算法。 当然它也有一些缺点:

  • 它只支持单精度浮点数,
  • 并且不扫描任何特殊值,如#INF等…
 __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; } 

在我看来,你想(手工)建立一个状态机,每个状态处理第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”。

如果做得好,这个方案大约对每个非零数字进行一次便宜的乘法加法,一次对尾数进行逐次浮动,一次浮动乘以用小数点的指数和位置来缩放结果。

我没有实现上述。 我用循环实现了它的版本,它们非常快。

我记得我们有一个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乘数= 0.01 / 10 = 0.001,乘以6,得到0.006,加上运行总计,得到123.456

当然,测试一个数字的正确性和负数会使它更复杂。 但是,如果您可以“假设”输入正确,则可以使代码更简单,更快捷。

您是否考虑过让GPU完成这项工作? 如果您可以将字符串加载到GPU内存并让它们处理所有内容,您可能会发现一个比您的处理器运行速度快得多的好算法。

或者,在FPGA中执行 – 可以使用FPGA PCI-E板来制作任意协处理器。 使用DMA将FPGA指向包含要转换的字符串数组的内存部分,然后让它们通过它们保留转换后的值。

你看过四核处理器了吗? 大多数情况下的真正瓶颈是内存访问……

-亚当