【问题标题】:Where can I find the world's fastest atof implementation?我在哪里可以找到世界上最快的 atof 实施?
【发布时间】:2010-09-11 01:06:36
【问题描述】:

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

基于初步答案的澄清:

~2 ulp 的误差对于这个应用来说是可以的。
要转换的数字将通过网络以小批量 ascii 消息的形式到达,我们的应用程序需要以尽可能低的延迟进行转换。

【问题讨论】:

  • 检查isdigit 上的区域设置更改?也许他们应该了解一下 ISO C 标准。 isdigit 没有与语言环境相关的行为;它必须检查字符是否是集合09 的元素,仅此而已。
  • 你能给我们一个问题域的想法吗?我猜这不是财务问题,或者您会使用定点算术。是用于控制系统,例如定位吗?你有(硬的或软的)实时要求吗?
  • 如果您可以修改消息格式,显然发送二进制浮点数(或更简单的二进制文本编码)将在另一端节省昂贵的解析。例如如果二进制不正确,则将浮点数转储为十六进制整数。
  • github.com/lemire/fast_double_parser (来自仅链接的答案)。 Daniel Lemire 是优化领域的知名人士,因此它的质量可能很好。另见他的相关博文:lemire.me/blog/2020/09/10/…

标签: c++ c performance assembly floating-point


【解决方案1】:

您的准确度要求是什么?如果你真的需要它“正确”(总是得到最接近指定小数的浮点值),可能很难击败标准库版本(除了删除你已经完成的语言环境支持),因为这需要进行任意精度的算术。如果您愿意容忍一两个 ulp 的错误(并且比次规范的错误更多),那么 cruzer 提出的这种方法可以工作并且可能更快,但它绝对不会产生

自 (IIRC) 286 以来,BCD 指令还没有在硬件中实现,现在只是微编码。它们不太可能具有特别高的性能。

【讨论】:

  • 您建议的方法实际上是我们正在使用从 perl/tcl 中获取的想法所做的事情。正如您所提到的,它更快+更精确。感谢您提供有关 BCD 历史的花絮 - 我没听说过,但循环计数似乎非常高
  • BCD 指令(AAM 等)甚至在 64 位模式下都不存在,这是不使用它们的另一个原因。
【解决方案2】:

我刚刚完成编码的这个实现的运行速度是我桌面上内置的“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

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

【讨论】:

    【解决方案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”隐藏在索引偏移中。]

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

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

    【讨论】:

      【解决方案4】:

      我已经实现了一些你可能会觉得有用的东西。 与 atof 相比,它快约 5 倍,如果与 __forceinline 一起使用,则快约 10 倍。 另一件好事是它似乎与 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;
      }
      

      【讨论】:

      • 这段代码看起来确实很麻烦,然后失败了。 pow(10.0,decexp) 对于 decexp 的非平凡小值是不精确的。似乎您将低于正常值的结果发送到 0 而不是它们的值。如果我错了,请纠正我。
      【解决方案5】:

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

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

      【讨论】:

      • 我认为你应该只做一次小数部分。收集到点的所有东西,当你到达点时,开始一个新数字和数字1。将两者乘以10。最后,你有3个浮点数,整数部分,小数部分作为整数和数量您需要将小数部分除以使其成为小数。现在除法和加法:intpart + decpart/divisor
      【解决方案6】:

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

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

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

      -亚当

      【讨论】:

      • 这些浮点数经常通过网络到达,但时间是随机的。我们的重点是延迟,而不是吞吐量,否则我认为您的 GPU 或 FPGA 建议会很可靠。我们使用 8 核和 16 核 CPU,内存/缓存与上述循环展开 + 转换有关。
      猜你喜欢
      • 2023-01-04
      • 1970-01-01
      • 2013-04-10
      • 2012-03-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-07-28
      • 1970-01-01
      相关资源
      最近更新 更多