c++中最有效的将string转换为int的方法(比atoi更快)

C++ most efficient way to convert string to int (faster than atoi)

本文关键字:方法 atoi 更快 int 转换 有效 c++ string      更新时间:2023-10-16

正如标题中提到的,我正在寻找比atoi更能给我带来性能的东西。目前,我知道的最快的方法是

atoi(mystring.c_str())

最后,我更喜欢不依赖Boost的解决方案。有人有好的表演技巧吗?

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

我尝试了使用查找表的解决方案,但发现它们充满了问题,而且实际上不是很快。最快的解决方案是最缺乏想象力的:

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的数字

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

这个页面比较了使用不同编译器的不同string->int函数之间的转换速度。根据给出的结果,没有提供错误检查的朴素函数提供的速度大约是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 != '' ) {

剩下

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

这里有相当多的代码示例非常复杂,并且做了不必要的工作,这意味着代码可以更精简、更快。

转换循环通常对每个字符做三件不同的事情:

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

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

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

碰巧c - '0'需要在这里计算…

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

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

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

这个先决条件并不像一开始看起来那么奇怪,因为p指向一个数字是解析器首先调用这段代码的原因。在我的代码中,整个组合看起来是这样的(省略了断言和其他生产质量的噪声):

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 << 3) + (n << 1) + d)更快,至少在我的机器上使用gcc 4.8.1和vc++ 2013。我的猜测是,两个编译器都使用LEA和索引缩放来一次将三个值相加,并将其中一个值缩放2、4或8。

在任何情况下,它都应该是这样的:我们在单独的函数中编写漂亮的干净代码,并表达所需的逻辑(n * 10, x % CHAR_BIT,无论如何),编译器将其转换为移位,屏蔽,leing等,将所有内容内联到大坏解析器循环中,并处理所有需要的混乱,以使事情更快。我们甚至不必再把inline放在所有东西的前面了。如果有的话,我们必须做相反的事情,当编译器变得过于急切时,明智地使用__declspec(noinline)

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

Paddy实现的fast_atoiatoi快——毫无疑问——但是它只适用于无符号整数

下面,我放了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(),我有时使用它转换signed整数:

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;
} 

下面是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几乎肯定是内联的,所以这不会花费您任何时间。

我真的看不出有改进的余地。

一个更快的转换函数,只适用于正整数,不进行错误检查。

乘法总是比和和移位慢,因此用移位代替乘法。

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

我对这里给出的不同函数和一些额外的函数做了一个快速基准测试,并在默认情况下将它们转换为int64_t。编译器= MSVC.

结果如下(左=正常时间,右=扣除开销后的时间):

atoi            : 153283912 ns => 1.000x : 106745800 ns => 1.000x
atoll           : 174446125 ns => 0.879x : 127908013 ns => 0.835x
std::stoll      : 358193237 ns => 0.428x : 311655125 ns => 0.343x
std::stoull     : 354171912 ns => 0.433x : 307633800 ns => 0.347x
-----------------------------------------------------------------
fast_null       :  46538112 ns => 3.294x :         0 ns => infx   (overhead estimation)
fast_atou       :  92299625 ns => 1.661x :  45761513 ns => 2.333x (@soerium)
FastAtoiBitShift:  93275637 ns => 1.643x :  46737525 ns => 2.284x (@hamSh)
FastAtoiMul10   :  93260987 ns => 1.644x :  46722875 ns => 2.285x (@hamSh but with *10)
FastAtoiCompare :  86691962 ns => 1.768x :  40153850 ns => 2.658x (@DarthGizka)
FastAtoiCompareu:  86960900 ns => 1.763x :  40422788 ns => 2.641x (@DarthGizka + uint)
-----------------------------------------------------------------
FastAtoi32      :  92779375 ns => 1.652x :  46241263 ns => 2.308x (handle the - sign)
FastAtoi32u     :  86577312 ns => 1.770x :  40039200 ns => 2.666x (no sign)
FastAtoi32uu    :  87298600 ns => 1.756x :  40760488 ns => 2.619x (no sign + uint)
FastAtoi64      :  93693575 ns => 1.636x :  47155463 ns => 2.264x
FastAtoi64u     :  86846912 ns => 1.765x :  40308800 ns => 2.648x
FastAtoi64uu    :  86890537 ns => 1.764x :  40352425 ns => 2.645x
FastAtoiDouble  :  90126762 ns => 1.701x :  43588650 ns => 2.449x (only handle int)
FastAtoiFloat   :  92062775 ns => 1.665x :  45524663 ns => 2.345x (same)

DarthGizka的代码是最快的,并且具有当字符是非数字时停止的优势。

同时,位移的"优化"比* 10稍微慢一点。

基准测试在伪随机字符串上运行每个算法1000万次迭代,以尽可能地限制分支预测,然后重新运行所有内容15次以上。对于每个算法,丢弃4个最慢和4个最快的时间,给出的结果是8个中位数时间的平均值。这提供了很多稳定性。此外,我运行fast_null是为了估计基准测试中的开销(循环+字符串更改+函数调用),然后在第二个数字中扣除该值。

下面是函数的代码:
int64_t fast_null(const char* str) { return (str[0] - '0') + (str[1] - '0'); }
int64_t fast_atou(const char* str)
{
    int64_t val = 0;
    while (*str) val = (val << 1) + (val << 3) + *(str++) - 48;
    return val;
}
int64_t FastAtoiBitShift(const char* str)
{
    int64_t val = 0;
    while (*str) val = (val << 3) + (val << 1) + (*str++ - '0');
    return val;
}
int64_t FastAtoiMul10(const char* str)
{
    int64_t val = 0;
    while (*str) val = val * 10 + (*str++ - '0');
    return val;
}
int64_t FastAtoiCompare(const char* str)
{
    int64_t val = 0;
    uint8_t x;
    while ((x = uint8_t(*str++ - '0')) <= 9) val = val * 10 + x;
    return val;
}
uint64_t FastAtoiCompareu(const char* str)
{
    uint64_t val = 0;
    uint8_t  x;
    while ((x = uint8_t(*str++ - '0')) <= 9) val = val * 10 + x;
    return val;
}
int32_t FastAtoi32(const char* str)
{
    int32_t val  = 0;
    int     sign = 0;
    if (*str == '-')
    {
        sign = 1;
        ++str;
    }
    uint8_t digit;
    while ((digit = uint8_t(*str++ - '0')) <= 9) val = val * 10 + digit;
    return sign ? -val : val;
}
int32_t FastAtoi32u(const char* str)
{
    int32_t val = 0;
    uint8_t digit;
    while ((digit = uint8_t(*str++ - '0')) <= 9) val = val * 10 + digit;
    return val;
}
uint32_t FastAtoi32uu(const char* str)
{
    uint32_t val = 0;
    uint8_t  digit;
    while ((digit = uint8_t(*str++ - '0')) <= 9) val = val * 10u + digit;
    return val;
}
int64_t FastAtoi64(const char* str)
{
    int64_t val  = 0;
    int     sign = 0;
    if (*str == '-')
    {
        sign = 1;
        ++str;
    }
    uint8_t digit;
    while ((digit = uint8_t(*str++ - '0')) <= 9) val = val * 10 + digit;
    return sign ? -val : val;
}
int64_t FastAtoi64u(const char* str)
{
    int64_t val = 0;
    uint8_t digit;
    while ((digit = uint8_t(*str++ - '0')) <= 9) val = val * 10 + digit;
    return val;
}
uint64_t FastAtoi64uu(const char* str)
{
    uint64_t val = 0;
    uint8_t  digit;
    while ((digit = uint8_t(*str++ - '0')) <= 9) val = val * 10u + digit;
    return val;
}
float FastAtoiFloat(const char* str)
{
    float   val = 0;
    uint8_t x;
    while ((x = uint8_t(*str++ - '0')) <= 9) val = val * 10.0f + x;
    return val;
}
double FastAtoiDouble(const char* str)
{
    double  val = 0;
    uint8_t x;
    while ((x = uint8_t(*str++ - '0')) <= 9) val = val * 10.0 + x;
    return val;
}

和我使用的基准代码,以防万一…

void Benchmark()
{
    std::map<std::string, std::vector<int64_t>> funcTimes;
    std::map<std::string, std::vector<int64_t>> funcTotals;
    std::map<std::string, int64_t>              funcFinals;
#define BENCH_ATOI(func)                     
    do                                       
    {                                        
        auto    start    = NowNs();          
        int64_t z        = 0;                
        char    string[] = "000001987";      
        for (int i = 1e7; i >= 0; --i)       
        {                                    
            string[0] = '0' + (i + 0) % 10;  
            string[1] = '0' + (i + 1) % 10;  
            string[2] = '0' + (i + 3) % 10;  
            string[3] = '0' + (i + 5) % 10;  
            string[4] = '0' + (i + 9) % 10;  
            z += func(string);               
        }                                    
        auto elapsed = NowNs() - start;      
        funcTimes[#func].push_back(elapsed); 
        funcTotals[#func].push_back(z);      
    }                                        
    while (0)
    for (int i = 0; i < 16; ++i)
    {
        BENCH_ATOI(atoi);
        BENCH_ATOI(atoll);
        BENCH_ATOI(std::stoll);
        BENCH_ATOI(std::stoull);
        //
        BENCH_ATOI(fast_null);
        BENCH_ATOI(fast_atou);
        BENCH_ATOI(FastAtoiBitShift);
        BENCH_ATOI(FastAtoiMul10);
        BENCH_ATOI(FastAtoiCompare);
        BENCH_ATOI(FastAtoiCompareu);
        //
        BENCH_ATOI(FastAtoi32);
        BENCH_ATOI(FastAtoi32u);
        BENCH_ATOI(FastAtoi32uu);
        BENCH_ATOI(FastAtoi64);
        BENCH_ATOI(FastAtoi64u);
        BENCH_ATOI(FastAtoi64uu);
        BENCH_ATOI(FastAtoiFloat);
        BENCH_ATOI(FastAtoiDouble);
    }
    for (auto& [func, times] : funcTimes)
    {
        std::sort(times.begin(), times.end(), [](const auto& a, const auto& b) { return a < b; });
        fmt::print("{:<16}: {}n", func, funcTotals[func][0]);
        int64_t total = 0;
        for (int i = 4; i <= 11; ++i) total += times[i];
        total /= 8;
        funcFinals[func] = total;
    }
    const auto base     = funcFinals["atoi"];
    const auto overhead = funcFinals["fast_null"];
    for (const auto& [func, final] : funcFinals)
        fmt::print("{:<16}: {:>9} ns => {:.3f}x : {:>9} ns => {:.3f}xn", func, final, base * 1.0 / final, final - overhead, (base - overhead) * 1.0 / (final - overhead));
}

为什么不使用stringstream呢?我不确定它的具体开销,但您可以定义:

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

当然,你需要

#include <stringstream> 

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

我尝试的东西(即使它使用内存访问,所以它可能很慢取决于缓存)是

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 ...

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

这是我的。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;
}