【问题标题】:C++ most efficient way to convert string to int (faster than atoi)C++ 将字符串转换为 int 的最有效方法(比 atoi 更快)
【发布时间】:2013-05-25 10:52:40
【问题描述】:

正如标题中所提到的,我正在寻找可以比 atoi 提供更多性能的东西。目前,我知道最快的方法是

atoi(mystring.c_str())

最后,我更喜欢不依赖 Boost 的解决方案。有没有人有很好的性能技巧来做到这一点?

附加信息:int 不会超过 20 亿,总是正数,字符串中没有小数位。

【问题讨论】:

  • 你将很难击败 atoi。
  • 这个问题的答案可能在一定程度上取决于您允许的整数范围。您想转换 any 整数,还是您允许的输入更具体?你确定mystring 包含 only 一个没有其他字符的整数吗?可以是负数吗?
  • 我添加了一些附加信息,常规大小的 int,总是正数,字符串中没有小数。
  • 你得到了很好的答案,但我总是想知道 - 你真的知道atoi 本身就消耗了你整体时间的健康百分比吗?人们经常会问这样的问题,而事实上还有其他东西可以提高速度,但他们不知道如何找到这样的东西。

标签: c++ string performance int type-conversion


【解决方案1】:

一种更快的转换函数,仅适用于正整数,没有错误检查。

乘法总是比求和和移位慢,因此用移位来改变乘法。

int fast_atoi( const char * str )
{
    int val = 0;
    while( *str ) {
        val = (val << 3) + (val << 1) + (*str++ - '0');
    }
    return val;
}

【讨论】:

  • 虽然您可以将10 分解为16 - 4 - 2,但更简单的分解是8 + 2
  • "乘法总是比 sum 和 shift 慢" --> 不是总是
  • 你能举个例子吗?
【解决方案2】:

这里的很多代码示例都非常复杂,并且会做一些不必要的工作,这意味着代码可以更精简、更快。

转换循环通常被编写为对每个字符执行三种不同的操作:

  • 如果是字符串结尾字符则退出
  • 如果不是数字就退出
  • 将其从代码点转换为实际数字值

第一个观察:不需要单独检查字符串结尾字符,因为它不是数字。因此,对“数字”的检查隐含地涵盖了 EOS 条件。

第二个观察:通过使用无符号类型并将范围锚定为零,可以将(c &gt;= '0' &amp;&amp; c &lt;= '9') 中的范围测试的双重条件转换为单一测试条件;这样在范围开始以下就不会有不需要的值,所有不需要的值都映射到上限以上的范围:(uint8_t(c - '0') &lt;= 9)

碰巧c - '0'无论如何都需要在这里计算...

因此可以将内部转换循环精简为

uint64_t n = digit_value(*p);
unsigned d;

while ((d = digit_value(*++p)) <= 9)
{
   n = n * 10 + d;
}

这里的代码是在p指向一个数字的前提下调用的,这就是为什么毫不费力地提取第一个数字的原因(这也避免了多余的MUL)。

这个前提条件并不像一开始看起来那么古怪,因为p 指向一个数字是解析器首先调用这段代码的原因。在我的代码中,整个 shebang 看起来像这样(断言和其他生产质量的噪音被消除了):

unsigned digit_value (char c)
{
   return unsigned(c - '0');
}

bool is_digit (char c)
{
   return digit_value(c) <= 9;
}

uint64_t extract_uint64 (char const **read_ptr)
{
   char const *p = *read_ptr;
   uint64_t n = digit_value(*p);
   unsigned d;

   while ((d = digit_value(*++p)) <= 9)
   {
      n = n * 10 + d;
   }

   *read_ptr = p;

   return n;
}

如果代码被内联并且调用代码已经通过调用is_digit() 计算了该值,编译器通常会忽略对digit_value() 的第一次调用。

n * 10 恰好比手动移位更快(例如n = (n &lt;&lt; 3) + (n &lt;&lt; 1) + d),至少在我的机器上使用 gcc 4.8.1 和 VC++ 2013。我的猜测是两个编译器都使用带有索引缩放的 LEA 来加起来一次到三个值,并将其中一个值缩放 2、4 或 8。

无论如何,它应该是这样的:我们在单独的函数中编写干净整洁的代码并表达所需的逻辑(n * 10,x % CHAR_BIT,等等),编译器将其转换为移位、屏蔽、LEA 等on,将所有内容内联到大的错误解析器循环中,并在引擎盖下处理所有必需的混乱以使事情变得更快。我们甚至不必再在所有内容前面加上inline。如果有的话,那么我们必须做相反的事情,当编译器过于急切时,明智地使用__declspec(noinline)

我在一个程序中使用上述代码,该程序从文本文件和管道中读取数十亿个数字;如果长度为 9..10 位,它每秒转换 1.15 亿个 uint,长度为 19..20 位(gcc 4.8.1)的每秒转换 6000 万个单位。这比strtoull() 快十倍以上(对于我的目的来说还勉强够用,但我离题了……)。这是转换每个包含 1000 万个数字 (100..200 MB) 的文本 blob 的时间,这意味着内存时间使这些数字看起来比在从缓存运行的合成基准测试中要差一些。

【讨论】:

  • 我喜欢使用unsigned 进行快速比较。 p[0]=='\0' 时不是 UB 的粉丝。
【解决方案3】:

Paddy 的 fast_atoi 实现 atoi 快 - 没有阴影怀疑 - 但是它只适用于无符号整数

下面,我放置了 Paddy 的 fast_atoi 的评估版本,它也只允许无符号整数,但通过将昂贵的操作 * 替换为 +

来加快转换速度
unsigned int fast_atou(const char *str)
{
    unsigned int val = 0;
    while(*str) {
        val = (val << 1) + (val << 3) + *(str++) - 48;
    }
    return val;
}

在这里,我放了 fast_atoi()完整版本,我有时也会使用它来转换单数整数:

int fast_atoi(const char *buff)
{
    int c = 0, sign = 0, x = 0;
    const char *p = buff;

    for(c = *(p++); (c < 48 || c > 57); c = *(p++)) {if (c == 45) {sign = 1; c = *(p++); break;}}; // eat whitespaces and check sign
    for(; c > 47 && c < 58; c = *(p++)) x = (x << 1) + (x << 3) + c - 48;

    return sign ? -x : x;
} 

【讨论】:

  • 不确定位移解决方案是否真的更快,因为 x86 截断乘法是一条指令,gcc 会知道高位无关紧要。
【解决方案4】:

这是我的。 Atoi 是我能想到的最快的。我使用 msvc 2010 编译,因此可以将两个模板结合起来。在 msvc 2010 中,当我组合模板时,它会使您提供 cb 参数的情况变慢。

Atoi 几乎可以处理所有特殊的 atoi 情况,并且与此一样快或更快:

int val = 0;
while( *str ) 
    val = val*10 + (*str++ - '0');

代码如下:

#define EQ1(a,a1) (BYTE(a) == BYTE(a1))
#define EQ1(a,a1,a2) (BYTE(a) == BYTE(a1) && EQ1(a,a2))
#define EQ1(a,a1,a2,a3) (BYTE(a) == BYTE(a1) && EQ1(a,a2,a3))

// Atoi is 4x faster than atoi.  There is also an overload that takes a cb argument.
template <typename T> 
T Atoi(LPCSTR sz) {
    T n = 0;
    bool fNeg = false;  // for unsigned T, this is removed by optimizer
    const BYTE* p = (const BYTE*)sz;
    BYTE ch;
    // test for most exceptions in the leading chars.  Most of the time
    // this test is skipped.  Note we skip over leading zeros to avoid the 
    // useless math in the second loop.  We expect leading 0 to be the most 
    // likely case, so we test it first, however the cpu might reorder that.
    for ( ; (ch=*p-'1') >= 9 ; ++p) { // unsigned trick for range compare
      // ignore leading 0's, spaces, and '+'
      if (EQ1(ch, '0'-'1', ' '-'1', '+'-'1'))
        continue;
      // for unsigned T this is removed by optimizer
      if (!((T)-1 > 0) && ch==BYTE('-'-'1')) {
        fNeg = !fNeg;
        continue;
      }
      // atoi ignores these.  Remove this code for a small perf increase.
      if (BYTE(*p-9) > 4)  // \t, \n, 11, 12, \r. unsigned trick for range compare
        break;
    }
    // deal with rest of digits, stop loop on non digit.
    for ( ; (ch=*p-'0') <= 9 ; ++p) // unsigned trick for range compare
      n = n*10 + ch; 
    // for unsigned T, (fNeg) test is removed by optimizer
    return (fNeg) ? -n : n;
}

// you could go with a single template that took a cb argument, but I could not
// get the optimizer to create good code when both the cb and !cb case were combined.
// above code contains the comments.
template <typename T>
T Atoi(LPCSTR sz, BYTE cb) {
    T n = 0;
    bool fNeg = false; 
    const BYTE* p = (const BYTE*)sz;
    const BYTE* p1 = p + cb;
    BYTE ch;
    for ( ; p<p1 && (ch=*p-'1') >= 9 ; ++p) {
      if (EQ1(ch,BYTE('0'-'1'),BYTE(' '-'1'),BYTE('+'-'1')))
        continue;
      if (!((T)-1 > 0) && ch == BYTE('-'-'1')) {
        fNeg = !fNeg;
        continue;
      }
      if (BYTE(*p-9) > 4)  // \t, \n, 11, 12, \r
        break;
    }
    for ( ; p<p1 && (ch=*p-'0') <= 9 ; ++p)
      n = n*10 + ch; 
    return (fNeg) ? -n : n;
}

【讨论】:

  • EQ1 显然是有问题的,当代码甚至没有经过测试时,它就会对基准产生怀疑。
【解决方案5】:

唯一确定的答案是检查你的编译器,你的真实数据。

我会尝试的事情(即使它正在使用内存访问,因此它可能会很慢,具体取决于缓存)是

int value = t1[s[n-1]];
if (n > 1) value += t10[s[n-2]]; else return value;
if (n > 2) value += t100[s[n-3]]; else return value;
if (n > 3) value += t1000[s[n-4]]; else return value;
... continuing for how many digits you need to handle ...

如果t1t10 等是静态分配的并且是常量,编译器不应该担心任何别名,并且生成的机器代码应该相当不错。

【讨论】:

    【解决方案6】:

    This page 比较使用不同编译器的不同 string->int 函数之间的转换速度。根据给出的结果,不提供错误检查的 naive 函数提供的速度大约是 atoi() 的两倍。

    // Taken from http://tinodidriksen.com/uploads/code/cpp/speed-string-to-int.cpp
    int naive(const char *p) {
        int x = 0;
        bool neg = false;
        if (*p == '-') {
            neg = true;
            ++p;
        }
        while (*p >= '0' && *p <= '9') {
            x = (x*10) + (*p - '0');
            ++p;
        }
        if (neg) {
            x = -x;
        }
        return x;
    }
    

    总是积极的

    删除上述代码中的否定检查以进行微优化。

    如果你能保证字符串除了数字字符之外什么都没有,你可以通过改变循环进一步微优化

    while (*p >= '0' && *p <= '9') {
    

    while (*p != '\0' ) {
    

    剩下的就是你了

    unsigned int naive(const char *p) {
        unsigned int x = 0;
        while (*p != '\0') {
            x = (x*10) + (*p - '0');
            ++p;
        }
        return x;
    }
    

    【讨论】:

    • 然而,幼稚的实现不符合标准:它不会丢弃前导空格。如果您不需要标准的保证..
    • @DyP: '\0' 将跳出循环......它是&lt; '0'。无论如何,链接页面没有列出除了这个天真的循环之外的任何功能,这些功能是优于 atoi 的重要候选者 - 他们没有利用来自上述见解的任何效率,即对有效字符的期望,总是积极的,已知的最大大小,不需要任何错误检查......
    • 哎呀,你是对的,大脑比较运算符坏了,sry..但是你可以把它改成while(*p != '\0')..
    • + 这基本上就是我这样做的方式,尽管在循环中我倾向于写{x *= 10; x += (*p++ - '0');}。它可能编译成大致相同的东西。
    • @johnnycrash 不知道为什么会这样。按位 & 和常量减法都是一条指令。
    【解决方案7】:

    这是 gcc 中 atoi 函数的全部内容:

    long atoi(const char *str)
    {
        long num = 0;
        int neg = 0;
        while (isspace(*str)) str++;
        if (*str == '-')
        {
            neg=1;
            str++;
        }
        while (isdigit(*str))
        {
            num = 10*num + (*str - '0');
            str++;
        }
        if (neg)
            num = -num;
        return num;
     }
    

    在您的情况下,空格和否定检查是多余的,但也只使用纳秒。

    isdigit 几乎可以肯定是内联的,所以这不会花费您任何时间。

    我真的觉得这里没有改进的余地。

    【讨论】:

    • 我能够使用它为不同的整数类型创建一个函数模板并在 AVR 上运行它。
    • “我真的看不出这里有改进的余地。”当最终结果 s/b LONG_MIN 时,10*num + (*str - '0') 是 UB。 isspace(*str)isdigit(*str) UB 当*str &lt; 0 - 不过不太可能担心 OP。
    【解决方案8】:

    我尝试了使用查找表的解决方案,但发现它们充满了问题,而且实际上速度不是很快。结果证明,最快的解决方案是最没有想象力的:

    int fast_atoi( const char * str )
    {
        int val = 0;
        while( *str ) {
            val = val*10 + (*str++ - '0');
        }
        return val;
    }
    

    使用一百万个随机生成的字符串运行基准测试:

    fast_atoi : 0.0097 seconds
    atoi      : 0.0414 seconds
    

    公平地说,我还通过强制编译器不要内联它来测试这个函数。结果还是不错的:

    fast_atoi : 0.0104 seconds
    atoi      : 0.0426 seconds
    

    如果您的数据符合fast_atoi 函数的要求,那是相当合理的性能。要求是:

    1. 输入字符串只包含数字字符,或者为空
    2. 输入字符串代表一个从0到INT_MAX的数字

    【讨论】:

      【解决方案9】:

      为什么不使用字符串流?我不确定它的特定开销,但您可以定义:

      int myInt; 
      string myString = "1561";
      stringstream ss;
      ss(myString);
      ss >> myInt;
      

      当然,你需要

      #include <stringstream> 
      

      【讨论】:

      • 这是典型的 C++ 方式,但它比精简的“幼稚”转换循环慢几个数量级。
      【解决方案10】:

      atoi 可以在某些假设下得到显着改进。 Andrei Alexandrescu 在 C++ and Beyond 2012 会议上的演讲有力地证明了这一点。 Hi 的替代品使用循环展开和 ALU 并行性来实现性能改进的数量级。我没有他的资料,但是这个链接使用了类似的技术:http://tombarta.wordpress.com/2008/04/23/specializing-atoi/

      【讨论】:

      • 我想我也看到了。 this 是您指的演示文稿吗?不过,它不是 C++ 及其他。而且我认为这主要是关于 int-to-string 而不是反向。但无论如何,我们可以从中学到很多东西。
      • 链接的int atoi(const char *str) 未能检测到所有溢出。
      猜你喜欢
      • 2012-11-06
      • 1970-01-01
      • 2012-07-26
      • 2020-01-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-06-08
      • 1970-01-01
      相关资源
      最近更新 更多