为什么使用snprintf打印单个数字时速度始终比ostringstream快2倍?

4

我正在测试多种在C++中格式化double的方法,这里是我想出来的一些代码:

#include <chrono>
#include <cstdio>
#include <random>
#include <vector>
#include <sstream>
#include <iostream>

inline long double currentTime()
{
    const auto now = std::chrono::steady_clock::now().time_since_epoch();
    return std::chrono::duration<long double>(now).count();
}

int main()
{
    std::mt19937 mt(std::random_device{}());
    std::normal_distribution<long double> dist(0, 1e280);
    static const auto rng=[&](){return dist(mt);};
    std::vector<double> numbers;
    for(int i=0;i<10000;++i)
        numbers.emplace_back(rng());

    const int precMax=200;
    const int precStep=10;

    char buf[10000];
    std::cout << "snprintf\n";
    for(int precision=10;precision<=precMax;precision+=precStep)
    {
        const auto t0=currentTime();
        for(const auto num : numbers)
            std::snprintf(buf, sizeof buf, "%.*e", precision, num);
        const auto t1=currentTime();
        std::cout << "Precision " << precision << ": " << t1-t0 << " s\n";
    }

    std::cout << "ostringstream\n";
    for(int precision=10;precision<=precMax;precision+=precStep)
    {
        std::ostringstream ss;
        ss.precision(precision);
        ss << std::scientific;
        const auto t0=currentTime();
        for(const auto num : numbers)
        {
            ss.str("");
            ss << num;
        }
        const auto t1=currentTime();
        std::cout << "Precision " << precision << ": " << t1-t0 << " s\n";
    }
}

让我想不明白的是,一开始当精度小于40时,我得到的性能差不多。但随后,snprintf的性能优势增加了2.1x。请参见我的输出:在Core i7-4765T、Linux 32位、g++ 5.5.0、libc 2.14.1下编译时,使用-march=native -O3

snprintf
Precision 10: 0.0262963 s
Precision 20: 0.035437 s
Precision 30: 0.0468597 s
Precision 40: 0.0584917 s
Precision 50: 0.0699653 s
Precision 60: 0.081446 s
Precision 70: 0.0925062 s
Precision 80: 0.104068 s
Precision 90: 0.115419 s
Precision 100: 0.128886 s
Precision 110: 0.138073 s
Precision 120: 0.149591 s
Precision 130: 0.161005 s
Precision 140: 0.17254 s
Precision 150: 0.184622 s
Precision 160: 0.195268 s
Precision 170: 0.206673 s
Precision 180: 0.218756 s
Precision 190: 0.230428 s
Precision 200: 0.241654 s
ostringstream
Precision 10: 0.0269695 s
Precision 20: 0.0383902 s
Precision 30: 0.0497328 s
Precision 40: 0.12028 s
Precision 50: 0.143746 s
Precision 60: 0.167633 s
Precision 70: 0.190878 s
Precision 80: 0.214735 s
Precision 90: 0.238105 s
Precision 100: 0.261641 s
Precision 110: 0.285149 s
Precision 120: 0.309025 s
Precision 130: 0.332283 s
Precision 140: 0.355797 s
Precision 150: 0.379415 s
Precision 160: 0.403452 s
Precision 170: 0.427337 s
Precision 180: 0.450668 s
Precision 190: 0.474012 s
Precision 200: 0.498061 s

我的主要问题是:这种两倍差异的原因是什么?此外,我如何让 ostringstream 的性能更接近于 snprintf
注意:另一个问题 为什么snprintf比ostringstream快或者说它确实更快吗? 与我的问题不同。首先,在那里没有具体的答案,解释为什么在不同精度下格式化单个数字较慢。其次,那个问题问的是“为什么一般情况下较慢”,这太笼统了,无法回答我的问题,而这个问题询问格式化单个 double 数字的一个特定场景。

可能是[为什么snprintf比ostringstream快,还是吗?]的重复问题(https://dev59.com/03RB5IYBdhLWcg3w-8Ho)。 - Flopp
@Flopp 这不是这样的:首先,为什么在不同精度下格式化单个数字会更慢没有具体答案。其次,它问“为什么总体上会更慢”,这太含糊了,没有任何意义,而我的问题则涉及一个具体的场景。 - Ruslan
我怀疑你正在构建DEBUG版本。当我使用Visual Studio构建Release版本时,snprintf和ostringstream之间的性能数字仅有轻微差异。 - selbie
@selbie 请注意我的编译选项:-march=native -O3。这绝对不是调试模式。 - Ruslan
@Ruslan:也许GCC的stringstream实现很糟糕,或者Visual Studio的snprintf实现很糟糕。 - Nicol Bolas
1个回答

6

std::ostringstream会调用vsnprintf两次:第一次使用小缓冲区尝试,第二次使用正确大小的缓冲区。请参见locale_facets.tcc第1011行附近(这里std::__convert_from_vvsnprintf的代理):

#if _GLIBCXX_USE_C99_STDIO
    // Precision is always used except for hexfloat format.
    const bool __use_prec =
      (__io.flags() & ios_base::floatfield) != ios_base::floatfield;

    // First try a buffer perhaps big enough (most probably sufficient
    // for non-ios_base::fixed outputs)
    int __cs_size = __max_digits * 3;
    char* __cs = static_cast<char*>(__builtin_alloca(__cs_size));
    if (__use_prec)
      __len = std::__convert_from_v(_S_get_c_locale(), __cs, __cs_size,
                    __fbuf, __prec, __v);
    else
      __len = std::__convert_from_v(_S_get_c_locale(), __cs, __cs_size,
                    __fbuf, __v);

    // If the buffer was not large enough, try again with the correct size.
    if (__len >= __cs_size)
      {
        __cs_size = __len + 1;
        __cs = static_cast<char*>(__builtin_alloca(__cs_size));
        if (__use_prec)
          __len = std::__convert_from_v(_S_get_c_locale(), __cs, __cs_size,
                        __fbuf, __prec, __v);
        else
          __len = std::__convert_from_v(_S_get_c_locale(), __cs, __cs_size,
                        __fbuf, __v);
      }

这与观察结果完全相符,即对于请求的小精度,性能与snprintf相同,而对于较大的精度,则性能较差2倍。

此外,由于使用的缓冲区不依赖于任何std::ostringstream缓冲区的属性,仅依赖于__max_digits,该属性定义为__gnu_cxx::__numeric_traits<_ValueT>::__digits10,因此似乎没有其他自然的解决方法,除了修复libstdc++本身。

我已经向libstdc++报告了这个错误,链接


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接