C++的std::string的“读时也拷贝”技术!
C++的std::string的读时也拷贝技术!
嘿嘿,你没有看错,我也没有写错,是读时也拷贝技术。什么?我的错,你之前听说写过时才拷贝,嗯,不错的确有这门技术,英文是Copy On Write,简写就是COW,非常’牛’!那么我们就来看看这个’牛’技术的效果吧。
我们先编写一段程序
#include <string> #include <iostream> #include <sys/time.h> static long getcurrenttick() { long tick ; struct timeval time_val; gettimeofday(&time_val , NULL); tick = time_val.tv_sec * 1000 + time_val.tv_usec / 1000 ; return tick; } int main( ) { string the_base(1024 * 1024 * 10, 'x'); long begin = getcurrenttick(); for( int i = 0 ;i< 100 ;++i ) { string the_copy = the_base ; } fprintf(stdout,"耗时[%d] \n",getcurrenttick() - begin ); }
嗯,一个非常大的字符串,有10M字节的x,并且执行了100此拷贝。编译执行它,非常快,在我的虚拟机甚至不要1个毫秒。
现在我们来对这个string加点料!
int main(void) { string the_base(1024 * 1024 * 10, 'x'); long begin = getcurrenttick(); for (int i = 0; i < 100; i++) { string the_copy = the_base; the_copy[0] = 'y'; } fprintf(stdout,"耗时[%d] \n",getcurrenttick() - begin ); }
现在我们再编译并执行这断程序,居然需要4~5秒!哇!非常美妙的写时才拷贝技术,性能和功能的完美统一。
我们再来看看另外一种情况!
string original = "hello"; char & ref = original[0]; string clone = original; ref = 'y';
我们生成了一个string,并保留了它首字符的引用,然后复制这个string,修改string中的首字符。因为写操作只是直接的修改了内存中的指定位置,这个string就根本不能感知到有写发生,如果写时才拷贝是不成熟的,那么我们将同时会修改original和clone两个string。那岂不是灾难性的结果?幸好上述问题不会发生。clone的值肯定是没有被修改的。看来COW就是非常的牛!
以上都证明了我们的COW技术非常牛!
有太阳就有黑暗,这句说是不是有点耳熟?
int main(void) { string the_base(1024 * 1024 * 10, 'x'); fprintf(stdout,"the_base's first char is [%c]\n",the_base[0] ); long begin = getcurrenttick(); for (int i = 0; i < 100; i++) { string the_copy = the_base; } fprintf(stdout,"耗时[%d] \n",getcurrenttick() - begin ); }
啊,居然也是4~5秒!你可能在想,我只是做了一个读,没有写嘛,这到底是怎么回事?难道还有读时也拷贝的技术!。
不错,为了避免了你通过[]操作符获取string内部指针而直接修改字符串的内容,在你使用了the_base[0]后,这个字符串的写时才拷贝技术就失效了。
C++标准的确就是这样的,C++标准认为,当你通过迭代器或[]获取到string的内部地址的时候,string并不知道你将是要读还是要写。这是它无法确定,为此,当你获取到内部引用后,为了避免不能捕获你的写操作,它在此时废止了写时才拷贝技术!
这样看来我们在使用COW的时候,一定要注意,如果你不需要对string的内部进行修改,那你就千万不要使用通过[]操作符和迭代器去获取字符串的内部地址引用,如果你一定要这么做,那么你就必须要付出代价。当然,string还提供了一些使迭代器和引用失效的方法。比如说push_back,等, 你在使用[]之后再使用迭代器之后,引用就有可能失效了。那么你又回到了COW的世界!比如下面的一个例子
int main( ) { struct timeval time_val; string the_base(1024 * 1024 * 10, 'x'); long begin = 0 ; fprintf(stdout,"the_base's first char is [%c]\n",the_base[0] ); the_base.push_back('y'); begin = getcurrenttick(); for( int i = 0 ;i< 100 ;++i ) { string the_copy = the_base ; } fprintf(stdout,"耗时[%d] \n",getcurrenttick() - begin ); }
一切又恢复了正常!如果对[]返回引用进行了操作又会发生情况呢,有兴趣的朋友可以试试!结果非常令人惊讶。
另外:上述例子是在linux环境下编译的,使用STL是GNU的STL。windows上我用的是vs2003,但是非常明显vs2003一点都不支持COW。
这篇文章出自http://ridiculousfish.com/blog/archives/2009/09/17/i-didnt-order-that-so-why-is-it-on-my-bill-episode-2/ 这里,我使用了它的例子。但是我重新自己组织了内容。
编写这篇文章的同时,我还参考了耗子的《标准C++类string的Copy-On-Write技术》一文
(转载本站文章请注明作者和出处 酷 壳 - CoolShell ,请勿用于任何商业用途)
《C++的std::string的“读时也拷贝”技术!》的相关评论
[]操作符一般会有两个:operator []()和operator[]()const,所以对一个const string对象进行[]时,应该不会COR吧?
相当不错的文章,从另一个角度说明了“Copy-On-Write”的另一个缺陷,读时也会发生Copy,哈哈,读时也拷贝,有意思。COW牛是牛,但是因为其不是完美的,而且在多线程中因为内存共享没有锁,会造成更严重重的问题。
另外,在原文中,不单单只是“引用”,还有“迭代器”也会造成读时才拷贝。原文中也提到过,在C++的标准书中,本来是可以使用const来区别读和写的,从而可以让const方式不发生COW,但C++标准委员会更为注重简单,方便和安全,所以并没有这样做。
如果按照标准,所有的STL类都会存在COW的问题(或者按LZ所调侃的“读时也拷贝”),但在多线程的今天,这样的设计还应该考虑线程同步或互斥的问题,如果在STL中加上线程同步的问题,那么这无疑会把一个语言范畴问题加入了系统范畴的问题,就算是跨平台了,这种设计也会让STL万劫不复。
今天,大多数新的STL库都把COW移除了,因为大家知道了安全和稳定这两件事比什么都重要,而这两个事正是C++的短板。
COW可能会带来多线程的性能问题吧,而且要用到那么长的字符串,而且又是重复的字符串也没必要非要用string来维护
同意2楼,21世纪了,COW 反而是不靠谱的
the_copy would be eliminated by DSE.
Please look into object code before drawing any conclusions..
精彩
皓哥,vc6.0下cow特性还在,在vc6.0下使用你的例子FreeLibrary后确实crash了,但是同样有cow特性的g++4.8.2 在dlclose后并不会crash,这应该是win平台和linux对于共享库不同处理造成的吧
评论错了,是http://coolshell.cn/articles/12199.html 《C++ STL STRING的COPY-ON-WRITE技术》中的例子