Comprehensive C++ String-to-Int Conversion Benchmarks(C89-C++23)

教育   科技   2023-03-28 21:16   英国  

本文整理了C++中String-to-Int的9种方式,并对其性能进行了对比。

这些方式包含:

  • atoi

  • strtol

  • sscanf

  • sstream

  • lexical_cast

  • stoi

  • from_chars

  • spanstream

  • constexpr from_chars

  • simple compile time to_int

这个列表是按时间排序的,从C89到C++23。

据群内小调查,使用atoi和sstream的人最多,stoi和from_chars的其次。

下面就让我们全面地对比一下这些方式。

1

C89/C90 atoi, sscanf

atoi是C89/C90就存在的东西,包含在<stdlib.h>。使用起来很简单,效率也很高,一个例子:

#include <iostream>
#include <cstdlib>
#include <string>

int main() {
    std::string s("20230324");
    int res = atoi(s.c_str());
    std::cout << res << "\n";
}

它的缺点在于,没有错误处理。你无法处理这种情况:

assert(atoi("0") != atoi("str"));

有许多人采用strtol(C99)来代替atoi,例子:

std::string s("str");
int res = (int)strtol(s.c_str(), NULL10);
if (errno != ERANGE)
    std::cout << res << "\n";

如果转换的值超过了返回值范围,那么errno会被置为ERANGE;如果不可转换,则返回0。它优于atoi的点就在于支持边界错误检查。

此外,strtol还支持多值转换,以及指定转换基底,通用性也要比atoi强。提供一个多值转换的例子,其中基数是10表示十进制。

const char* s = "10 20 30 40";
for (;;) {
    errno = 0;
    char *end;
    int val = (int)strtol(s, &end, 10);
    if (s == end)
        break;

    if (errno != ERANGE)
        printf("'%.*s' ==> %d\n", (int)(end-s), s, val);

    s = ++end; // skip the whitespace
}

// Output:
// '10' ==> 10
// '20' ==> 20
// '30' ==> 30
// '40' ==> 40

另一个C89的可以用来转换的特性是sscanf,相信没人会陌生。但是它有缓冲区溢出和类型安全等等问题,C++ devs用得不多。

2

C++98 sstream

atoi和strtol都来自C,在C++中使用起来不是太自然。C++标准中第一个支持类型转换的组件就是sstream,功能还是比较强大的和完整的。

看一个多值转换的例子:

int main() {
    std::string str = "10 20 30";
    std::stringstream ss(str);
    int x, y, z;

    ss >> x >> y >> z;

    std::cout << "x: " << x << " y: " << y << " z: " << z << "\n";
}

// Output:
// x: 10 y: 20 z: 30

sstream是String IO,易用、简洁、灵活,这种方式更加C++。

C++ devs早期就是借助sstream来编写转换工具,当年的一种经典实现:

template <class Tclass U>
class is_convertible {

    typedef char small_type;
    struct big_type { char dummy[2]; };
    static small_type test(T);
    static big_type test(...);
    static U makeU();
public:
    enum { value = sizeof(test(makeU())) == sizeof(small_type) };
};

template <class Tclass U,
    typename std:
:enable_if<!is_convertible<T, U>::value>::type* = nullptr>
void convert(T& to, const U& from) {
    std::stringstream ss;
    ss << from;
    ss >> to;
}

template <class Tclass U,
    typename std:
:enable_if<is_convertible<T, U>::value>::type* = nullptr>
void convert(T& to, const U& from) {
    to = from;
}

int main() {
    std::string str("230326");
    int retval;
    convert(retval, str);

    std::string str2;
    convert(str2, retval);

    std::cout << retval << " --- " << str2 << "\n";
}

只要把std::enable_if自己实现一下,在C++11之前的项目中也可以编译这段代码。

万事都有代价。sstream如此灵活的代价,就是存在对象创建、内存分配和输入解析等等开销,导致性能较低。

3

boost lexical_cast

boost库在05/06年加入并完善了lexical_cast,用以支持类型转换,同时还为数值到数值之间的转换提供了numeric_cast。

lexical_cast要比sstream更加易用,且性能更高。一个例子看一下:

#include <iostream>
#include <string>
#include <boost/lexical_cast.hpp>

int main() {
    std::string str = "123";
    int retval = boost::lexical_cast<int>(str);
    std::cout << retval << "\n";
}

使用人较少的原因,可能是因为没有进标准,一些开发者不喜欢依赖非标准以外的库。再者,后面C++也增加了一些转换相关的组件,性能远超lexical_cast。

4

C++11 stoi

通常来说,越是新加的组件,其必然在易用性或性能方面优于之前的组件,否则也就没有必要引入标准了。

C++11加入了一系列String转换函数,包含stoi, stol和stoll。顾名思义,stoi的转换范围是int,stol和stoll分别是long和long long

一个例子:

#include <iostream>
#include <string>
#include <stdexcept>

int main() {
    std::string s {"20230327214805"};
    try {
        int retval = std::stoi(s);
        std::cout << "stoi: " << retval << "\n";
    } catch (std::invalid_argument const& e) {
        std::cout << "std::invalid_argument::what: " << e.what() << "\n";
    } catch (std::out_of_range const& e) {
        long long llretval = std::stoll(s);
        std::cout << "stoll: " << llretval << "\n";
    }
}

// output:
// stoll: 20230327214805

除了atoi,stox的性能要超过之前的其他所有方法,并且提供了良好的错误处理。此外,stox系列函数其实有三个参数,第二个参数能够得到实际处理的字符数,第三个参数能够指定基数。因此,它是一种比较好的方法。

5

C++17 from_chars

C++17又增加了一组工具:to/from_chars,它们既不会分配内存,也不会抛出异常,是一种更加轻量级的转换方式。

一个例子:

#include <charconv>
#include <iostream>
#include <string_view>
#include <system_error>

int main() {
    for (std::string_view const str : {"10""str""20230327214805"}) {
        int result{};
        auto [ptr, ec] = std::from_chars(str.data(), str.data() + str.size(), result);

        if (ec == std::errc())
            std::cout << "result: " << result << "\n";
        else if (ec == std::errc::invalid_argument)
            std::cerr << "invalid_argument\n";
        else if (ec == std::errc::result_out_of_range)
            std::cerr << "result_out_of_range\n";
    }
}

// output:
// result: 10
// invalid_argument
// result_out_of_range

这是当前标准中最高效的方式,效率超过前面的任何一种方式。追求效率时,应该首选这种方式。

6

C++23 spanstream

C++23新增的spanstream和stringstream对应,不过后者是String IO,它是Array IO。

因此,它的用法和sstream一样,不过效率要稍高一点点。例子:

#include <iostream>
#include <span>
#include <spanstream>

int main() {
    char input[] = "10 20 30";
    std::ispanstream is{ std::span<char>{input} };
    int x, y, z;
    is >> x >> y >> z;
    std::cout << "x: " << x << " y: " << y << " z: " << z << "\n";
}

7

C++23 constexpr from_chars

以上所有方式都是运行期的,C++23支持编译期转换:constexpr to/from_chars。

编译期方式的效率要远远高于运行期的方式,因此这种方式是最高效的。使用例子:

#include <charconv>
#include <optional>
#include <string_view>

constexpr std::optional<int> to_int(std::string_view s) {
    int value;
    if (auto [p, err] = std::from_chars(s.data(), s.data() + s.size(), value); err == std::errc{}) {
        return value;
    } else {
        return std::nullopt;
    }
}


int main () {
    constexpr std::string_view sv = "20230317";
    constexpr int result = *to_int(sv);
    static_assert(result == 20230317"failed to convert string to int at compile time.");
}

当前只有msvc 19.34+支持该特性,gcc13和clang16离发布还得一段时间。

8

simple compile time to_int

自己也可以借助constexpr来实现简单的编译期转换函数,下面是一种实现方式:

int main () {
    constexpr auto to_int = [](this auto self, const char *str, int h = 0) -> int {
        return *str ? self(str + 1, h * 10 + *str - '0') : h;
    };

    constexpr char s[] = "20230317";
    constexpr int result = to_int(s);
    static_assert(result == 20230317"failed to convert string to int at compile time.");
}

这种方式也没有范围检查和错误处理等等额外操作,但它发生于编译期,效率很高。这里还使用了recursive lambda,若要在低版本使用,只需将lambda替换为常规函数就可以。

9

Benchmarking

现在来测试一下所有的方法,对比一下他们的效率。测试代码如下:

1#include <benchmark/benchmark.h>
2#include <string>
3#include <string_view>
4#include <cassert>
5
6
7#include <cstdlib>
8static void atoi_bench(benchmark::State& state) {
9    std::string s {"20230317"};
10    for (auto _ : state) {
11        int result = std::atoi(s.c_str());
12        assert(result != 0);
13        benchmark::DoNotOptimize(result);
14    }
15}
16BENCHMARK(atoi_bench);
17
18
19#include <cerrno>
20static void strtol_bench(benchmark::State& state) {
21    std::string s {"20230328"};
22    for (auto _ : state) {
23        int result = (int)std::strtol(s.c_str(), NULL10);
24        assert(errno != ERANGE);
25        benchmark::DoNotOptimize(result);
26    }
27}
28BENCHMARK(strtol_bench);
29
30
31#include <cstdio>
32static void sscanf_bench(benchmark::State& state) {
33    std::string s {"20230317"};
34    for (auto _ : state) {
35        int result{};
36        int ec = sscanf(s.c_str(), "%d", &result);
37        assert(ec == 1);
38        benchmark::DoNotOptimize(result);
39    }
40}
41BENCHMARK(sscanf_bench);
42
43
44#include <sstream>
45static void sstream_bench(benchmark::State& state) {
46    std::string s {"20230317"};
47    for (auto _ : state) {
48        std::istringstream ss(s);
49        int result{};
50        ss >> result;
51        assert(!ss.fail());
52        benchmark::DoNotOptimize(result);
53    }
54}
55BENCHMARK(sstream_bench);
56
57
58#include "boost/lexical_cast.hpp"
59static void lexical_cast_bench(benchmark::State& state) {
60    std::string s {"20230317"};
61    for (auto _ : state) {
62        int result{};
63        result = boost::lexical_cast<int>(s);
64        benchmark::DoNotOptimize(result);
65    }
66}
67BENCHMARK(lexical_cast_bench);
68
69
70static void stoi_bench(benchmark::State& state) {
71    std::string s {"20230317"};
72    for (auto _ : state) {
73        int result = std::stoi(s);
74        benchmark::DoNotOptimize(result);
75        // catch std::invalid_argument or std::out_of_range exceptions to check the result.
76    }
77}
78BENCHMARK(stoi_bench);
79
80
81#include <charconv>
82static void from_chars_bench(benchmark::State& state) {
83    std::string s {"20230317"};
84    for (auto _ : state) {
85        int result{};
86        auto [ptr, ec] = std::from_chars(s.data(), s.data() + s.size(), result);
87        benchmark::DoNotOptimize(result);
88    }
89}
90BENCHMARK(from_chars_bench);
91
92
93#include <span>
94#include <spanstream>
95static void spanstream_bench(benchmark::State& state) {
96    char input[] = "20230317";
97    for (auto _ : state) {
98        std::ispanstream is{ std::span<char>{input} };
99        int result{};
100        is >> result;
101        benchmark::DoNotOptimize(result);
102    }
103}
104BENCHMARK(spanstream_bench);
105
106
107// Compile-time convert string to int
108constexpr int to_int(const char *str, int h = 0) {
109    return *str ? to_int(str + 1, h * 10 + *str - '0') : h;
110}
111
112static void simple_compile_time_to_int_bench(benchmark::State& state) {
113    constexpr char s[] = "20230317";
114    for (auto _ : state) {
115        constexpr int result = to_int(s);
116        benchmark::DoNotOptimize(result);
117    }
118}
119BENCHMARK(simple_compile_time_to_int_bench);
120
121
122// constexpr from_chars
123// #include <optional>
124// constexpr std::optional<int> to_int(std::string_view s) {
125//     int value;
126//     if (auto [p, err] = std::from_chars(s.data(), s.data() + s.size(), value); err == std::errc{}) {
127//         return value;
128//     } else {
129//         return std::nullopt;
130//     }
131// }
132
133// static void constexpr_from_chars_bench(benchmark::State& state) {
134//     constexpr std::string_view sv = "20230317";
135//     for (auto _ : state) {
136//         constexpr int result = *to_int(sv);
137//         benchmark::DoNotOptimize(result);
138//     }
139// }
140//BENCHMARK(constexpr_from_chars_bench);
141
142BENCHMARK_MAIN();

由于现在只有msvc v19.34+才支持constexpr from_chars,gcc和clang尚不支持,所以暂时将它加了注释。

测试结果为:

---------------------------------------------------------------------------
Benchmark                                 Time             CPU   Iterations
---------------------------------------------------------------------------
atoi_bench                             43.6 ns         43.6 ns     16152578
strtol_bench                           45.9 ns         45.9 ns     15233262
sscanf_bench                            220 ns          220 ns      3239227
sstream_bench                           731 ns          731 ns       876900
lexical_cast_bench                     54.6 ns         54.6 ns     12817159
stoi_bench                             44.3 ns         44.3 ns     15799663
from_chars_bench                       19.9 ns         19.9 ns     35240157
spanstream_bench                        422 ns          422 ns      1577471
simple_compile_time_to_int_bench      0.632 ns        0.632 ns   1000000000

可以看到,运行期方法中C++17 from_chars效率是最高的,其次是atoi和stoi,再是strtol和lexical_cast,sstream与sscanf的开销要远远大于其他方式,spanstream较sstream效率有所提高。编译期方法不出意料,效率最高。

10

Conclusion

C++98追求效率使用strtol,追求易用性使用sstream,允许用boost则使用lexical_cast。C++11使用stox系列函数,C++17以上使用from_chars是最佳选择。若是支持C++23,那么还可以使用constexpr from_chars。

多值转换追求方便的话可以使用sstream/spanstream,追求效率的话使用strtol或是from_chars+Ranges。


CppMore
Dive deep into the C++ core, and discover more!
 最新文章