C 性能优化实践(一)
数字转换为字符串
在日常开发中我们经常需要将数字转换为字符串,c++11提供了std::to_string方法来做转换,c++11之前还有其它一些方法也可以实现数字到字符串的转换,比如std::stringstream, sprintf,c++17中又提供了std::to_chars将数字转换为字符串,除此之外,还有一些format库也提供了相应的转换方法。因此,在c++17中我们有至少5种方法来实现数字到字符串的转换,那么我们到底应该选择哪种方法更好呢?
一个简单的选择标准是性能最优就是最好的选择。不妨做一个简单的性能测试看看这些方法哪个的性能更好。
#include <iostream>
#include <chrono>
#include <charconv>
#include <string>
#include <sstream>
#include <fmt/core.h>
#include <stdio.h>
class ScopedTimer {
public:
ScopedTimer(const char* name)
: m_name(name),
m_beg(std::chrono::high_resolution_clock::now()) { }
~ScopedTimer() {
auto end = std::chrono::high_resolution_clock::now();
auto dur = std::chrono::duration_cast<std::chrono::nanoseconds>(end - m_beg);
std::cout << m_name << ' : ' << dur.count() << ' ns\n';
}
private:
const char* m_name;
std::chrono::time_point<std::chrono::high_resolution_clock> m_beg;
};
void to_string(){
ScopedTimer timer('to_string');
for(size_t i=0; i<10000; i++) {
std::to_string(i);
}
}
void ss_to_string() {
ScopedTimer timer('ss_to_string');
for(size_t i=0; i<10000; i++) {
std::stringstream ss;
ss<<i;
}
}
void fmt_string() {
ScopedTimer timer('fmt_to_string');
for(size_t i=0; i<10000; i++) {
fmt::format('{}', i);
}
}
void printf_string() {
ScopedTimer timer('sprintf_to_string');
for(size_t i=0; i<10000; i++) {
char str[10];
sprintf(str, '%d', i);
}
}
void conv_string() {
ScopedTimer timer('conv_to_string');
for(size_t i=0; i<10000; i++) {
char str[10];
std::to_chars(str, str + 10, i);
}
}
int main() {
to_string();
ss_to_string();
fmt_string();
printf_string();
conv_string();
}
-O2编译选项输出结果:
to_string: 822330 ns
ss_to_string: 3277426 ns
fmt_to_string: 828938 ns
sprintf_to_string: 658275 ns
conv_to_string: 61313ns
从测试结果看,stringstream是最慢的,主要原因是频繁构造了该对象,std::to_string和fat::format性能相当,sprintf性能比std::to_string快约20%, c++17的to_chars无疑问是最快的,比其他的to string方法快了一个数量级。
因此在c++17中应该优先使用std::to_chars来获得最优的性能。在c++17之前可以使用sprintf和std::to_string,尽量不要用stringstream。
虽然c++17的to_chars已经很快了,但是在一些场景下我们还能继续提升to_string的性能,比如彻底消除to_string的运行期开销,到这里大家应该知道优化的思路了:利用编译期计算来彻底消除to_string的运行期开销。
编译期数字转换为字符串
很多时候我们转换的数字是一个编译期常量,这时候就应该在编译期来做转换而不是在运行期做转换,消除运行期开销以实现最优的性能。
对于编译期数字常量在编译期转换为字符串最简单的方法是用一个字符串化的宏来实现,两行代码即可。
#define STR(x) #x
#define TOSTRING(x) STR(x)
TOSTRING(1); '1'
TOSTRING(2); '2'
如果不希望用宏,那么可以用元编程实现编译期数字转换为字符串的元函数。
//https://stackoverflow.com/questions/23999573/convert-a-number-to-a-string-literal-with-constexpr/24000041
namespace detail
{
template<uint8_t... digits> struct positive_to_chars {
static const char value[];
static constexpr size_t size = sizeof...(digits);
};
template<uint8_t... digits> const char positive_to_chars<digits...>::value[] = {('0' + digits)..., 0};
template<uint8_t... digits> struct negative_to_chars { static const char value[]; };
template<uint8_t... digits> const char negative_to_chars<digits...>::value[] = {'-', ('0' + digits)..., 0};
template<bool neg, uint8_t... digits>
struct to_chars : positive_to_chars<digits...> {};
template<uint8_t... digits>
struct to_chars<true, digits...> : negative_to_chars<digits...> {};
template<bool neg, uintmax_t rem, uint8_t... digits>
struct explode : explode<neg, rem / 10, rem % 10, digits...> {};
template<bool neg, uint8_t... digits>
struct explode<neg, 0, digits...> : to_chars<neg, digits...> {};
template<typename T>
constexpr uintmax_t cabs(T num) { return (num < 0) ? -num : num; }
}
template<typename T, T num>
struct string_from : ::detail::explode<num < 0, ::detail::cabs(num)> {};
string_from<unsigned, 1>::value; //'1'
static_assert(string_from<unsigned, 1>::size==1);
简单和运行期数字转换为字符串做一个性能比较。
void compile_time_to_string() {
ScopedTimer timer('compile_time_to_string');
for(size_t i=0; i<10000; i++) {
string_from<unsigned, 1>::value;
}
}
void macro_string() {
ScopedTimer timer('macro_to_string');
for(size_t i=0; i<10000; i++) {
TOSTRING(1);
}
}
int main() {
to_string();
ss_to_string();
fmt_string();
printf_string();
conv_string();
macro_string();
compile_time_to_string();
}
//输出结果:
to_string : 822330 ns
ss_to_string : 3277426 ns
fmt_to_string : 828938 ns
sprintf_to_string : 658275 ns
conv_to_string : 36267 ns
macro_to_string : 32 ns
compile_time_to_string : 31 ns
//https://godbolt.org/z/xsbsGjjWa
可以看到将数字常量在编译期转换为字符串的优化效果很明显,彻底消除了运行期开销!
后续还会继续谈一些c++性能优化方法和实践,请继续关注c++性能优化系列文章。