20

我正在寻找针对美国英语语言环境、ASCII 和非科学记数法优化的 IA32 上极快的 atof() 实现。windows 多线程 CRT 在每次调用 isdigit() 时都会检查语言环境的变化,因此在这里很糟糕。我们目前的最佳表现源自 perl + tcl 的 atof 实现中的最佳表现,其性能比 msvcrt.dll 的 atof 高出一个数量级。我想做得更好,但我没有想法。BCD 相关的 x86 指令似乎很有希望,但我无法让它胜过 perl/tcl C 代码。任何 SO'ers 都可以挖掘到最好的链接吗?也欢迎基于非 x86 汇编的解决方案。

基于初步答案的澄清:

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

4

6 回答 6

10

你的精度要求是什么?如果你真的需要它“正确”(总是得到最接近指定小数的浮点值),可能很难击败标准库版本(除了删除你已经完成的语言环境支持),因为这需要进行任意精度算术。如果您愿意容忍一两个 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指令还没有在硬件中实现,现在只是微编码。它们不太可能具有特别高的性能。

于 2008-09-19T02:26:15.450 回答
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

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

号码解析状态机图

于 2011-11-14T12:53:10.113 回答
3

在我看来,您想(手动)构建相当于每个状态处理第 N 个输入数字或指数数字的状态机;这个状态机的形状像一棵树(没有循环!)。目标是尽可能进行整数运算,并且(显然)隐式记住状态中的状态变量(“前导减号”、“位置 3 的小数点”),以避免分配、存储和以后获取/测试这些值. 仅在输入字符上使用普通的旧“if”语句实现状态机(因此您的树将成为一组嵌套的 if)。对缓冲区字符的内联访问;您不希望函数调用getchar减慢您的速度。

可以简单地抑制前导零;你可能需要一个循环来处理非常长的前导零序列。无需将累加器归零或乘以十即可收集第一个非零数字。前 4-9 个非零数字(对于 16 位或 32 位整数)可以通过整数乘以常数值 10 来收集(大多数编译器将其转换为几次移位和加法)。[在顶部:零位不需要任何工作,直到找到一个非零位,然后需要 N 个连续零的乘法 10^N;您可以将所有这些连接到状态机]。前 4-9 之后的数字可以使用 32 位或 64 位乘法来收集,具体取决于您机器的字长。由于您不关心准确性,因此您可以在收集 32 或 64 位价值后简单地忽略数字;一世' 根据您的应用程序对这些数字的实际操作,猜测当您有一些固定数量的非零数字时,您实际上可以停止。在数字字符串中找到的小数点只会导致状态机树中的分支。该分支知道该点的隐含位置,因此以后如何适当地按 10 的幂进行缩放。如果您不喜欢这段代码的大小,您可以通过努力组合一些状态机子树。

[在顶部:将整数和小数部分保留为单独的(小)整数。这将需要在最后进行额外的浮点运算来组合整数和小数部分,可能不值得]。

[在顶部:将数字对的 2 个字符收集到一个 16 位值中,查找 16 位值。这避免了寄存器中的乘法以换取内存访问,这在现代机器上可能不是一个胜利]。

在遇到“E”时,如上所述将指数收集为整数;在预先计算的乘数表中查找准确的预先计算/缩放的 10 次幂(如果指数中存在“-”符号,则为倒数)并将收集的尾数相乘。(永远不要做浮点除法)。由于每个指数收集例程都位于树的不同分支(叶)中,因此它必须通过偏移 10 指数的幂来调整小数点的明显或实际位置。

ptr++[顶部:如果您知道数字的字符线性存储在缓冲区中并且不跨越缓冲区边界,则可以避免成本。在沿着树枝的第 k 个状态中,您可以将第 k 个字符访问为*(start+k). 一个好的编译器通常可以在寻址模式下将“...+k”隐藏在索引偏移中。]

如果做得对,该方案对每个非零数字进行大约一次廉价的乘加,尾数转换为浮点数,以及通过指数和小数点位置缩放结果的浮点乘法。

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

于 2012-05-03T04:18:13.693 回答
2

我已经实现了一些你可能会觉得有用的东西。与 atof 相比,它的速度大约是 x5,如果与 atof 一起使用,__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;
}
于 2009-10-28T16:20:40.910 回答
1

我记得我们有一个 Winforms 应用程序在解析一些数据交换文件时执行得很慢,我们都认为这是 db 服务器抖动,但我们聪明的老板实际上发现瓶颈在于将解析的字符串转换为的调用小数点!

最简单的方法是对字符串中的每个数字(字符)进行循环,保持一个运行总数,将总数乘以 10,然后加上下一个数字的值。继续这样做,直到到达字符串的末尾或遇到一个点。如果遇到点,请将整数部分与小数部分分开,然后有一个乘数,每个数字除以 10。继续添加它们。

示例:123.456

running total = 0,加 1(现在是 1) running total = 1 * 10 = 10,加 2(现在是 12) running total = 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

当然,测试数字的正确性以及负数会使其更加复杂。但是,如果您可以“假设”输入是正确的,那么您可以使代码更简单、更快。

于 2008-09-19T02:06:52.640 回答
0

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

或者,在 FPGA 中进行 - 您可以使用 FPGA PCI-E 板来制作任意协处理器。使用 DMA 将 FPGA 指向包含要转换的字符串数组的内存部分,并让它快速浏览它们,将转换后的值留在后面。

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

-亚当

于 2008-09-19T02:39:55.477 回答