拯救你的字符串

 

你还在为string s = str1 + "123" + str2 + str3;这样的链式运算代码产生的临时对象,和二次级复杂度(最少的复杂度为str1 长度乘以"+"的个数)而苦恼吗?

 

从今天开始起,彻底远离这些问题吧。

 

这招我原本以为是自己发明的,还给他取了个名字叫做"操作符代理"(英文翻译出来居然有关键字,歉),结果世间达人早有类似的思想, 叫做表达式模板(expression-template,我叫他et),(字体的区别其实是正房太太和偏房太太的区别,外资和民企的区别),嘿。但是我觉得我远远的扩展了它已有的一些概念,从而觉得我自己完全有资格给这项技术取个新名字。

 

大家可以先看看我的代码,代码中2次测试了字符串8次连加,单个字符串ascii长度在10-30(这样的长度很常见吧,url, sql, xml操作中最最常见的长度区间)。连续执行10000000次。对于2次测试的区别,请注意115行和123行的区别(看到115行那个seed"种子"了吧,呵呵)。*strb.begin() = '0'是用来防止某些stl实现使用引用计数和copy on write减少了拷贝次数(这样的技术天生就是为了让你在双核多线程程序中困惑的)。

 

 

release编译后运行测试。具体的时间显然和各自机器性能有关,直接比较没有意义,有的服务器太快我甚至把测试次数加了2个0。就说说比例吧:

 

个人电脑,amd双核xp系统下关闭一切其他软件,

1)vc6编译器 + vc6stl,“有种”版本拷贝用的时间大约为“没种”版本用时的56%,

2)intel编译器(v10.1.034)+ vc6stl结果大致与1)相同,不到1%的波动范围;

3)vc6编译器 + stlport4.6.2,“有种”版本拷贝用的时间大约为“没种”版本用时的75%,这个版本用iostream来输出编译器会崩溃,呵呵

4)intel编译器(v10.1.034)+ stlport4.6.2,“有种”版本拷贝用的时间大约为“没种”版本用时的60%。

5)vc2008编译器 + vc2008stl,“有种”版本拷贝用的时间大约为“没种”版本用时的70%

 

公共服务器,由于有别人在使用,数据偶尔有波动,不过我看top几乎也就我的程序一直占着99%的cpu,呵呵

5)unix-center的 ubuntu服务器双核单线程(ubuntu.unix-center.net), gcc 4.2.4,“有种”版本拷贝用的时间大约为“没种”版本用时的89-92%。

 

6)unix-center的 solaris服务器八核四线程(t1000.unix-center.net), gcc 4.0.3,“有种”版本拷贝用的时间大约为“没种”版本用时的80-85%。

7)unix-center的 aix服务器2芯双核单线程(aix.unix-center.net), gcc 4.2.0,居然链接说找不到string的构造函数,go,google说这个版本aix的gcc确实有这问题,我也没心思去给它再编译一个来玩了。

 

结论:

大家也看到了,“有种”和"没种"之间性能提升还是不小的,windows平台尤其明显。gcc似乎比较生猛,性能提升不像win那么bt,我估计是匿名返回值优化作的比较好吧。另外有一点打击我的是intel编译器居然并不比vc6快多少,亏我逢人就推荐它呢。总之如果你存在性能优化的需求并且c++代码中有如上所述的自定义2元运算符链式操作,上面的技术还是很值得你考虑的,几乎不费一枪一弹.(不!花了一"弹",呵呵)

 

用法:

首先确定了操作结果类型后,先定义一个种子.如106行 et_seed<std::string> seed;创建一个string类型的种子.

 

然后把种子放到运算链的左端, 可以放在第一位,如本代码115行;也可以放第二位. (你熟悉std::string和const char*的混合连加的约束话,就明白这里为什么不能放在第三位了). 用操作符把它和已有的运算链合并.这里是"+". 一般放在第一位比较好看也比较好改, 推荐.

 

最后如果有改变运算顺序的括号,并且该括号内没有子括号,把括号内部的表达式看作一个独立的运算链重复上一步.

如本代码115行,string("3 ")和改变运算顺序无关,不理睬,而下一个括号显然改变了运算顺序,但是里面有子括号,我们只需要在子括号那里再放一次种子.

总之找最""的括号放种子.注意种子放多了也没事,0损失,只要你喜欢种菜.挖最深的土去种菜和广撒种几乎没有多大区别,不是吗?

 

好了,大功告成!10%-40%的效率提升等着你.

 

事实上,绝大多数操作仅需要创建一个种子然后放在最左端就可以了,只要满足结合律的自定义运算符我们其实都可以把括号去掉(即满足a+b+c == a+(b+c)),上面的代码是为了说明.另外种子是可以重用的,有兴趣你甚至可以让他singleton.  临时(匿名,右值,叫法很多)种子也是可以用的, 115行可以把seed换成et_seed<std::string>(),看你兴趣了.说了这么久了你也应该注意到了et_seed是一个空类,创建几乎是无开销的(取决于你的编译器) .所以种子种多了也没有什么损失.

 

 

不管怎么说,这一招还是很有原创性和实用价值的:

1。非侵入性,只需要在使用的时候"+"一个“种子”。几乎不可能和已有的代码产生冲突。

2。极佳的易用性,顶多只需要修改代码10来个字母左右。

3。极佳的可移植性,笔者在不影响效率的前提下尽可能的少的使用一些c++的高级特性,小心的让代码可以在vc6这些古董编译器中编译通过。

4。极广的适用范围,和原有的操作符相比,仅满足了下列要求的类型就可以使用,这里用"+"操作和std::string类型来描绘一下而已,大家可以把+批量替换成其他2元运算符来使用,如*,/,-等(c++的泛型不能泛操作符,囧)。

要求分为语法要求语义要求2类:

<1>有无参构造函数(自定义或默认皆可)。只要不是引用类(如std::auto_ptr_ref),都应该有吧。语法要求

 

<2>支持"+"操作。废话还是要说的。语法要求

 

<3>"+"操作返回类型固定为原类型。见过string + string == string  && string + const char* == const char* 这种情况吗?如果你见到这样的类,还是赶紧把它扔掉吧,别让它污染你的代码。语义要求


<4>支持和"+"操作相同类型的"+="操作(非必须)。如string的 "+"操作支持string,const char*,char等操作类型,string的"+="操作也通过重载支持了这些类型。如果你的"+="少支持了某些类型,不到用时不抱错语义要求

 

<5>"+="操作和"+"操作的参数都是传常量引用类型或者传值的如string operator+ (const string& lhs, char rhs);第一个传常量引用,第2个传值。string& operator+= ( char c );同上理。如果你用某个类时遇到a=b+c之后,b或c的值发生了变化的情况,推荐你还是珍爱生命,远离不良类库吧。语法要求


<6>a = a + b和a += b等价, a 等价于 string() + a, a + b + c等价于a + (b + c) 。语义要求。非常重要!!

 

注1:

如果违反上面这些要求中的某一条,不满足语法要求将会编译时抱错,不满足语义要求可能编译时报错,也有可能编译时不报错而可以运行,运行时又分高效运行和原效率运行2种情况,具体情况非常复杂,大家可以留言讨论。违反了要求<6>将会导致错误的运行结果!模板的静态检查能力毕竟有限,我能设的语法桩有限,何况我还要兼容古董编译器,太复杂了编译器直接罢工...

注2:

除非违反了要求<6>,不存在编译时不抱错而运行时错误的情况。运行时发生错误请首先检查操作类型是否满足<6>.

注3:

还有一些要求,满足上述要求的同时也就自动满足了,如可同类型拷贝构造等("+"操作返回值所要求的)。不再赘述.反而想提醒大家同类型拷贝构造函数有explicit关键字修饰吗?

 

经典的c++库里面的"+"操作符重载都满足上述要求,如stl, ace, qt等等(boost太博大精深了,笔者不敢妄言,呵呵)。你写的库呢,够"种"吗?

 

简单描述原理:种子会让"+"运算符的结果为一个2叉树,节点为"+"左右运算对象的引用;而2叉树和2叉树,2叉树和原始的运算对象"+"的结果都是一棵新的二叉树.从而最终二元链式"+"运算结果为一棵二叉树, 其中叶子节点为原始运算对象和种子,种子本质是用来第一步作为叶子节点帮助原始的运算对象构造2叉树,避开原有的二元"+"运算. 树最终将附值(或拷贝)给目标类型,此时树利用无参构造函数先生成一个目标类型对象R,然后从头后序遍历整棵树,遇到子树继续后序遍历,遇到原始的运算对象时利用它对生成的目标类型对象R作+=操作,遇到种子节点不做任何操作.此过程中利用+=返回引用不需要返回值的优点,一次性的构造出了结果对象R.最后利用生成的结果对象R,附值(或拷贝).

 

注意这里的二叉树包含2个引用,创建开销很小,二叉树创建的数量和+的数量相同.

 

关键字:二叉树,+=操作.

(最新版本请阅读)

http://blog.csdn.net/zy498420/archive/2010/11/10/6000525.aspx

可能有的朋友看我的代码有些吃力,我在下一篇会和大家解释其中的一些奥妙,我的一些心得,我走的一些弯路,我的一些建议.其实c++库的设计就是这样的,写库的人累死累活,用库的人方便的欲仙欲死.笔者这里至少写了5000行代码才有了现有这个种子的思路的.并不是每个人都需要看懂stl的,会用就行!!

 

鄙人对代码版权毫无任何要求,但是请求拷贝走代码的朋友请留言,方便笔者统计.如果有更好建议,还请不吝赐教.

 

补充一句:这个东西取名叫"种子",是为了纪念一门几乎不为人知的民族英烈

种世衡,种谔,种师道,种师中

世代英烈,先后对抗西夏,辽,金,论贡献比杨家将大多了,很可惜现在的人除非读水浒时注意到“老种经略相公”和“小种经略相公"并且愿意深入探寻者方知一二.

水浒里面的好汉人人几乎都以在大小种经略相公手下做过事为荣耀,王进、鲁智深、杨志等都在老种经略相公手下任过职,金钱豹子汤隆在老种经略相公手下打造过兵器. 大小种经略相公是谁,希望大家还是踊跃去搜搜,这样的英雄值得记忆!

 

远离教科书,"礼失求诸于野",独立思考,立剑为脊!

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值