Сравнение 3 современных способов C++ для преобразования целых значений в строки

Я пытался выбрать стандартный способ преобразования интегралов в строки, поэтому я продолжил и провел небольшую оценку производительности, измерив время выполнения 3 методов.

#include <iostream>
#include <string>
#include <sstream>
#include <vector>
#include <chrono>
#include <random>
#include <exception>
#include <type_traits>
#include <boost/lexical_cast.hpp>

using namespace std;

// 1. A way to easily measure elapsed time -------------------
template<typename TimeT = std::chrono::milliseconds>
struct measure
{
    template<typename F>
    static typename TimeT::rep execution(F const &func)
    {
        auto start = std::chrono::system_clock::now();
        func();
        auto duration = std::chrono::duration_cast< TimeT>(
            std::chrono::system_clock::now() - start);
        return duration.count();
    }
};
// -----------------------------------------------------------

// 2. Define the convertion functions ========================
template<typename T> // A. Using stringstream ================
string StringFromNumber_SS(T const &value) {
    stringstream ss;
    ss << value;
    return ss.str();
}

template<typename T> // B. Using boost::lexical_cast =========
string StringFromNumber_LC(T const &value) {
    return boost::lexical_cast<string>(value);
}

template<typename T> // C. Using c++11 to_string() ===========
string StringFromNumber_C11(T const &value) {
    return std::to_string(value);
}
// ===========================================================

// 3. A wrapper to measure the different executions ----------
template<typename T, typename F>
long long MeasureExec(std::vector<T> const &v1, F const &func)
{
    return measure<>::execution([&]() {
        for (auto const &i : v1) {
            if (func(i) != StringFromNumber_LC(i)) {
                throw std::runtime_error("FAIL");
            }
        }
    });
}
// -----------------------------------------------------------

// 4. Machinery to generate random numbers into a vector -----
template<typename T>
typename std::enable_if<std::is_integral<T>::value>::type 
FillVec(vector<T> &v)
{
    std::mt19937 e2(1);
    std::uniform_int_distribution<> dist(3, 1440);
    std::generate(v.begin(), v.end(), [&]() { return dist(e2); });
}

template<typename T>
typename std::enable_if<!std::is_integral<T>::value>::type 
FillVec(vector<T> &v)
{
    std::mt19937 e2(1);
    std::uniform_real_distribution<> dist(-1440., 1440.);
    std::generate(v.begin(), v.end(), [&]() { return dist(e2); });
}
// -----------------------------------------------------------

int main()
{
    std::vector<int> v1(991908);
    FillVec(v1);

    cout << "C++ 11 method ......... " <<
        MeasureExec(v1, StringFromNumber_C11<int>) << endl;
    cout << "String stream method .. " <<
        MeasureExec(v1, StringFromNumber_SS<int>) << endl;
    cout << "Lexical cast method ... " <<
        MeasureExec(v1, StringFromNumber_LC<int>) << endl;

    return 0;
}

Типичный вывод (работает Release в VS2013, который подразумевает /O2 флаг оптимизации) будет

C++ 11 метод......... 273

Метод струнного потока.. 1923

Лексический метод приведения... 222

ОБНОВИТЬ

В качестве альтернативы онлайн запустить на GCC с

g++ -std=c++11 -Ofast -march=native -Wall -pedantic main.cpp && ./a.out

C++ 11 метод......... 414

Метод струнного потока.. 1538

Лексический метод приведения... 275

Отказ от ответственности: результаты должны сравниваться между собой, а не между машинами

Вопросы

1. Почему метод струнного потока постоянно худший (на порядок)? Должно ли это рассматриваться как устаревшее, когда появились более быстрые альтернативы?

2. Почему лексический состав неизменно является лучшим? Можно ли предположить, что это самая быстрая реализация?

Пожалуйста, не стесняйтесь настраивать и играть с вашими версиями этого кода. Буду признателен за ваше понимание этой темы.

PS

Код, который был фактически запущен, имел только одно измерение на main(), Здесь все были представлены 3 вместе, чтобы сэкономить место.

Флаги оптимизации зависят от компилятора или от приложения. Я просто предоставляю блоки кода для выполнения тестов и ожидаю, что пользователи SO сошлются со своими результатами или предложениями о том, какой будет оптимальная конфигурация для каждого компилятора (для чего стоит предоставить флаги, используемые здесь).

Код работает для любого преобразования чисел в строки (требуется изменение типа v1 в main). Сехе сделал для double (упоминается в комментарии к ответу). Это хорошая идея, чтобы поиграть с этим тоже.

3 ответа

Решение

Вопрос 1 Почему метод струнного потока постоянно худший?

Классическая ошибка: создание нового струнного потока каждый раз

template<typename T> // 1. Using stringstream
string StringFromIntegral_SS(T const &value) {
    thread_local stringstream ss;
    ss.str("");
    ss.clear();
    ss << value;
    return ss.str();
}

Вопрос 2 Почему лексический состав неизменно лучший? Можно ли предположить, что это самая быстрая реализация?

Потому что это наиболее специализированный; и нет, существуют более быстрые реализации. Насколько я знаю, у FastFormat и Boost Spirit есть конкурентные предложения.

Обновление Boost Spirit Karma по-прежнему легко обыгрывает кучу:

template<typename T> // 4. Karma to string
std::string StringFromIntegral_K(T const &value) {
    thread_local auto const gen = boost::spirit::traits::create_generator<T>::call();
    thread_local char buf[20];
    char* it = buf;
    boost::spirit::karma::generate(it, gen, value);
    return std::string(buf, it);
}

Тайминги:

C++ 11 method 111
String stream method 103
Lexical cast method 57
Spirit Karma method 36
Spirit Karma method with string_ref 13

Смотрите это в прямом эфире на Coliru Clang или GCC


БОНУС

Просто чтобы обмануть, версия, использующая boost::string_ref намного быстрее еще из-за сокращенных ассигнований:

template<typename T> // 5. Karma to string_ref
boost::string_ref StringFromIntegral_KSR(T const &value) {
    thread_local auto const gen = boost::spirit::traits::create_generator<T>::call();
    thread_local char buf[20];
    char* it = buf;
    boost::spirit::karma::generate(it, gen, value);
    return boost::string_ref(buf, it-buf);
}

Я проверил все модифицированные методы на корректность, используя проверочный цикл:

return measure<>::execution(
    //[&]() { for (auto const &i : v1) { func(i); }});
    [&]() { for (auto const &i : v1) { assert(func(i) == StringFromIntegral_LC(i)); }});

станд::формат

С C++20 в наш арсенал был добавлен новый метод, а именно метод std::format. Получение строки из числаnumбудет так же просто, как:

      std::format("{}", num);

Поскольку gcc еще не поддерживает его, я расширил исходный тест, используя:

      fmt::format("{}", num);

т.е. библиотекаstd::formatоснован на (и, вероятно, будет основной частью будущих реализаций) в этой онлайн-демонстрации . Разочарован, увидев, что он в 4 раза медленнее, чем заявленная скорость библиотеки:

      C++ 11 method ......... 56
String stream method .. 1171
Lexical cast method ... 78
Format method ... 210

Может быть, я не отдаю должное этой библиотеке (после всех заявленных тестов дляfmtрассмотретьprintфункциональность), поэтому я оставляю тест и сообщаю здесь, на случай, если будут предложены настройки или исправления.

Это версия бенчмарка с оптимизированным использованием Sehestringstream(т.е. как локальная переменная потока)


чаркон

Рекламируемый как самый быстрый современный метод, использующий заголовок charconv , выполняет свои обещания и превосходит конкурентов с относительными временными параметрами, такими как:

      Program returned: 0
C++ 11 method ......... 13
String stream method .. 1044
Lexical cast method ... 23
Format method ... 152
Charconv method ... 11

Как показано в следующем демонстрационном примере , основное отличие API заключается в том, что он заставляет пользователя предварительно выделять буфер результатов (куда помещается вывод), даже помещая его в автоматическую память (стек). Если кто-то идет против течения и (в отличие от того, что показано в демонстрации выше) делает такие вещи, как создание строк на лету, например, в случае, если им нужно сохранить каждый «новый объект»:

      template<typename T> // D. Using c++17 to_chars() ===========
std::string StringFromNumber_CharConv(T const &value) {
    std::array<char, 10> ret;
    auto [ptr, ec] = std::to_chars(ret.data(), ret.data() + ret.size(), value);
    return  std::string(ret.data(), ptr);
}

тогда производительность совпадает с . Так что две мысли по этому поводу:

  1. Либо распределение памяти является здесь определяющим фактором, и этот тест недостаточно детализирован, чтобы различатьto_stringиto_chars.
  2. Илиstd::to_stringуже вобрал в себя достижения, сделанные .

Тем не менее, кажется, что если нужно работать как можно быстрее, для этого предоставляется API, поскольку он не требует выделения памяти, а алгоритм преобразования «ядра», по-видимому, настолько быстр, насколько это возможно.

Определяющим фактором, конечно же, будет длина чисел, которые мы конвертируем, поскольку разные алгоритмы могут работать для разных входных данных. Четыре целых числа, которые мы используем в тесте, могут не оставлять много места дляcharconvчтобы показать свою мощь, то, что большие числа с плавающей запятой продемонстрировали бы лучше.

я добавилabsl::StrCatкак вариант, убедился, что код также будет обрабатывать числа с плавающей запятой, а затем запустил цикл бенчмарка 3 раза.

Я также запускал их как на clang, так и на gcc.

См. https://godbolt.org/z/7feoaEEKr

Для int на gcc я получил следующие результаты:

      C++ 11 method ......... 81
String stream method .. 112
Lexical cast method ... 96
Format method ......... 212
StrCat method ......... 608

C++ 11 method ......... 81
String stream method .. 131
Lexical cast method ... 143
Format method ......... 704
StrCat method ......... 118

C++ 11 method ......... 103
String stream method .. 153
Lexical cast method ... 633
Format method ......... 191
StrCat method ......... 91

Я не знаю, что происходит с тестами, иногда получающими гораздо более медленные результаты, но я подозреваю, что из-за выделения/освобождения памяти происходит некоторая сборка мусора распределителя.

Другие вопросы по тегам