最近写一个程序中两个差不多的模块,一个使用了snprintf输出中间数据,另一个偷懒使用stringstream。结果你猜怎么着?居然压帧了!!到底是谁拖了性能的后退?
我上网一搜,有人做了性能分析实验。他的实验demo大概有4个步骤:
在十万次调用结束后,上述4种方式所耗时情况为: 方法 2 > 方法 3 > 方法 4 > 方法 1 方法2>方法3>方法4>方法1 方法2>方法3>方法4>方法1
可见,不要干这种不必要的在循环体内部反复构造析构的事情。
那么,为啥呢?这俩到底做了啥呢?
观察它的源码,会发现和其他printf一样,他是一个可变参函数,这就意味着它会经历一系列的递归展开:
/* Maximum chars of output to write in MAXLEN. */
extern int snprintf (char *__restrict __s, size_t __maxlen,
const char *__restrict __format, ...)
__THROWNL __attribute__ ((__format__ (__printf__, 3, 4)));
当展开到最底层的时候,这个函数首先根据所需的字符串长度预先分配内存,底层差不多这样:
char* buf = (char*)malloc(buf_size);
然后,对分配的内存执行格式化操作:
int result = vsnprintf(buf, buf_size, format, args);
可以看到有意思的是,他的参数展开是依赖于vsnprintf
这个函数的:
extern int vsnprintf (char *__restrict __s, size_t __maxlen,
const char *__restrict __format, _G_va_list __arg)
__THROWNL __attribute__ ((__format__ (__printf__, 3, 0)));
为了不让自己的头变得很大,我在这里做一个非常短小精悍的vsnprintf精华版实现:
int vsnprintf(char *__restrict __s, size_t __maxlen,
const char *__restrict __format, _G_va_list __arg) {
int result;
va_list copy;
va_copy(copy, args);
result = vsnprintf_l(__restrict __s, __maxlen, __restrict __format, copy);
va_end(copy);
return result;
}
这里其实他的实现因编译器而异,我在这使用了vsnprintf_l,是一个线程安全的版本。
接下来,打住!请确保你已经了解了可变参数列表和相关函数的基础知识!如果不太了解,过两天我再写一个博客(肥水不流外人田.jpg)
接下来,我们看一下这个函数干了什么:
va_copy(copy, args);
创建了一个可变参数列表的副本。为什么要创建副本呢?本质上是为了防止修改参数列表而对原来的参数列表造成难以debug的痛苦影响。vsnprintf_l(__restrict __s, __maxlen, __restrict __format, copy);
这个函数接收了下列参数并格式化了buf显然,这个时候,我们陷入了一个套娃:看起来snprintf要做的事,被vsnprintf拿去了,而vsnprintf要做的事,又被vsnprintf_l拿去了!
为什么呢?因为涉及到数据的写入,我们一定得考虑多线程的情况下是否写入操作是安全的。
我们来看看这个线程安全的vsnprintf_l:
#define MAX_BUFFER_SIZE 1024
typedef struct {
locate_t locate;
char buffer[MAX_BUFFER_SIZE];
size_t size;
}vsnprintf_data;
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
// 名字很长长长长的参数名写不动了,换成小名吧 =.=
int vsnprintf_l(char *str, size_t size, const char *format, va_list args) {
vsnprintf_data data;
data.size = size;
data.locale = locale_t();
if (format) {
data.locale = newlocale(LC_ALL_MASK, format, data.locale);
}
int result = vsnprintf(data.buffer, MAX_BUFFER_SIZE, format, args);
if (data.locale) {
freelocale(data.locale);
}
return result;
}
我们看到,我们有一个用于存储线程安全的数据的结构体vsnprintf_data,它的内容是这样的:
locate_t locate
:存储当前线程的locate信息buffer
:存储格式化后的字符串size
:我们的老朋友size,表示缓冲区的大小我们首先定义了一个互斥锁来守护多线程环境下操作的正确性。接着:
需要注意的是,在snprintf函数中,每次重新分配内存后,新的内存块会被写入到原始内存块的后面,以充分利用已分配的内存空间。此外,如果在第一次分配内存后有足够的空间容纳格式化后的字符串,那么不会发生重新分配内存的情况。
可以看到,由于格式化字符串解析的复杂性、参数的数量和类型、字符串的大小和内容等因素,这个函数的性能会受到一些影响。
完了,打不到车了。我先打车回家明天再写55555
我来更新了。
我们再看std::stringstream
。
stringstream本质上就是一个类,我截取了一部分定义:
template <typename _CharT, typename _Traits, typename _Alloc>
class basic_stringstream : public basic_iostream<_CharT, _Traits>
{
public:
// Types:
typedef _CharT char_type;
typedef _Traits traits_type;
// _GLIBCXX_RESOLVE_LIB_DEFECTS
// 251. basic_stringbuf missing allocator_type
typedef _Alloc allocator_type;
typedef typename traits_type::int_type int_type;
typedef typename traits_type::pos_type pos_type;
typedef typename traits_type::off_type off_type;
// Non-standard Types:
typedef basic_string<_CharT, _Traits, _Alloc> __string_type;
typedef basic_stringbuf<_CharT, _Traits, _Alloc> __stringbuf_type;
typedef basic_iostream<char_type, traits_type> __iostream_type;
private:
__stringbuf_type _M_stringbuf;
public:
// Constructors/destructors
/**
* @brief Default constructor starts with an empty string buffer.
* @param __m Whether the buffer can read, or write, or both.
*
* Initializes @c sb using the mode from @c __m, and passes @c
* &sb to the base class initializer. Does not allocate any
* buffer.
*
* That's a lie. We initialize the base class with NULL, because the
* string class does its own memory management.
*/
explicit
basic_stringstream(ios_base::openmode __m = ios_base::out | ios_base::in)
: __iostream_type(), _M_stringbuf(__m)
{ this->init(&_M_stringbuf); }
可以看到,它里面也有一个用于存放string的buffer,使用ios::in
和ios::out
来执行底层的输入输出。
既然有buffer,那我们就应该想到,这个类会确保缓冲区是否已满,是否buffer需要清空重新分配。而这也应该是影响它性能最大的因素。
想到stringstream写入数据的三种方式:
put()
:它将单个字符写入到stringstream我们刚才所看到的缓冲区中。在使用这个函数时,会检查当前缓冲区是否满,如果不满,则直接写入;否则分配更大的空间。write()
:这个函数将制定数量的字符从给定的字符数组写入到缓冲区。<<
运算符。看着还是后者方便啊。叹息。