此例程被调用无数次,以创建充满数字的大型 CSV 文件。有没有更有效的方法来做到这一点?
static std::string dbl2str(double d)
{
std::stringstream ss;
//convert double to string w fixed notation, hi precision
ss << std::fixed << std::setprecision(10) << d;
//output to std::string
std::string s = ss.str();
//remove trailing 000s (123.1200 => 123.12, 123.000 => 123.)
s.erase(s.find_last_not_of('0') + 1, std::string::npos);
//remove dangling decimal (123. => 123)
return (s[s.size()-1] == '.') ? s.substr(0, s.size()-1) : s;
}
开始之前,请检查此功能是否花费了大量时间。通过使用分析器或其他方式进行测量来完成此操作。知道你调用它无数次固然很好,但如果你的程序仍然只在这个函数上花费了 1% 的时间,那么你在这里所做的任何事情都不可能将程序的性能提高超过 1%。如果是这种情况,您的问题的答案将是“就您的目的而言,不,此功能无法显着提高效率,如果您尝试,您就是在浪费时间”。
首先,避免
s.substr(0, s.size()-1)
。这会复制大部分字符串 并且 它使您的函数不符合 NRVO 的资格,所以我认为通常您会在返回时得到一份副本。所以我要做的第一个更改是将最后一行替换为:
if(s[s.size()-1] == '.') {
s.erase(s.end()-1);
}
return s;
但是如果性能是一个严重问题,那么我会这样做。我并不保证这是最快的,但它避免了不必要的分配和复制的一些问题。任何涉及
stringstream
的方法都需要从字符串流复制到结果,因此我们需要一个更底层的操作,snprintf
。
static std::string dbl2str(double d)
{
size_t len = std::snprintf(0, 0, "%.10f", d);
std::string s(len+1, 0);
// technically non-portable, see below
std::snprintf(&s[0], len+1, "%.10f", d);
// remove nul terminator
s.pop_back();
// remove trailing zeros
s.erase(s.find_last_not_of('0') + 1, std::string::npos);
// remove trailing point
if(s.back() == '.') {
s.pop_back();
}
return s;
}
对
snprintf
的第二次调用假定 std::string
使用连续存储。这在 C++11 中得到了保证。在 C++03 中不能保证这一点,但对于 C++ 委员会已知的所有积极维护的 std::string
实现都是如此。如果性能确实很重要,那么我认为做出不可移植的假设是合理的,因为直接写入字符串可以节省稍后复制到字符串中的操作。
s.pop_back()
是 s.erase(s.end()-1)
的 C++11 表达方式,s.back()
是 s[s.size()-1]
对于另一个可能的改进,您可以摆脱对
snprintf
的第一次调用,而是将s
的大小设置为像std::numeric_limits<double>::max_exponent10 + 14
这样的某个值(基本上,-DBL_MAX
需要的长度)。问题在于,这分配和置零的内存远多于通常需要的内存(IEEE 双精度数为 322 字节)。我的直觉是,这将比第一次调用 snprintf
慢,更不用说在字符串返回值被调用者保留一段时间的情况下浪费内存。但你总是可以测试它。
或者,
std::max((int)std::log10(d), 0) + 14
计算所需大小的相当严格的上限,并且可能比 snprintf
精确计算它更快。
最后,或许可以通过改变功能接口来提升性能。例如,您可以附加到调用者传入的字符串,而不是返回新字符串:
void append_dbl2str(std::string &s, double d) {
size_t len = std::snprintf(0, 0, "%.10f", d);
size_t oldsize = s.size();
s.resize(oldsize + len + 1);
// technically non-portable
std::snprintf(&s[oldsize], len+1, "%.10f", d);
// remove nul terminator
s.pop_back();
// remove trailing zeros
s.erase(s.find_last_not_of('0') + 1, std::string::npos);
// remove trailing point
if(s.back() == '.') {
s.pop_back();
}
}
然后调用者可以
reserve()
足够的空间,多次调用您的函数(可能在其间附加其他字符串),并将结果数据块一次性写入文件,除了 reserve
之外,无需任何内存分配
。 “充足”不一定是整个文件,它可以一次是一行或“段落”,但任何避免无数内存分配的东西都是潜在的性能提升。
在速度或简洁性方面高效?
char buf[64];
sprintf(buf, "%-.*G", 16, 1.0);
cout << buf << endl;
显示“1”。在恢复为科学记数法之前,格式化最多 16 位有效数字,不带尾随零。
snprintf
和一组 char
代替 stringstream
和 string
char
缓冲区的指针传递给其打印的 dbl2str (以避免返回时调用 string
的复制构造函数)。将要打印的字符串组装到字符缓冲区中(或在调用时将字符缓冲区转换为字符串或将其添加到现有字符串中)在头文件中声明函数
inline
#include <cstdio>
inline void dbl2str(char *buffer, int bufsize, double d)
{
/** the caller must make sure that there is enough memory allocated for buffer */
int len = snprintf(buffer, bufsize, "%lf", d);
/* len is the number of characters put into the buffer excluding the trailing \0
so buffer[len] is the \0 and buffer[len-1] is the last 'visible' character */
while (len >= 1 && buffer[len-1] == '0')
--len;
/* terminate the string where the last '0' character was or overwrite the existing
0 if there was no '0' */
buffer[len] = 0;
/* check for a trailing decimal point */
if (len >= 1 && buffer[len-1] == '.')
buffer[len-1] = 0;
}