侃一侃vc的std::string

来源:互联网 发布:隐秘录像 知乎 编辑:程序博客网 时间:2024/06/05 19:01
 
那天心血来潮敲下了这坨代码:
<!--

Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/

-->cout << sizeof(std::string) << endl;
我的平台是XP+VC9.0,运行结果是32,不知道为什么要那么多,于是在源代码里捣鼓了一番,许久之后终于有了一点眉目,下面是我的一些总结。


一,结构布局
string的原型是

<!--

Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/

-->typedef basic_string<char, char_traits<char>, allocator<char> >    string;

它是basic_string一个typedef,首先来看basic_string的类层次结构:

_Container_base_secure
       /\
       |
_String_base
       /\
       |
_String_val
       /\
       |
basic_string


_Container_base_secure里有一个_Iterator_base类型的指针成员_Myfirstiter,得占4个字节,_String_base没有数据成员,_String_val里有一个_Alval类型(一个allocator)的成员_Alty,没有数据成员,只占用1个字节,加上padding,也就是4个字节,basic_string有3个数据成员,一个union占16个字节,两个跟长度相关的整型变量各4个字节。所有类都不含虚函数,布局如下:

<!--

Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/

-->_Container_base_secure
    _Myfirstiter:_Iterator_base
*  (4 bytes)

_String_base (nop)

_String_val
      _Alty:_Alval  (
1 byte +3 bytes padding)
      
basic_string
    _Bx        :   _Bxty       (
16 bytes)
    _Mysize :   size_type   (
4 bytes)
    _Myres :  size_type   (
4 bytes)



这个是basic_string的union:
<!--

Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/

-->enum{  _BUF_SIZE = 16 / sizeof (_Elem) < 1 ? 1 : 16 / sizeof(_Elem)};
union _Bxty

    _Elem
  _Buf[_BUF_SIZE];
    _Elem
* _Ptr;
} _Bx;

通过这个union可以看出vc的std::string在字符串长度较小的时候会使用一个栈上缓冲区(_Bxty::_Buf)来保存字符串内容,如果字符串长度超过了某个范围则会使用allocator分配动态内存(_Bxty::_Ptr),_BUF_SIZE控制着这个范围值,_Buf缓冲区始终是16个字节大小。可以看出,vc的std::string没有使用通用Copy-On-Write技术,因为它没有reference count成员。那么vc采用的这个技术对于字符串的使用效率在实际使用中表现如何呢?

下面是一段测试拷贝不同字符串长度的代码:
<!--

Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/

-->#include <windows.h>
#include 
<stdio.h>
#include 
<iostream>
#include 
<string>
#include 
<vector>

using namespace std;

template 
<size_t N>
void test_string()
{
    
const size_t loop = 1000000; // 一百万次
   
    // 预先分配好内存
    vector
<string> vec;
    vec.resize(loop);
    
    
char szbuf[N] = {};
    memset(szbuf, 
'a', N - 1);

    // 计算拷贝时间
    size_t time 
= GetTickCount();
    
for (int i = 0; i < loop; ++i)
    {
        vec[i] 
= szbuf;
    }
    time 
= GetTickCount() - time;

    printf(
"buffer size: %u, loop: %u, used %u ms\n", N, loop, time);
}


int main(int argc, char* argv[])
{
    test_string
<8>();
    test_string
<12>();
    test_string
<16>();
    test_string
<20>();
    test_string
<24>();
    test_string
<32>();

    
return 0;
}
编译使用release版全默认参数,结果如下:

buffer size: 4, loop: 1000000, used 78 ms
buffer size: 8, loop: 1000000, used 94 ms
buffer size: 12, loop: 1000000, used 93 ms
buffer size: 16, loop: 1000000, used 94 ms
buffer size: 20, loop: 1000000, used 422 ms
buffer size: 24, loop: 1000000, used 422 ms
buffer size: 32, loop: 1000000, used 438 ms


在字符串(包含0结尾字符)小于16个字节的时候消耗的时间都很低而且几乎一致(94ms),而一旦超过了16个字节消耗时间则迅速增加,变成了422ms(近5倍)。可以看出,如果使用长度较小的字符串,在涉及大量拷贝处理的时候,vc这个技巧的效率还是挺高的。


下面是相同代码采用使用了Copy-On-Write技术的GNU std::string的运行结果(Ubuntu + gcc4.4 + -O2参数):

buffer size: 8, loop: 1000000, used 140 ms
buffer size: 12, loop: 1000000, used 119 ms
buffer size: 16, loop: 1000000, used 140 ms
buffer size: 20, loop: 1000000, used 126 ms
buffer size: 24, loop: 1000000, used 154 ms
buffer size: 32, loop: 1000000, used 129 ms


速度增长很平均,在16字节以下的时候用时比vc多,但是超过16字节后效率比vc高(细节没去研究)。


二,虚析构函数

basic_string和它的基类_Container_base_secure的析构函数都不是virutual function,这在继承的时候会有副作用,先看下面这段代码:
<!--

Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/

-->class mystring : public std::string
{
public:
    
~mystring()
    {
        cout 
<< "dctor" << endl;
    }
};

void main()
{
    std::
string* pstr = new mystring();
    delete pstr;  // ~mystring()没有被调用
}

没有虚函数所以析构函数无法动态绑定,上面的代码运行到delete时将直接调用std::string的析构函数,而不会去找mystring的析构函数。如果~mystring涉及到资源释放的话,那么无疑上面的代码将导致泄漏。

还有一点值得一提的就是对std::string或包含std::string的对象实施ZeroMemory

虽然大多数合格的C++程序员都知道不应该对非POD对象实施ZeroMomory,但是在工程实际中确实有这样的代码,而且他们运行的很正常(vc++下),这是为什么呢?我试图做一个简单解释。

如上所述,std::string的继承体系里没有virtual function,对象布局里也就没有vptr,而且整个继承体系都采用的是单继承,bptr又省了(参考Lippman的<Inside C++ Object Model>),所以std::string对象的布局全是它的自身的数据成员,_Container_base_secure的_Myfirstiter是指针成员,本身在初始化时就需要置零,_String_val的_Alval成员是个allocator,只有函数不包含数据,调用时也就不必传this指针,所以给它置零不会有副作用,basic_string指针本身的三个成员分别是缓冲区,字符串长度和预留长度,初始化时将它们置零也是没有副作用的。所以对std::string进行ZeroMemory操作的代码依然运行的很正常。


原创粉丝点击