簡體   English   中英

C++ 將字符串轉換為 int 的最有效方法(比 atoi 更快)

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

正如標題中提到的,我正在尋找比 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 會議上的演講有力地證明了這一點。 Hi 的替代品使用循環展開和 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 != '\0' ) {

這讓你有

unsigned int naive(const char *p) {
    unsigned int x = 0;
    while (*p != '\0') {
        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指向一個數字是解析器首先調用此代碼的原因。 在我的代碼中,整個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()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,等等),編譯器將其轉換為移位、屏蔽、LEA 等,內聯一切都進入大的壞解析器循環,並在引擎蓋下處理所有必需的混亂以加快速度。 我們甚至不必再在所有內容之前保持inline 如果有的話,那么我們必須做相反的事情,當編譯器過於急切時,明智地使用__declspec(noinline)

我在一個從文本文件和管道中讀取數十億個數字的程序中使用了上面的代碼; 如果長度為 9..10 位,則每秒轉換 1.15 億個單位,長度為 19..20 位(gcc 4.8.1)時轉換為 6000 萬/s。 這比strtoull()快十倍以上strtoull()對於我的目的來說幾乎不夠,但我離題了......)。 這是轉換每個包含 1000 萬個數字 (100..200 MB) 的文本 blob 的時間,這意味着內存時間使這些數字看起來比在從緩存運行的合成基准測試中要差一些。

帕迪實施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;
} 

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

為什么不使用字符串流? 我不確定它的特定開銷,但您可以定義:

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

如果t1t10等是靜態分配的並且是常量,編譯器不應該擔心任何別名,並且生成的機器代碼應該相當不錯。

我對此處給出的不同功能 + 一些附加功能進行了快速基准測試,默認情況下我將它們轉換為 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}x\n", func, final, base * 1.0 / final, final - overhead, (base - overhead) * 1.0 / (final - overhead));
}

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

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM