CString 的效率

CString 的一个问题是它确实掩藏了一些低效率的东西。从另外一个方面讲,它也确实可以被实现得更加高效,你可能会说下面的代码:

CString s = SomeCString1;
s += SomeCString2;
s += SomeCString3;
s += ",";
s += SomeCString4;

比起下面的代码来,效率要低多了:

char s[1024];
lstrcpy(s, SomeString1);
lstrcat(s, SomeString2);
lstrcat(s, SomeString 3);
lstrcat(s, ",");
lstrcat(s, SomeString4);

  总之,你可能会想,首先,它为 SomeCString1 分配一块内存,然后把 SomeCString1 复制到里面,然后发现它要做一个连接,则重新分配一块新的足够大的内存,大到能够放下当前的字符串加上SomeCString2,把内容复制到这块内存 ,然后把 SomeCString2 连接到后面,然后释放第一块内存,并把指针重新指向新内存。然后为每个字符串重复这个过程。把这 4 个字符串连接起来效率多低啊。事实上,在很多情况下根本就不需要复制源字符串(在 += 操作符左边的字符串)。
  在 VC++6.0 中,Release 模式下,所有的 CString 中的缓存都是按预定义量子分配的。所谓量子,即确定为 64、128、256 或者 512 字节。这意味着除非字符串非常长,连接字符串的操作实际上就是 strcat 经过优化后的版本(因为它知道本地的字符串应该在什么地方结束,所以不需要寻找字符串的结尾;只需要把内存中的数据拷贝到指定的地方即可)加上重新计算字符串的长度。所以它的执行效率和纯 C 的代码是一样的,但是它更容易写、更容易维护和更容易理解。
  如果你还是不能确定究竟发生了怎样的过程,请看看 CString 的源代码,strcore.cpp,在你 vc98的安装目录的 mfc/src 子目录中。看看 ConcatInPlace 方法,它被在所有的 += 操作符中调用。

啊哈!难道 CString 真的这么"高效"吗?比如,如果我创建

CString cat("Mew!");

  然后我并不是得到了一个高效的、精简的5个字节大小的缓冲区(4个字符加一个结束字符),系统将给我分配64个字节,而其中59个字节都被浪费了。
  如果你也是这么想的话,那么就请准备好接受再教育吧。可能在某个地方某个人给你讲过尽量使用少的空间是件好事情。不错,这种说法的确正确,但是他忽略了事实中一个很重要的方面。
  如果你编写的是运行在16K EPROMs下的嵌入式程序的话,你有理由尽量少使用空间,在这种环境下,它能使你的程序更健壮。但是在 500MHz, 256MB的机器上写 Windows 程序,如果你还是这么做,它只会比你认为的“低效”的代码运行得更糟。
  举例来说。字符串的大小被认为是影响效率的首要因素,使字符串尽可能小可以提高效率,反之则降低效率,这是大家一贯的想法。但是这种想法是不对的,精确的内存分配的后果要在程序运行了好几个小时后才能体现得出来,那时,程序的堆中将充满小片的内存,它们太小以至于不能用来做任何事,但是他们增加了你程序的内存用量,增加了内存页面交换的次数,当页面交换的次数增加到系统能够忍受的上限,系统则会为你的程序分配更多的页面,直到你的程序占用了所有的可用内存。由此可见,虽然内存碎片是决定效率的次要因素,但正是这些因素实际控制了系统的行为,最终,它损害了系统的可靠性,这是令人无法接受的。
  记住,在 debug 模式下,内存往往是精确分配的,这是为了更好的排错。
  假设你的应用程序通常需要连续工作好几个月。比如,我常打开 VC++,Word,PowerPoint,Frontpage,Outlook Express,Forté Agent,Internet Explorer和其它的一些程序,而且通常不关闭它们。我曾经夜以继日地连续用 PowerPoint 工作了好几天(反之,如果你不幸不得不使用像 Adobe FrameMaker 这样的程序的话,你将会体会到可靠性的重要;这个程序机会每天都要崩溃4~6次,每次都是因为用完了所有的空间并填满我所有的交换页面)。所以精确内存分配是不可取的,它会危及到系统的可靠性,并引起应用程序崩溃。
  按量子的倍数为字符串分配内存,内存分配器就可以回收用过的内存块,通常这些回收的内存块马上就可以被其它的 CString 对象重新用到,这样就可以保证碎片最少。分配器的功能加强了,应用程序用到的内存就能尽可能保持最小,这样的程序就可以运行几个星期或几个月而不出现问题。
  题外话:很多年以前,我们在 CMU 写一个交互式系统的时候,一些对内存分配器的研究显示出它往往产生很多内存碎片。Jim Mitchell,现在他在 Sun Microsystems 工作,那时侯他创造了一种内存分配器,它保留了一个内存分配状况的运行时统计表,这种技术和当时的主流分配器所用的技术都不同,且较为领先。当一个内存块需要被分割得比某一个值小的话,他并不分割它,因此可以避免产生太多小到什么事都干不了的内存碎片。事实上他在内存分配器中使用了一个浮动指针,他认为:与其让指令做长时间的存取内存操作,还不如简单的忽略那些太小的内存块而只做一些浮动指针的操作。(His observation was that the long-term saving in instructions by not having to ignore unusable small storage chunks far and away exceeded the additional cost of doing a few floating point operations on an allocation operation.)他是对的。
  永远不要认为所谓的“最优化”是建立在每一行代码都高速且节省内存的基础上的,事实上,高速且节省内存应该是在一个应用程序的整体水平上考虑的。在软件的整体水平上,只使用最小内存的字符串分配策略可能是最糟糕的一种方法。
  如果你认为优化是你在每一行代码上做的那些努力的话,你应该想一想:在每一行代码中做的优化很少能真正起作用。你可以看我的另一篇关于优化问题的文章《Your Worst Enemy for some thought-provoking ideas》。
  记住,+= 运算符只是一种特例,如果你写成下面这样:

CString s = SomeCString1 + SomeCString2 + SomeCString3 + "," + SomeCString4;

则每一个 + 的应用会造成一个新的字符串被创建和一次复制操作。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章