随笔 - 59, 评论 - 580, 引用 - 50

导航

关于


微软提供的免费计数器

山不在高,有仙则名。水不在深,有龙则灵。斯是陋室,唯吾德馨。苔痕上阶绿,草色入帘青。谈笑有鸿儒,往来无白丁。可以调素琴,阅金经。无丝竹之乱耳,无案牍之劳形。南阳诸葛庐,西蜀子云亭。
子曰∶“何陋之有?”


My name is BEN.
This is my master's "doghouse". hehe...

标签

每月存档

最新留言

广告

【第1页/共5页,61条】
首页
前页
1
2005年07月12日

一直以来对于什么是AOP没有太深入的概念,直到读完白话面向智能体编程(Agent Oriented Programmig, AOP)[1]之后仍然是没有很深入的认识。也许日后有机会可以深入了解一下,不过今天想将读完该Blog之后的想法记载于此,尽管这样的思考也许挺幼稚的。我对于这些概念没有太多的知识,如果认识有误欢迎指正(通过交流获得知识我觉得是比较有效率的)。

  首先说明一下,这里的AOP当中的A是Agent,而不是Aspect。虽然目前我没有摸清楚Agent和Aspect的具体差别在哪里,但是可以肯定是有区别的。我想Aspect-OP应该是强调概念与实现的一对一映射,其中单一概念与单一职责原则比较接近,单一实现我暂时找不到比较接近的概念。关于单一实现可以这么说:我们大家都知道MVC的模式、或者一些其它的模式,这些模式其实通过我们口中说出来只是一种想法,或者说是概念。而最终形成代码的才是实现,可是也许会有很多地方会用到MVC,于是对于MVC就会有各种各样的实现。(参见“白话面向智能体编程(Agent Oriented Programmig, AOP)”系列第一篇一开始引用的文章The Ted Neward Challenge (AOP without the buzzwords)[2],原文的链接无效,这个是google出来的。)引用[2]的作者在文中提到他的研究当中最夸张的情形是,在某个软件当中有一种设计上的概念竟然有10万个(次)实施。(In one system I've studied, the concept:implementation ratio for that single requirement is well in excess of 1:100,000. )也许这么说还比较难理解,那么我们说说记录异常(to log failures),可能我们会在软件当中随手写道:
MessageBox.Show(ex);
又或者
logfile.WriteLine(ex.Message);
还可能是
myLogger.LogObjectTypeA(ex, objectA);
myLogger.LogObjectTypeB(ex, objectB);
...

  而事实上将错误记录下来应该是一个问题,或者一种设计上的概念,然而却会在软件当中存在多种不同的实施方案。这种情况对于维护来说可能是一件相当麻烦的事情,因为本来都是同一个概念里面的东西,如果我们因为概念上的问题需要进行更新维护,可能就需要对所有不同的实现进行维护,这将会是一件相当烦人的事情。

  上面说的是我对Aspect-OP的理解,那么Agent-OP呢?我想“白话面向智能体编程(Agent Oriented Programmig, AOP)”的系列说的还是比较容易理解的,其大意是,我们需要一个跟真实世界更加接近的模型。在Agent-OP里面有很多与OOP不一样的思考方向,例如“有心智”或者说“有目标感”,我想这一点是比较重要的概念。说到OOP我们自然能够想到“对象”,对象的其中一个作用是划定知识界限——什么属于这个对象以内的(比如对象当中的成员),什么属于这个对象以外的。这一概念对于软件界是一个里程碑,应当说是非常有意义的。然而我们也应该能够体会到OOP本身并没有对“驱动力”进行探讨,而事实上目前的软件中的“目标感”更多地是通过下列两种力量驱动的:用户,以及软件背后的设计。

  用户驱动并没有什么问题,毕竟使用软件的是人。然而通过软件背后的设计来驱动“目标感”,我一直都觉得不是很顺畅——因为这种设计相对来说是静态的,基本上不会根据环境的变化进行相对的调整。比如引用[1]当中提到的一个“鸡块熟了之后捞起来”的例子,也许我们会设计一个ChickenClass和一个ChefClass,ChickenClass有一个事件Cooked告诉ChefObject某个CheckenObject熟了,然后我们就会有下面的代码(C# 2k5):

CheckenObject.Cooked += ChefObject.Checken_Cooked;

  这个设计的后果是,不管厨师有多忙,鸡块觉得自己熟了之后就会踢厨师一脚,这时候厨师不管情愿不情愿都得把鸡块捞起来。事实上目前的绝大多数软件中的“厨师”本身根本就没有情愿不情愿想法,只不过是一个奴隶、一个工具而已。有人会说,有的软件中的“厨师”是有心智的,但我想还是与Agent-OP是有区别的,其区别可能在于两个方面:

1、有可能这只是无意识造成的偶然结果;
2、如果是有意识设计出这样的“厨师”,很有可能并非用的Agent-OP的工具。(可能制作的方法并不规范,可能存在一些问题。)

  也许Agent-OP比较前沿,我自己也说不清楚,换一个面向过程和面向对象来说吧。首先让我们时光倒流20年,假设我们正在使用C语言。我们能够将某些知识限定到某个范围以内吗?我想可以用struct,我们可以设计一个v-table,就像C++背后做的那样。于是我们也一样可以写出类似obj->Member(obj2); 这样的东西来,例如:(obj.v_table[ID_Member])(obj2);

  可这仍然不是OOP,还有很多的问题是需要有专门的OOP工具才能够解决的。Agent-OP也一样,我们目前如果要制作一个Agent-OP的软件,工具还是相当缺乏的,甚至连人的思想也相当缺乏的。不要忘了,光有OOP的工具,如果使用的人还是以面向过程的方式去思考和设计,最终出来的东西也不是OO的。我想Agent-OP也一样,例如让我来使用Agent-OP的工具兴许就弄不出什么花样来。

  我为什么会觉得需要Agent-OP呢?因为Agent-OP应该能够提供一定的自适应能力(比如什么时候鸡块算是熟了,什么时候我有时间去处理,以什么样的方式去处理等),有了自适应性才有可能提供自组织能力。没有自组织能力的系统,其整个设计实际上仍然是硬梆梆的。关于这点我们可以用制造行业的发展来做对比:现在的制造业有一种较新的概念叫做“柔性制造”,与此相对的是“刚性制造”。对于“刚性制造”来说,生产线的各个机构的运动方向只能够或者前后或者左右,顶多可以换一个模子或者刀具,因此其任务和目标是定死的。当任务或者目标发生改变的时候,比如原来是造汽车门的,现在要制作飞机机舱门了,那么整个生产线就报废了,对于软件来讲就是需要重新设计。但“柔性制造”就不一样了,生产线上的工作机构有许多的自由度,要制作什么完全依靠工控程序的控制,因此面对任务和目标的变化,只需要更换工控程序就可以了。对于软件来说,我们也希望能够仅仅替换某一个很小的“智能部件”,就能够在一定范围内改变任务和目标,因为我们都知道软件的需求(也就是任务和目标)的改变往往是代价非常高的。要达到这一目的,“智能部件”必须能够自行适应其外部的系统状态,或者说能够与系统的其它部分自行组织起来。要做到所有这些,以目前的技术条件看起来是相当困难的。

  说到这里,也许已经看到我对于Agent-OP的理解跟引文[1]中所述的有一定的出入了。正是因为这些出入,我对于原文的某些观点有一些不同意之处,且容我细细说来:

痒处一:OO并没有对现实世界中的实体加以区分
  引文[1]的作者认为需要区分“发票”和“员工”,因为“发票”是没有智能的死物,而“员工”是有智能的活物。死物只是被动的接受各种的操作,而活物除了会主动的进行操作之外,还有可能主动的进行调整,甚至是拒绝操作。
  然而我认为现实世界如此,并不代表虚拟的世界也必须如此。难道虚拟世界就不能够如同动画片或者科幻片中的想象那样,存在具有思想感情的“发票”吗?更尖锐一点说“机器(人)”是死物呢,还是活物?反过来也一样,“员工”一定是活物吗?还是那个例子,如果你家里有一个机器人员工你打算怎么办?当然,这样的例子比较变态,但是我想从中说明,虚拟世界的对象是否应该具有心智,并不取决于现实世界中影射的对象是否具有心智。而对于作者说的“可以引入计时器或者多线程,但是总是因为与现实不符而不爽”是非常认同的,然而实际上引起这一问题的实质并非在于虚拟对象与现实对象在心智上的映射不能相对应。关于这一问题,我想在后面的条目中会有所揭示。

痒处二:同步和异步被人为地剥离。
  因为[1]的作者认为,操作者不应该关心到底是使用同步方式还是异步方式,这个问题应该由被操作对象自行决定。
  然而我觉得这种想法可能存在问题。
  问题一:被操作对象必须具有心智才能够决定到到底是同步还是异步,假如被操作对象是一张发票,那么这如何进行解释?
  问题二:事实上现实世界绝大多数情况下都是以异步的方式进行的。老师说“同学们翻开第78页”,然后无论同学们翻不翻书,老师都会继续说下去。上司向下属布置任务,也只有上司在不停的说,决不会每一句话都等下属确认“明白”之后才说下一句话。此外,现实世界如果需要进行同步,通常都是双方都有决定权的。老师说“同学们翻开第78页”,如果哪个同学还没有翻到那一页,深怕漏听了哪一句话,肯定得说“老师等一下,我还没有翻到那一页”。(高考复习给出重点复习内容的时候大家没少遇到这种情况吧,大学期末考试老师给出考点的时候也没有少遇到这种情况吧?)而上司如果深怕下属没有想明白某句话的时候,也会问下属“明白了没有?”或者“有没有什么问题?”,等到下属说“明白”或者“没问题”之后才继续说下去。
  问题三:其实现实世界当中的同步是通过等待他人“虚拟”出来的概念,正如计算机世界的异步是通过独立线程“虚拟”出来的概念。只不过目前对于计算机世界来说,被调用方向要主动要求调用方等待(或者不要等待)是一件较为困难的事情。除了软件方法当中缺乏这样的概念和工具之外,这也对调用方提出了更高的知识要求:原来只要知道“同步”(或者“异步”)调用对方的处理方法就能够完成工作,而现在却还需要知道如果对方要求“异步”(或者“同步”)的话怎么办。较为简单的办法是调用方继续假装一切按照同步的方式进行,但是这样的话有何意义呢?反过来也有类似的问题。当然,问题总是能够解决的,只是这种解决所付出的代价和获得的回报之间是否存在利益。很明显,以目前的技术条件不可能在所有的操作面前我们都能够从中获利。(假如是工作流引擎的话,可能性会比较大,毕竟工作流比较贴近现实,异步的操作实际上比较多。)

痒处三:无法自然地模拟现实世界中的感知能力(Sensebility)
  引文[1]作者认为,目前软件的问题在于缺乏感知能力。例如:“如果鸡块的颜色由肉色转变至金黄色,俺就必须做出相应的操作/处理:把鸡块捞出锅来。”而目前的软件设计更多的是有鸡块引发“熟了”这个事件,然后因为“我”订阅了鸡块的“熟了”事件因而此时被鸡块“踢了一脚”,“我”就立刻按照程序把鸡块捞出来。从这种描述看来,“我”并不是通过观察“鸡块”的颜色是否变黄来判断“鸡块”是否“熟了”。
  而我认为这种想法也很有问题(也许只是作者的例子举的不恰当)。

  首先,这里并不是没有进行观察,只是观察的是一个现实世界当中不存在的观察量。假如我换一种方法,变成订阅“鸡块”的颜色值变化事件(BackColorChanged),当颜色值发生改变的时候鸡块就引发事件。而如果“我”发现“鸡块”的颜色偏黄到一定的程度((BackColor.R + BackColor.G)/BackColor.B > threashold),“我”就捞起“鸡块”。怎么样,现在改成观察鸡块是否变黄来决定鸡块是否该捞起来了吧?可是你一定还是觉得不对,因为根本原因不在观察什么,而在于“我”被鸡块“踢了一脚”。
  在现实世界如果你被鸡块踢了一脚告诉你该捞起来了,你也许会觉得不爽,但在计算机的世界未必如此。假如你给KFC开发一个自动捞鸡块的机器,客户一定期望着鸡块熟了的时候能够踢机器一脚,让它立刻把鸡块捞起来。而且在可以有直接的、积极的、主动的方法进行沟通的情况下,为什么非得要屏蔽这一方法,舍近求远舍本求末的采取间接的、消极的、被动的方法进行沟通呢?现实世界的鸡块之所以不会再熟了的时候踢你一脚,是因为现实世界的鸡块确实是死物,并不是你不期待它会踢你一脚。如果鸡块炸糊了,你会感到更不爽的。再举一个例子,为什么现实世界的烧水的水壶还带一个“哨子”,水烧开了就叫呢?就是因为实际上人们期望水烧开的时候水会“踢”你一脚,让你赶紧把火给关了。虽然水是死的,我们却希望让它活过来,不是吗?
  因此我觉得这里是本末倒置了:很多时候我们之所以希望贴近现实,只是因为我们期望它能够跟现实当中那样方便;而如果现实当中的情况不符合我们的期望的时候,我们就不应该按照现实中的那样去做。

  其次,这个例子之所以能够引起我们的感触,是因为我们需要对“鸡块”的颜色做出判断,但是计算机竟然不需要。而且现实当中每个人对于“鸡块”的颜色变成什么样了才算熟,并没有一致的答案。其根本原因在于现实世界有很多很多的“量”,大体可以分为两种“可观测量”和“不可观测量”。鸡块的便面颜色属于可观测量,是否熟了属于不可观测量。于是我们会面对根据可观测量猜测不可观测量的问题,而每个人之所以会有不同的答案,原因在于猜测过程当中需要用到很多知识,也包括经验。每个人的知识不同,因此猜测的准确度就不可能相同。在这个例子当中,我们的根本任务是根据不可观测量的变化作为进一步操作的依据,也就是说“如果鸡块熟了,就捞起来”。但是“熟了”这个量不可直接测得,因此只好根据可观测量猜测不可观测量的值。可是如果现在给你一个神奇设备,能够直接告诉你鸡块“熟了”,你还会多此一举去判断鸡块的颜色吗?也许会看一下,以防设备坏了不知道,但是如果设备没坏,我想你是不会费神去根据鸡块颜色来判断鸡块是否熟了。
  简而言之,如果软件中的鸡块能够提供“熟了”的信息,就不应该屏蔽它然后提供“表面颜色”来让厨师猜测,此举舍近求远,浪费脑细胞——除非你在设计一款猜谜游戏,玩家扮演的是“厨师”这个角色……
  当然,从另一个角度看,我们也确实可以看到,现在常见的商业软件方法并没有“不可观测量”这个概念(但是在科学计算里面经常可以看到,不过也不是作为软件设计概念存在的),更不要说对此有什么解决方案了。如果我们正在设计的部分属于跟现实世界进行互动的部分,那么这一概念是不能够忽略的。对于工作流引擎来说就是这样的一种情况——什么时候属于“完成”了或者“出状况”了,有的时候并没有一个直接可观测得标志能够表明这一情况。如果软件足够智能,就应该考虑根据可观测量对不可观测量进行猜测的能力。

  最后,作者在文中接近最后所提出的一个说法,我想要提出比较强烈的异议。作者说:
  “在现有的delegate解决方案中,我们只能针对实例(Instance)进行注册,而不可以针对类型(Type)进行注册。……如果能够提供针对类型的注册机制,只要将俺的后续操作到鸡块类上注册一次,在感知范围内的所有鸡块,管他是十块还是二十块,都能被俺感知到颜色上的变化并执行正确的后续操作,这样会来的更简洁,更自然。
  首先,至少在.NET里面类型上面完全可以有delegate/event,比如说AppDomain.UnhandledExceptions就是类型上的事件,针对类型进行注册在.NET里面是完全可能的,并且不难看到。因此说这个并不是OO的问题,顶多属于某种语言内部的问题。其次,这种通过注册到某个类上面来对所有正在炸的鸡块进行观测,是一种很不明智的做法。因为这不属于类型上面的知识范畴,而应该属于某个锅以内的鸡块集合的范畴。假如现在有三个锅正在炸鸡块,那引文[1]作者的想法可就得挨屁股了。

  不过话又说回来了,我非常认同作者说的其中一点,那就是:不应该由鸡块决定什么时候被捞起来,而应该由厨师来决定,尤其是要根据他自己所处环境的其他状况来决定——海啸来了还管鸡块干什么,赶紧逃命吧!这是目前的模型所欠缺的,不管厨师的情况如何,鸡块熟了都会引起厨师的相应动作。当然了,你可以这么写:

PriorityQueue needToBeFishedOut = new PriorityQueue();

private void Chef_ChickenCooked(object sender, EventArg e)
{
    needToBeFishedOut.Enqueue(sender);
    needToBeFishedOut.BringUpPriority();
}

然后再弄一个线程或者计时器进行相应的处理。然而正如作者所言,很不爽。好比让你用C来写真正OO的程序,用asm来写网页,或者用小刀来刻硬盘。工具不对,做起来既费劲又不规范,因此Agent-OP确实需要发展。

最后,我还是要补充一句,OOP和Agent-OP属于不同层次的概念,因此Agent-OP和OOP的关系更像是Class和Method的关系,前者不可能替代后者,并且应该在后者的基础上发展。(我甚至觉得Agent-OP已经不属于语言范畴的东西了,不一定会出现像OO出现之后,世界上所有语言都向OO的方向发展那样的壮观场景了。)

posted on 2005-07-12 14:59:00 by sumtec  评论(7) 阅读(4453)

 
2005年03月09日

首先感谢拓荒者为我们提供了错误样本。其次感谢kaneboy把这个错误报告上去,并告诉我们这却是一个Bug,并且知道这个Bug在桌面上的所有x86版本的.NET上面都存在。这个Bug预计在下一个版本的.NET 2.0 beta里面得到修正,已有版本的SP就不知道什么时候才有了……这个Bug到底有多严重,见仁见智吧,不想在这里做评论。这里写一下只是让闲得无聊的人见识一下这个奇异现象,或者让真的觉得威胁到自己软件安全的人注意一下。
其次希望微软能给我奖金,或者至少稿费。


该错误在一般使用的情况下很少遇到,但在非常特殊的使用方式下才会产生,尤其在您特意使用一些C#的副作用的时候更容易产生。不过如果遇到了,说不定真是会损失惨重。(我个人感觉这种错误跟Intel多年前奔腾芯片的浮点错误非常神似。)

 

错误简述:

如果您的程序:
1、在某个整形变量上面进行不带检查的加法操作(unchecked,默认的行为方式,不包括减法、乘法、除法操作),并且产生溢出(溢出后数值必然是负值),并且
2、立刻紧跟在该加法操作后面判断是否小于常量零(加完之后有乘除或者函数调用等,或者判断的是某个变量里面的零,或者判断的是小于其他常量值等,都不符合该条件),并且
3、在该函数当中使用了该变量的引用,例如:a.XXXX() 或者 AnotherFunction(ref a)。

或者与此相反的:
1、……减法……(……正值)……
2、……大于常量零……
3、……

则会引起上述第二步的判断与我们的期望不符——似乎该数大于等于0,并且因此没有执行该条件分支的语句或者语句块。



下面是错误重现:

1、在C#里新建一个Console项目
2、插入下列代码:

 class Test
 {
  static void Main()
  {
   int a = 0x79de61c0; //2044617152;
   a +=    0x12345678;    
   //a 应为 0x8c12b838;  //-1944930248
 
   if( a < 0 ) a = -a;
 
   System.Console.WriteLine( a );
   string str2 = a.ToString();
   Console.ReadLine();
  }
 }

3、运行后发现,if(a<0) a = -a; 这一句话出现瑕疵,a < 0 测试出错,并因此没有执行后续的  a = -a 语句。

当我们去掉 string str2 = a.ToString(); 这一句话之后,错误消失。


下面是对该问题的具体分析:

该问题实质上是由于JIT引擎翻译逻辑有瑕疵引起的。当注释掉string str2 = a.ToString() 之后,我们调试时打开反编译窗口以及寄存器窗口。在寄存器窗口上面点击右键,选上“标志”。此时我们可以看到:



请注意图片黄色箭头处,if(a < 0) 实际上被翻译成jns 0016。jns机器指令的含义是,如果不是负数则跳转,实际上判断的是“符号标志”,也就是途中红圈圈上的"PL",这个标志位(以及其他一些标志位)由上一个指令add产生(这是该指令的副作用)。 由于符号为负,并没有条件转移,因此能够执行下一句a = -a (也就是neg esi)。但是请注意图中另外一个寄存器标志OV,该标志表示“溢出”。很明显我们的代码是因为相加溢出才导致结果变为负数,所以该标志位被置位。


当我们再去掉string str2 = a.ToString(); 并运行之后,我们可以看到:



注意图中红圈处,原来的jns指令现在被改为jge指令。jge机器指令的含义是,如果大于等于则跳转,实际上判断的是“OV”、“PL”,相当于if ((OV ^ PL) == false) goto xxx。也就是说,多关注了一个OV标志。

很明显由于前面的溢出,造成OV标志置位,因此条件转移成立,结果没有执行后续的neg esi。所以我们从源代码的角度看,似乎此时变量a的值是非负数,这跟该数为负数的事实不符。

尽管从源代码的角度看,似乎没有任何与if(a < 0)有关系的改动,实际上由于我不清楚的理由,当该函数中使用到了a的引用,结果造成了if(a < 0)翻译成机器指令的不同,进而对一些副作用的反应不相同。

此例当中对a的引用是a.ToString(),实际上如果您使用AnotherFunction(ref a)替代这一句话,也会引起相同的问题。与此相反的减法操作的问题,原因和解释类似。


该问题的解决方法:
1、尽可能不要利用C#相加/减溢出后变成负数的副作用,例如用下列方式避免副作用:
  checked
  {
     a += 1234;
  }

2、如果您确信相加后溢出的副作用是必须的,那么请采取下列措施避免该错误:
  a += 1234;
  b = a;
  if (b < 0)
  {
    a = -a;
  }

注意,这个并非微软的官方解决方法,目前我并不清楚微软的KB里面是否有该问题的纪录。

p.s.: 有人告诉我,不一定非要有对a的引用才会引起问题,具体我也不研究了,反正对a进行引用一定出问题。(也许是对其他的引用产生了ldloca之类的il代码吧,没有研究过)

posted on 2005-03-09 15:22:00 by sumtec  评论(16) 阅读(5518)

 
2004年12月08日
好久没有上来写点什么东西了,也有一段时间没有到博客堂拨客园上面来采风了,今天一上来就看到这个文章:
数据类型的BUG还是???

里面罗列了一些问题,也许我能略解一二。比如说问题二:

dim ss as double

ss = 400*1000

在VB6中,报越界!!

ss = 400*100000没有异常

其实是这样的,VB6里面对于常数,如果没有带数据类型标志符(例如#)或者小数点,就认为是整数。而对于实际上是什么整数,则根据最小化原则,认为400和1000同为16位带符号整形。而数值计算结果的数据类型和操作数中表示范围最大的相一致(其实大部分语言都是这么定义的。ps:对于VB6及以下版本,似乎没有应用常量传播,要到计算里面才会出错),很明显400*1000的计算结果超出16位带符号整形的表示范围,报越界。而400*100000里面后者被认为是32位带符号数值,因此计算结果也是32位带符号数值,所以不会越界。

请注意,大部分语言的计算过程是有一个中间计算结果的,这个结果跟最终承载变量没有关系,而跟该语言的运算法则相关。在本例当中无论ss被定义为double、long还是别的数值类型,都必然会引发越界,这是由VB6里面的语言定义所引起的。而中间计算结果要经过一个转换过程才能够得到最终变量的数据类型,一般的基本数据类型之间都有“隐式”转换,有的是强制转换,这个一般由语言本身所定义。例如在VB6里面,几乎所有数值类型之间都能够自由的进行隐式转换,但是在C#里面,浮点数转换为整形数字的时候就必须要强制转换。

当然,从某种角度来讲,所有数值类型之间能够自由的隐式是VB6语言定义本身的缺陷,因为这样可能会引发很多“看不见”的问题。但是这实际上是VB6语言本身的定义,而不是设计人员无意识的或者不期望的结果,所以我宁愿称之为Fault也不愿意说是Bug。

可是大坏蛋却说.NET里面:
 double ss;
 int firstInt = 2147483646;
 int secondInt = 2;
 ss = firstInt + secondInt;
 Console.WriteLine(ss);

结果:ss = -2147483648

似乎对这个现象有点意见。首先,还是那个原因,计算是有中间结果的,中间结果的类型在这里仍然是int。其次,要追溯C语言本身的处理方式,在C语言里面不会对整形的上下界超界产生任何疑问,甚至不会报错。因为这个被认为是C语言的“特性”之一,C#“号称”继承了C/C++,那自然也会尽可能继承这些传统习惯,因此他就作为语法规范里面的一部分了,无可厚非。而事实上这也不是.NET Framework的功劳,而仅仅是C#的定义而已。因为在VB.NET里面,这会产生异常的。因为在C#的编译器对整数加减法使用的是不带检验的IL指令,而VB.NET则使用的是待检验的IL指令。比如C#使用的是add指令,而VB.NET则使用的是add.ovf指令。当然,这是在最普遍的代码编写方式,以及默认的语言参数下面而言的。

如果有什么疑问,请尝试下述代码:

 int ss;
 int firstInt = 2147483646;
 int secondInt = 2;
 ss = firstInt + secondInt;
 Console.WriteLine(ss);

呵呵,现在再请没有疑问的尝试下述代码:
int ss = int.MaxValue + 2;

回过头来我们再看看第一个问题:

dim ss as double

ss = 194268.02 – 194268

肉眼可以判断结果为0.02,而VB中计算的结果:ss = 0.199999999895226E-02

ss = 1.2 - 1 VB计算的结果为:0.2


要知道这个问题的答案,我们首先要看看这里的浮点数到底是什么浮点数。在.NET Framework里面(以及VB/VC等)遵循的是IEEE标准,那么为什么0.02不是0.02了呢?其实这个在IEEE里面可以找到一个快速的解答。那么为什么后面一个计算会是正确的呢?那其实是因为“精度”足够,使得你认为它就是0.2。事实上IEEE浮点数永远不可能精确等于2的n次幂相加所构成的数值(比如1.375 = 20 + 2-2 + 2-3,我“简称”这种数字为“可被2整除的数字”。),除非IEEE更改了他的标准。(关于IEEE浮点数的定义可以参考这里。)顺带给出double的1.2和0.2的十六进制编码:
1.2 = IEEE_double(3FF3 3333 3333 3333)
0.2 = IEEE_double(3FC9 9999 9999 999A)
而1.2-1的运算结果却是 IEEE_double(3FC9 9999 9999 9998),看到了吗?其实1.2 - 1并不等于0.2的。而事实上IEEE浮点运算即使是在“可被2整除”的数字之间进行,通常都会有误差的,这主要源于精度丢失。前面的1.2 - 1的误差并不属于这个范畴,这主要是由于操作数本身无法被精确表示而造成的(虽然也有精度丢失的原因)。可以说精度不丢失的情况是相当特殊的,比如说完全相等的两个数相加减,乘、除以2的整倍或者正负1以及0,和0或者“非数字”之间的计算,等等。

所以说这些问题千万不要往MS的头上扣,也不是MS所能够改变得了的。

posted on 2004-12-08 17:56:00 by sumtec  评论(13) 阅读(6685)

 
2004年11月18日
今天看到朋友wayfarer写的一篇文章,大概是关于protected的“保护性”问题的,看过之后内心有些想法想与大家分享,如果大家不嫌弃,敬请往下看。

拍脑壳所想之
 
  ——戏言面向对象


说到protected这个词,我不可避免的就会想到一个概念——面向对象。那么什么是面向对象呢?其实我个人认为面向对象这个概念是一直在发展变化的,到了今天,面向对象这个词也许让它叫做面向抽象更加贴切。在刚刚建立面向对象这个概念的时候,大概连创造者对于到底什么是面向对象都不是很清楚。要搞清楚面向对象(编程,或者设计)是什么,也许得看看过去的软件代码都是什么样的。



I.公元前
软件开发在最初的十几二十年里面,基本上就是面向过程的。面向过程的核心内容有两项,一个是控制流,另外一个就是数据流。在这一个时期里面,软件界最大的发展估计是数据结构与算法这两个“科目”了,这两者分别对应着两个“流”。在面向过程的软件代码里面,执行主体是过程或者函数。一个过程所代表的就是一个动作,动作的对象(这里还不是面向对象的对象)是一些数据,数据也许通过参数得到,也许通过全局变量得到,还有一些常量或者预定义值。如果我们仔细想一下,就会发现这是一个“动宾”结构的体系,比如说Basic里面比较著名的“Line (x1, y1) -(x2, y2)”,翻译成自然语言就是“画一条(x1,y1)到(x2,y2)的直线”。类似的例子还有很多,比如C语言里面的“printf("%s\r\n", "Hello world!");”。

可是主语在哪里?



II.创世纪
面向过程的代码里面并没有突出一个主语,很多时候这个主语也并非不存在,就像上面的例子里面,主语就是一个屏幕。可是如果我们需要往打印机里面画一条直线呢?(或者打印一个"Hello world"。)在面向过程的代码里面,我们就不得不自己写一个PrintLine的函数。(C语言往文件里面些东西就是fprintf。)如果我们要往远程设备上画一条直线,那还要写一个RemoteLine,如果……不需要我多说,您也会觉得麻烦。围绕着这样一个问题,人们就开始思考:是否能够把主语明确的给写出来?是否能够让我们少做一点重复性的工作?后来就有了面向对象这个东西,在面向对象是一个“主谓宾”结构的世界,绝大多数东西都有一个主语,比如我们所熟悉的“g.DrawLine(pen, pt1, pt2);”,由于我们有了“主语”,我们就可以让不同的东西,用相似的方法做相似的事情。如果光是把g换成h,仅仅解决了“在这个窗口画”与“在那个窗口画”的问题,如果我们希望他能够在其他类型的空间上画,我们还需要容许主语的类型可以不完全相同。但我们要解决的更多问题还是概念相同之处,例如打印机的g和屏幕的g都能够画线,因此有了诸如继承、封装等概念。这就是面向对象的一切了吗?



III.改革开放
随着面向对象概念的诞生,春风沐浴大地。正如上帝说要有光,于是有了光。上帝说要有毒蛇,于是有了毒蛇,上帝说要有苹果,于是有了苹果,结果亚当和夏娃吃了这个上帝创造出来的苹果受到了上帝的“惩罚”。真不明白,既然上帝不希望亚当和夏娃吃这个苹果,为什么还要创造这么一个东西?其实上帝创造这个苹果当然是不希望他们“吃”这个苹果,创造这个苹果实际上是为了产生浪漫的爱情以及其后千秋万代的动人故事。如果你把这个苹果仅仅看成是吃的,那么接下来你看到的就是痛苦的惩罚。如果你看到的是背后动人的故事,那么浪漫甜蜜等美好之辞就会充满你的大脑。

面向对象也一样,他的核心意义并不在于你把东西封装成什么样了,不在于有什么东西被继承出来了,最重要的是他容许我们用抽象的方式来构建一个软件。比如当我们写代码写到:

stream.Write(buff, 4, buff.Length - 4);
或者
hashbuff = hasher.ComputeHash(buff);

我们是否需要关心stream到底是什么,hasher用的又是什么算法呢?如果我们由始至终,在做相应的东西的操作都用相同的stream对象和hasher对象,任务是否都应当能够正确完成呢?应该是能够正确完成的,因为这正是我们的期待。如果让我们来设计某一个stream,是否应该从这个角度去考虑如何设计这一个类呢?如果我们定义这个stream变量,是否应该更抽象一点呢?考虑这么一个函数:
void DoSomething(FileStream stream, MD5CryptoServiceProvider hasher, byte[] buff) {...}

如果写成如下形式将会更加灵活,也更加符合面向对象(面向抽象)的真实含义:
void DoSomething(Stream stream, HashAlgorithm hasher, byte[] buff) {...}

换句话说,所有的封装、继承、接口等等,实际上是为了提供抽象能力而存在的。如果我们把protected当作保护“某些方法的存在”这个秘密的话,那就大错特错了。保护这些秘密严格说来应该是密码学的职责,而不是面向对象的职责。



IV.回顾历史
面向对象的核心是面向抽象,但我们看到,实际发展的过程并非如此。我们在过去有着太多错误的概念了,比如说这个面向对象技术的面向对象,就太容易让我们认为,这项技术的核心就是面向对象。于是很多时候我们写一个“面向对象”的程序充斥的过度的对象,泛滥的继承,以及不知道为什么的封装。并且不少开发者,包括我在内,都曾经认为所谓的面向对象就是把一些要素抽象成对象,进行封装,然后从某个基类派生出万物。好比有一个基类叫做物体,派生出活物与死物,活物派生出细菌病毒植物动物,动物里面有猴鸡狗猪和人,人里面有张三李四王二麻子(还有个娃)。
没错,面向对象当然得包括这些,但是这不是全部,更不是根本。根本就是在于我们写某些东西的时候,不需要关心具体的对象是什么,只需要知道至少它应该是一个什么。比如上一节当中的例子,DoSomething只需要知道stream是一个流,而hasher是一个哈希算法提供者就够了。至于具体提供的是什么样的流和哈希算法,则不应当是我们关心的,而是使用我们这段代码的用户所关心的。如此一来,我们就可以在设计这一段我们所关心的功能的时候,不需要考虑过多的、过于具体的、不断变化的问题。
仔细想想,我们是否真的已经明白了面向对象的核心所在呢?



V.封装保护的是什么
面向对象的封装并非保护你的秘密,而是防止被错误使用,是为了明确划分问题的界限。就“保护”这个词而言,更进一步的讲,它并非对使用该对象的用户(下面称为用户)做出使用某个成员的授权,而是对延展该类的设计人员(下面称为设计人员)做出延展问题领域的授权。现在让我们回过头来看一下wayfarer所写的例子:

class Base
    
{
        
protected void Print()
        
{
            Console.Write(
"This is protected method in Base Class!");
        }

    }


    
class Derived:Base
    
{
        
public new void Print()
        
{
            
base.Print();
        }

    }
class OtherClass
    
{
        
        [STAThread]
        
static void Main(string[] args)
        
{
            Derived d 
= new Derived();
            d.Print();
            Console.ReadLine();
        }

    }


这个例子确实是非常容易迷惑人的,曾经,我也被这样的问题所困扰。在解决这个困扰之前我们首先要弄清楚下面两个问题:
protected是什么?new又是什么?

protected 很好回答,他表明该成员容许在派生类当中被使用,但不允许使用本类对象的用户代码直接使用。实际上是对设计人员的有限度授权,和对用户的拒绝授权。
而new也并非难以回答,比如说:他是为了在没有override的情况下造成一种被重写了的假象。如果您真的这么认为,那就掉入了幻觉的漩涡当中去了。事实上new的作用并非一个trick,让你可以造成各种各样的假象,或者企图绕过某些使用与设计的授权。new的作用仅仅是为了解决一个命名冲突的问题,也就是说new所指定的成员实际上与基类的同名成员毫无干系,只是非常抱歉的跟他重名了只好声明此Print非彼Print。如果您真的企图用new来制造trick假象的话,终究是要撞掉你的门牙的。在我举出“撞掉你的门牙”的例子之前,请容许我首先给出一个正确使用new关键字的场景。

不知道各位有没有真正的研究过.NET Framework里面interface呢?如果研究过,对于下面的这个问题应当不是非常难以回答。

    public interface IFoo
    
{
        
bool Bubble();
    }


    
public class Boo
    
{
        
public void Bubble()
        
{
        }

    }


    
public class Foo : Boo, IFoo
    
{
        
public bool Bubble()
        
{
        }

    }

上面这个代码会在Foo的Bubble函数上面产生一个警告,但是仍然能够编译通过。为什么能够编译通过呢?这个问题留给读者自己琢磨了。解决这个警告的办法有两个:一个是显式实现接口IFoo;可是如果我不希望通过显式的方式来实现该接口,那么就只能够在Foo的Bubble函数前面添加一个new修饰符,告诉编译器我知道他们有冲突,但是我还是希望选择用这种方式来完成它们。这两个Bubble相同的名称给我们一种它们之间有什么联系的错觉,事实上 new bool Bubble() 与 bool new_Bubble() 的含义接近,和Boo里面的void Bubble可以看作毫无关系。如果你觉得有关系的话,那么下面的我将举出一个例子让你碰一鼻子灰。

    public class Boo
    
{
        
public void Bubble()
        
{
            Console.WriteLine(
"Boo sheet");
        }

    }


    
public class Foo : Boo
    
{
        
public new void Bubble()
        
{
            Console.WriteLine(
"Foo sheet");
        }

    }


    
class Program
    
{
        
static void Main(string[] args)
        
{
            Foo obj 
= new Foo();
            Test(obj);
            Console.ReadLine();
        }


        
static void Test(Boo obj)
        
{
            obj.Bubble();
        }

    }


你猜你会说Boo sheet呢,还是Foo sheet?为什么会这样也请自个儿思考一下。

wayfarer所举的那个例子,看起来确实容易造成困惑,或者会让大家觉得这里有一个暴露protected函数的bug,但事实上并非如此。还记不记得前面我说过了,protected是一个授权问题,而非保密问题,这一个说法应该能部分解决您的困惑。而上面的Boo sheet例子表明,实际上你并没有暴露那个protected函数,因为你仍然无法直接从一个指向Derived实例的Base变量上面寻求到使用Print函数的方法。如果您还记得我前面说过的面向抽象这个概念,也应该意识到,您写的Derived类只是对Base类的补充,而用户一般应该用Base变量来使用您的对象,而不是Derived变量,除非他认为他需要使用Derived提供而Base不提供的功能。如果说我不使用new关键字,而是把Derived的Print函数命名为PrintBase,那是否算是会引起“暴露”基类成员的Bug呢?

显然不是。此时如果用Base变量仍然无法访问Print,用Derived变量则仍然可以访问到PrintBase(并最终调用Print)。还记得派生是为了延展问题领域的边界吗?这里将Base的一个受保护函数暴露出来,就是延展了问题领域的边界。设计Base的人认为,Print的功能是对象的内部事务,而Derived的设计人员则认为Print功能应该是外部与内部之间的事务,此时是否仍叫Print已不重要了。(P.S.: 容许派生类使用,却不允许被暴露,这是根本不可能的事情,即使new关键字不存在也一样。而真正能够称之为“暴露”的是反射邦定/反射调用等。可惜我们还是不能够称之为Bug,因为这正是“反射”这个设计所期望的功能,而非不小心造成的有害副作用。)

说到这里,我想protected和new的问题应该已经讲完了。还有什么疑惑吗?


VI.回到未来
回过头来再说说面向抽象,以及面向接口。 前面已经提到了面向抽象了,不知道大家是否有更多的感想。抽象到了头是什么?当然不是什么都没有,不是虚空太极。抽象的本质是描述某个主语能够完成一组什么动作,这些动作构成了什么样的功能。如果我们从这样的一个角度去想,就会发现接口能够很好的完成这样的任务。比如说IList,它表达的是“一个列表”这样的抽象,这个抽象能够提供一组相应的动作,比如"object this[int index];" 能够取出或设置列表当中的第n项内容。只有拥有IList接口所定义的成员,才能够表明这个物体确实能够称之为“一个列表”。“服务”这个词也许能够更加深刻的表达上述的含义:
class ArrayList : IList {...}
这样的定义表明,ArrayList提供“列表”相关的服务。如果我们在定义变量和参数的时候更多的使用接口,而不是具体的类,那么我们的代码将拥有更大的自由度。这个时候我们关心的事某个对象是否能够提供我所需要的服务,而不关心他到底是什么。

在面向对象刚刚开始的时候,我们在这个方面走进了一个误区,就是用多继承来解决上面的这个需求。比如我们可以在C++里面看到Stream派生自IStream和OStream,这么做也能够解决问题,也许同样能够表达“服务”这个含义。但是我觉得这样做还是会引起许多不必要的麻烦,比如同名称冲突等。现代的理论甚至直接告诉我们继承他不是一个好东西,如果能够用引用来代替继承,那就不要继承,如DesignPattern里面的Decorate等。


真正未来的理论是什么,我不知道。但是至少我看到面向接口的设计思想比“面向对象”要更为先进,而目前真正能够这样思考的人远比知道如何封装继承的人要少得多。从这个角度讲,那也算是未来的技术,至少是未来需要普及的技术。(现在COM不就是这样一种思想吗?)

posted on 2004-11-18 12:27:00 by sumtec  评论(24) 阅读(8670)

 
2004年11月13日
前一段时间已经发现CF在载入资源的时候会怪怪的,但是这一段时间都不曾记起要对这个问题研究一下。最近又发现这个问题了,实在是恼火。俗话说择日不如撞日(粤语),唉,就今天啦。这个问题是在VS2k5里面调试的时候才发现的,以前在VS2k3里面一直都不知道有这么一个问题存在。事情是这样的:

有一次在VS2k5里面调试运行一个SmartPhone的程序的时候,发现突然在Output窗口里面显示了好几个First Chance Exception,仔细一看发现有两个FileNotFoundException,以及一大堆的UnathorizedException。而这些异常却没有引起中断,也就是说在程序的某一个部分被try...catch截获了。但问题是我的代码里面根本就没有任何的try...catch,不应该呀?后来仔细想想,又想到using也截获异常,所以一下子下不了定论。而我这个程序只有关于画图方面的代码,因此问题应该在画图相关的部分。我立刻想到有两种可能性,第一种是加载资源的时候引起了异常,另外一种可能是画图代码由一些可能引起异常、但是系统能够处理的参数。由于没有时间,开始的时候就没有仔细探索,反正能够正常使用。

而昨天呢,我稍微研究了一下,发现问题确实是出在加载资源的部分。当你的SmartDevice程序里面包含了图片资源的话,并且通过资源管理器的GetObject获取这项资源的话,就会在内部引发一系列的异常——第一次引发两个,第二次引发另外两个,后面的就每次引发一个异常。简单的跟踪发现,问题可能出在本地化方面的问题。似乎在GetObject的控制流当中的某一个环节,会检查你指定的“文化(CultureInfo,不知道中文术语是否这么讲)”,然后找到相应的资源文件载入。这个资源文件是干什么的我没有仔细看,这只是一个粗略的分析,估计是用来显示出错信息的。由于没有指定CultureInfo,或者指定CultureInfo的资源文件没有被安装上来,结果就引发了FileNotFoundException了。至于后面的UnaotherizedException,我估计就是一开始引发了FileNotFound之后,某些初始化步骤出现问题的副作用引起的。

这个问题也许值得花时间去了解清楚。大家都知道抛出一个Exception,无论代码有没有catch,时间开销都是相当大的。可以说如果你的程序有图片资源,其启动速度就会跟没有图片资源的相差非常大。大家可以试一下建立一个空白的程序,运行一下感受速度。然后在这个项目的Form上面添加3个PictureBox,设置它们的Image属性,分别换上不同的图片。再次运行的时候就会感到速度会有差异了(应该还好,不算很明显。)如果在VS2k5里面做这个实验就再好不过了,因为能够在Output窗口上面看到很明显的First chance exception项目的。这个CF如果跟桌面的.NET Frameword对比,真算不上是精品工程,越用就越多问题。无论如何,在正常使用的情况下抛出一个错误(虽然接住了),都是不正常的设计方式。

呵呵,不知道现在VS2k5的beta里面是否有中文的Rom了呢?如果没有的话,下一次来看我的解决方案,今天不在工作机器上面,很多东西没法写,怕不准确害死人。OK,今天就这些。

posted on 2004-11-13 15:59:00 by sumtec  评论(7) 阅读(4118)

 
2004年11月11日
不过从质量的角度讲,那就是另外一回事了。

   blogchina.com/     Go to http://www.blogchina.com/
www.blogchina.com

  

Average Traffic Rank: 516
Average Customer Review: 5 out of 5 stars Based on 3 reviews. Write a review.



  blogcn.com/     Go to http://www.blogcn.com/
www.blogcn.com

  

Average Traffic Rank: 843
Average Customer Review: 2.5 out of 5 stars Based on 2 reviews. Write a review.




   blogdriver.com/     Go to http://www.blogdriver.com/
www.blogdriver.com

  

Average Traffic Rank: 2,370



      Go to http://www.yourblog.org
www.yourblog.org

  

Average Traffic Rank: 2,436



  cnblogs.com/     Go to http://www.cnblogs.com/
www.cnblogs.com

  

Average Traffic Rank: 18,188



   blog.joycode.com/     Go to http://blog.joycode.com/
blog.joycode.com

  

Average Traffic Rank: 20,362



   seov.net/     Go to http://www.seov.net/
www.seov.net

  

Average Traffic Rank: 64,846

posted on 2004-11-11 18:19:00 by sumtec  评论(54) 阅读(4136)

 
最近逛新闻站点(非IT),有那么两件郁闷的事情不吐不快。
第一件事情是万恶的凤凰网,以前还算好好的,但是自从个把月之前全新改版之后,就出了一些让我难以忍受的恶心问题。首先是主页加载速度奇慢,然后是新闻页面加载更慢,经常是打开了之后两三分钟才会显示出来。如果连续打开多个新闻页面,甚至会出现莫名其妙的stack overflow at line 0的错误!稍微看了一下发现很多时候实际上整个html已经下载下来了,就是不知道他在干什么。也许里面include了另外一个什么页面或者脚本,但是出现stack overflow这样的错误实在是无法理喻。更加无法理喻的是,都这么长时间了都没有明显的改善,仿佛凤凰网的人从来就不检查一下自己的网页到底怎么样的。现在我经常是在打开一个新闻,等上两分钟,然后还是没有画面的话,只好按Alt-V、C来看源代码了。凤凰网就是凤凰卫视的官方网站:
http://www.phoenixtv.com/
(难道只是我这里的网络有毛病?)

同样让我异常怀疑到底是我这里的网络有毛病呢,还是网站有毛病的是下面这一幅截图:


我开始粗一看去,觉得没有什么,不就是Access Error。后来觉得不对,正规的说法应该是Access Denial吧?更加让我吃惊的是最后一行小字“dxxxxx fxxx out”,这个也太夸张了吧?这个是访问新浪的时候出现的错误,暂时不清楚是新浪修改了出错页面呢,还是IIS的隐藏错误呢,或者是我这里的网络提供商插入的数据呢,抑或是我们内部网关的问题?
但是无论如何这都不是我的错,我只想上上网,结果就……

谁也遇到过这样的问题吗?原因有人知晓吗?请恕我孤陋寡闻。

posted on 2004-11-11 15:44:00 by sumtec  评论(27) 阅读(7854)

 
2004年11月01日

1、读一文件2M大,与读144个共200K文件同速;
2、快排序远比删除要慢(仅供参考)。

posted on 2004-11-01 23:00:00 by sumtec  评论(9) 阅读(3393)

 
2004年10月31日
从一个气压计想到的事先声明,本文章与技术没有实际关系。不喜欢的跟我讲,我立马从首页撤下来!
不知道大家读过一个笑话没有:
有一个物理学的教授邀请了他的一位朋友——另外一名非常著名的物理学家,来帮忙评判他的一个学生的考卷,其中有一道题是这样的:你现在有一个气压计,如何用这个气压计来获得一栋楼的高度?他的学生是这么回答的:先度量气压计的高度以此作为单位高度,然后度量该楼一级楼梯台阶的高度,输出每一层楼有多少个台阶,一共有多少层楼,然后四者相乘即可得出高度……
这位教授实在觉得不知道怎么评判比较好,一方面觉得比较有创意,另外一方面跟气压方面的物理知识一点关系都没有。他的朋友建议让那个学生再来做一次这道题目,当然,是用完全不同的方法。如果他能够用比较技巧性的,通过复杂数学计算的物理学方法做出来就算他通过。
当那一个学生来到这两位面前,并了解了他们的意图之后,开始苦思冥想。当教授和他的朋友都以为他做不出来的时候,他说了一个令他们目瞪口呆的答案:
嗯,首先,用一根一米长的绳子绑住这个气压计,然后到楼顶上作单摆运动,计下周期T1,然后到楼底下做同样的实验,记录周期T2。根据单摆的公式,我们已知周期T,单摆摆长L,就可以求出重力加速度g。再通过万有引力定律,已知g,地球质量M,就可以求出离地心的半径R。T1算出R1,T2算出R2,R1-R2就是楼的高度。
看到两个人愣在那边,这个学生得意地说:我刚才想了那么久并不是想不出来,而是想出来太多的东西了,要仔细考虑一下那一个更符合你们的要求。当然,我知道你们想让我通过气压方面的知识来得到这个题目的答案,不过那样太没有意思了,谁都知道答案是什么样的。
教授就问了,那你还有什么样的答案呢?学生说:比如我可以用一根绳子绑着气压计然后从楼顶吊到楼底下,或者用气压计来做一个自由落体运动,再或者测出气压计的高度和它太阳下的影子高度,再测出大楼的投影长度。实在不行了,我可以把这个气压计送给楼底下看门的老头子,让他告诉我大楼的高度。

这是一个笑话,所以从这里想出来的大约都是胡诌,就随意胡诌一下吧:
1、根据气压方面的知识来计算大楼的高度,也许就是最为传统的软件工程方法,CMM等之类的东西。

2、如果真的以气压计作为单位测量台阶高度来计算大楼高度,也许跟手工作坊大生产比较象。

3、要是你一个人来做实验,比如用绳子吊着气压计到大楼底,那么也许会因为不小心碰到二楼阳台的晾衣杆,以为到大楼的了,结果数据出错。所以这个要两个人来做,一个在楼顶吊着,一个在楼底下看着。这么做的好处很多:除了可以防止上面那种人为失误之外,如果在楼顶上吊气压计的兄弟胳膊实在是酸了,可以让楼底下的哥儿们上来替换一下。当然,楼下的兄弟通过观察绳子的情况,也可以知道楼顶上的兄弟是否睡着了。看出来了吧,这就是结对编程的好处。

4、自由落体运动的方案看起来不错,如果一切都是比较接近理想状态的话。可是如果正好有一个往上吹或者往下吹的风呢?所以测试之前最好在大楼中间的地方来一个监测,看看到达正中点的时间是否正好是总时间的二分之根号二,如果不是的话可以断言测试失败,至少这个方案有问题。这个时候我们得想办法把风去掉(比如打电话给上帝),不过在我们真正遇到这个问题之前我们可以假设事情就是理想的。不过这么做最好有一个伙伴跟你一块来做,当然了,TDD最好是结对的。

5、用气压计收买看门老头确实是好办法,完全不需要动脑筋,我喜欢。其实这个我们更应该动这样的脑筋,其实每一个国家安全局以及间谍机构里面的人都是这么干的,而且通常很漂亮。什么?计算机界里面有没有?有,黑客啊!没有听说过后门吗?一般的后门都是小打小闹的,据传闻某国政府在某个操作系统里面可能安有后门,如果这是真的,那肯定是收买老头子方案的超大型翻版。所以不要相信任何关于绝对安全的鬼话,那根本是不现实的,除非全人类都没有任何欲望。

6、世界上还真有人用单摆的方法来得到大楼高度的吗?天啊!那多么烦人啊!如果让我来做这个实验我肯定会疯掉的!想一想为了让大楼顶端和大楼底端得到的T值的精确度足够,我们得测量多少个周期的单摆运动啊?估计我睡着了都不会得到满意的结果的。(呵呵!哈哈!)笑?谁在笑?还真别笑!我们以前就是这么干的,不要说直接写机器代码的那个黑暗时代了,就是C语言的时代我们也不见得好多少。那个时候所有人都在不停的重复的颠来倒去的翻来覆去的做一些实际上完全一样的工作,而且所有人做的东西几乎都是重复的,现在想想我宁愿去睡觉。我们就是这么走过来的,没有这些工作我们也没有如今的舒服。话又说回来了,人类确实是通过这个方法来得到有关重力加速度的,虽然这是几百年前的事情了。

7、用三角函数求大楼高度(就是那个测投影的方案),是一种以小见大的方式。当然了,这种方式需要评判答案的人有一定的想象力——如果气压计的高度和它影子的长度成一个比例的话,这栋大楼也应该一样。在你让对方接受这种方式计算出来的答案以前,我们必须先让对方想象一下上面所说的场景,并且接受这么一个其实无法直接证实的事情(就大楼和气压表两者而言)。我们做软件的时候也经常需要这样,首先弄一个原型出来让客户看看,如果客户接受的话就需要一个舌头能够打活结的人来说服客户:如果我们的文档一步一步按计划出来,那么最终产品就是你们想要的东西,跟你们看到的那个原型意思是一个样的。

8、如果你对于上面的一些方案感到可笑,是因为你觉得从实际角度看,每个地方都存在误差,甚至你觉得你无法通过这些实验来得到准确的大楼高度,那么也许你是一个程序员;
如果你对于上面的一些方案感到可笑,是因为你觉得过程方面完全被忽略了,比如缺乏每一步实验步骤的描述,那么也许你是一个设计师;
如果你对于上面的一些方案感到可笑,是因为你觉得这些方案本身的大方向就错了,比如所用到的物理定律有问题,那么也许你是一个架构师;
如果你对于上面的一些方案感到可笑,是因为你觉得这些方案根本没有考虑到一些最基本的问题,比如说他不赚钱,那么也许你是股东,搞不好就是董事长;
如果你对于上面的一些方案感到可笑,那么对不起,得分两种情况:
a、我就觉得该这样做,那个谁,过来给我把它做出来,那么也许你是CEO。
b、不知道你在说什么,那个谁,把这篇帖子给我删了,那么也许你是LHS(幽默感缺乏综合症)。

9、其实我最关心的是,有没有更加恶搞的解题方法? 不过必须和气压计有关,并且从方法上是不同的。比如说用气压计收买楼顶上的清洁工,那就没有意义了。
10、这篇文章昨天就写好了,没有Post出来,是因为受到情绪的影响。今天Post出来没有关系吧?

posted on 2004-10-31 23:35:00 by sumtec  评论(22) 阅读(4255)

 
2004年10月29日
最近一直在忙于性能优化方面的调试工作,所以一直在研究这方面的问题。其实那些研究都是不着边际的事情,但是作为自己的知识储备,也算是一件有意义的事情。目前的工作算是“OK”了,不过我自己却不是很满意。在整个过程当中发现了不少的问题,首先是XML的速度,后来是FileStream的问题,接下来是Thread的互相影响问题。天啊!我真想喊出来,才解决了数据读取的问题又出现了Thread的问题——Ce里面的背景线程一旦运行,就会严重阻塞前台的线程。有没有谁知道CE里面(包括.NET CF里面)的线程机制是什么样的?是轮换式还是抢占式?优先级有没有作用?.NET CF里面的线程简直就是弱的不堪设想,除了能够让他运行起来,加上调一个优先级,就没有更多的功能了。不能够强制中断,不能够Suspend/Resume,也读不出来他的运行状态……真是要命,如果没有人能够给我答案的话只好自己慢慢找了。还好,目前的性能问题还可以含糊过去,比如让背景线程推迟30分钟之后再运行,但愿这样JGTM不会察觉出来他的存在。

很久没有仔细去看看外面周围都发生了些什么了,今天下午真的要好好的看看一些很久没有看到的Blog了。比如Neil CowburnAlex Yakhnin的,发现里面又有好多新的东西了,真是很喜欢他们的Blog,虽然更新速度并不快,大师每篇都值得一看。不像我这个Blog,充满了不可阅读的或者不值得阅读的废话。哎,废话少说,还是来看看最近他们都说了些什么吧:

(所有内容未经我亲自验证,仅代表原作者观点)
Neil Cowburn说:
Ultra-Simple Application Logging
原来在.NET CF上面开发,如果想要Log一些东西是这么简单的。大概意思就是通过P/Invoke改变标准输入输出流的方向,然后就可以直接用Console里面的方法来输出字符串到一个特定的文件里面了,没错,就那么简单。

Pocket PC Magazine Best Software Awards 2004 -- WE WON!
opennetcf.org获得了PPC Mag 的2004年度最佳软件大奖。嗯,应该指的是里面的SmartDevice Framework吧?说句老实后,这个东西确实是一件神奇的东西,它能够快速的解决许多你认为非常头疼的事情(也许待会儿我要到那里面找找有没有增强型的Thread类),尽管里面有很多地方有Bug。Bug不是问题,因为这个东西完全免费和开源,只要你自己会调试,水平足够的人都可以自己去截取代码和修改。这个东西最大的问题是:太大了,也还不够大。一方面我希望他能够小巧一点,因为这么大的东西会影响加载速度和占用内存的空间,而我自己又想偷懒,不想老是自己截取代码来避免这个缺点。另外一方面我却希望他能够解决更多更多的问题,比如COM交互,或者DX之类的东西,哈哈,这个就真是勉为其难了。总结一下,SD Framework还是很好D……

Craig on .NET Compact Framework 2.0
Craig是谁?我不知道,他最近在一个内部培训的时候进行即席演讲,并录了下来。他说了一些他觉得.NET CF 2.0里面最让他喜欢的东西,不听的话你真是错过了很精彩的东西!比如说,.NET CF 2.0 里面将会有Generics,有Iterators,有Com Interop,有Sql CE 3.0,有DX,有SMS Queue,有直接操作Inbox的能力,有Data Encryption,有Location Based API,有Camera API,有……听得我是极度兴奋(还有性能改善忘了列出来了)。不过这个只是内部培训的时候录下来的,估计是为了训练演讲能力,提高培训能力的。(上一次有幸参加了MS给MVP做的类似培训,感觉棒极了。哈达不一定赞同,他水平太高了……)毕竟不是官方的东西,可信程度还是不高,不过也比我自己瞎吹要好多了。嗯,别的东西我不要,就要Generics、Sql ce.NET、DX,还有性能提升,就这些了,我还是不太贪心的。
这里还有另外一个录像,我还没有仔细看,大家也可以看看,是关于ASP.NET 2.0的(显著优点的)。
这个Post实际上是来自Carter Maslan的。

CFNunitBridge GotDotNet Workspace
嗯?CF也上NUnit了?嘿嘿……怀疑ing

Who would like to fly Virgin Galactic?
FT,还有可以飞到室女座星云的方法?Neil想,我可不想。那么大的星云(跟银河系一般大)要找个落脚点可不容易,目前人类在300光年的范围以内都尚未找到和地球环境相近的星球,更何况要在一个直径是万光年的星系里面去找,真是茫茫大海呀……

Meet the Visual Studio for Devices Team
关于Visual Studio for Devices Team里面的录像

Using POOM with .Net Compact Framework Whidbey Beta1
前面说了.NET CF 2.0 可以POOM,这里有一个例子可以去看看。

Show me your desktop!
看看人家多好,最小的也使17寸的液晶……羡慕有两个显示器,以后有钱了一定要买四个显示器,一个打开VS2kX玩玩,一个运行自己写出来的程序玩玩,一个摆着玩玩,还有一个摔着玩玩……

Catch-up Round-up
都是一些连接,值得一看。

Visual Studio For Devices Team Blog
VSD Team的Blog,如果你对如何改进VS有什么想法,可以给他们说说,也许会管用。


Alex Yakhnin 说:
Accessing Memory Mapped Files from CF
这个是关于在CF上面使用FileMapping技术的Post,值得研究。

Nota bene...
这个……这个……

Using cordbg for .NetCF
其实也是转自别人的Blog,好像Neil也转了。值得研究,这样我们也许有机会知道.NET CF里面到底搞了些什么小动作。

Great info on Managed App Startup and Retargeting
有点深度的内容,具体是什么还没有仔细看。好像说的是.NET CF里面EE启动Managed App的机制,不知道有没有看错。

XPathReader on .NetCF
在.NET CF上面XPath?上次仅仅是XML已经让我非常头疼了,还要XPath呢!不过文章里面说,有人提到现在的XML解析方法有问题(没有提到到底是DOM还是XmlReader),而值得庆幸的是Dare Obasanjo,一个MS的PM,发表了一个 XPathReader,据说除了提供了一些XPath的能力之外,还有效率方面的提高?该死的Alex说:
The next step?will be to test a comparison performance of this library vs XmlDocument in CF.
但是却没有写出来,难道非要我也研究一遍?待会儿给他email以下,如果效率有很大提高,那么这些天的工作算是白忙活了……(估计XML的解析效率很难提高的,毕竟是解析……不过话还是不要说死为好。)

Sending mail with attachments from .NetCF.
如果有人对于在.NET CF里面如何发送带有附件的邮件有困难的话,可以看看这个Post,也许对你有帮助。

OK,今天的大转载集合到此,还要看看Thread的问题呢……

posted on 2004-10-29 17:06:00 by sumtec  评论(16) 阅读(6257)

 

一个强制转换快还是ToString快,搞得我晕头转向的。以为OK了出结果了,却出现奇怪的情况,还好,总算是山穷水尽(复,李敖说的)疑无路,柳暗花明又一村。

一开始,我想大家都跟我猜想的一样,认为ToString比较慢,而且还有危险。结果一开始我的试验结果跟我的猜测比较吻合,ToString比强制转换要慢一个级别,不同的机子上面会有不同的结果,在10:1到6:1之间。但是后来有人说在Console下面不一样,问题就变得让我困惑起来了。事实上这个跟GDI,跟Form本身,跟DoEvents函数统统没有关系,因为我没有动用到无关的东西,也没有把非必要的部分给测量出来。更让我感到有趣的是,即使是在Console下面,只要自己写的类继承自Form,测试结果就会变慢。如果取消继承关系,两者的速度就会相差无几。认为Form的初始化会比较复杂,会动用Native资源,会在释放对象的时候有效率问题,这些都是错的,因为我很明显没有不断的进行构造和释放,也没有把这部分的时间计算在内。那么到底是什么问题呢?昨天没有时间进行试验,今天一大早起来就是为了进行测试。
其实我一开始就在汇编底下注意到那个ToString之后的调用,总觉得很奇怪:为什么不用强制转换之后的那一个赋值调用呢?(就是下面这个)
00000112  mov         esi,eax
00000114  push        esi 
00000115  mov         ecx,edi
00000117  mov         edx,995358h
0000011c  call        71E7021D


所以我从一开始就怀疑性能损失在这个地方,但是因为暂时没有办法调式到内部(MS的调试器……),所以我只能够进行一些猜想:是不是跟测试所在的实例有关系呢?
用一句代码来举一个例:
s = o.ToString();
本来觉得性能应该仅仅取决于调用了什么方法(ToString还是强制转换),以及所调用对象本身的类型(跟o有关)。比如考虑到ToString是虚函数,也许会有关系。但是后来看到这一系列奇怪的结果之后,不得不怀疑这个测试还跟this有关系。

首先我就猜测,是不是跟继承的深度有关系?如果是这个问题就太荒谬了,但是Form类和自己写的类之间的差别之一就是继承深度,还是有嫌疑的理由。为了验证这个问题,写了两个类:

public class TestBase
{
   
protected const int testRound = 100000000;
   
private string sTestBase;
   
public void Test();
}
public class First : TestBase
{
   
private string sFirst;
   
public void FirstTest();
}

构造相应的实例并本别调用Test和FirstTest,函数内部的代码大致如下:

object o = "Hello! this is a test!";
long dt;
int i;

dt 
= DateTime.Now.Ticks;
for (i = 0; i < testRound; i++)
{
    sFirst 
= (string) o;
}

dt 
= DateTime.Now.Ticks - dt;
Console.WriteLine(dt.ToString(
"N"));

dt 
= DateTime.Now.Ticks;
for (i = 0; i < testRound; i++)
{
    sFirst 
= o.ToString();
}

dt 
= DateTime.Now.Ticks - dt;
Console.WriteLine(dt.ToString(
"N"));


这里写成两个函数主要是避免虚函数可能带来的影响,尽管不太可能,还是不要节外生枝比较好。测试的结果表明几乎没有任何差别,我还是不死心,把继承深度从1层扩大到3层,测试结果还是一样,因此排除了继承深度带来的影响。
然后我只好怀疑是否因为对象内部实现了某些接口造成性能上面的差异,因为Form内部确是有实现不少的接口,尽管也挺荒谬的。于是我又写了一个类:

public class Second : First, ICloneable, IComparable, IFormattable, IServiceProvider, IDisposable, IConvertible, ICustomFormatter, IAsyncResult, IAppDomainSetup, IDbCommand, IFeatureSupport, IFileReaderService, IContainerControl, IDataAdapter, IButtonControl, ICommandExecutor, IDbDataParameter, IColumnMapping, IMessageFilter, ITableMapping, IWin32Window, IWindowTarget, ITableMappingCollection
{
    
private string sSecond;
    
public void SecondTest();
    
// 接口实现就不贴上来了
}

不要怪我狠心,我开始的时候添加了三个接口,观测到几十毫秒的差异,于是就一路添加上去。最后发现差别不会超过100毫秒,并且很可能是误差造成的。现在添加了二十多个接口都没有问题,那么到底问题在哪里呢?
再看就剩下Form的父类没有研究了,难道还真的是构造函数里面有些损耗性能的东西?那就太郁闷了,也非常荒谬。没办法,猜测不解决问题,还是来测试一下吧。看看怎么个测试法呢?想了想觉得还是从最上面的父类开始找问题,也就是说设计一个从MarshalByRefObject派生的类:

public class Fourth : MarshalByRefObject
{
    
private string sFourth;
    
public void FourthTest();
}

不测不知道,一测下一跳,问题就是由MarshalByRefObject引起的,试验结果跟前面的10:1非常吻合。后来为了挖掘更多的数据,我做了更多的相关测试,例如测试函数是实例上的,但是sFourth则是静态的结果会怎么样?如果函数是静态的,但是全局变量是实例上的呢?两者都是静态的呢?所以还写了更多的函数来测试:

public void TestStatic();
static public void StaticTest(Fourth fourth);
static public void StaticTestStatic(Fourth fourth);

这个测试我在.NET 1.1(DEBUG)、.NET 2.0(DEBUG/RELEASE)下面分别测试,测试结果数据如下:

说明 .NET 1.1 DEBUG .NET 2.0 DEBUG .NET 2.0 RELEASE
TestBase : object
string = (string) o
string = o.ToString()

First : TestBase
string = (string) o
string = o.ToString()

Second : First, Interfaces
string = (string) o
string = o.ToString()

Third : First
string = (string) o
string = o.ToString()

Fourth : MarshalByRef
string = (string) o
string = o.ToString()
string = MyClass.GetString()
string = this.GetString()
string = Fourth.GetString()

Fourth
static string sStatic;
public void TestStatic()
{
sStatic = this.someThing
}

Fourth
private string sFourth;
static public void StaticTest(obj)
{
sFourth = obj.someThing
}

Fourth
static string sStatic;
static public void StaticTestStatic()
{
sStatic = Fourth.SomeThing
}

---End of Test---
TestBase
10,625,000.00
11,250,000.00

First
10,468,750.00
11,093,750.00

Second
10,781,250.00
10,937,500.00

Third
10,468,750.00
11,093,750.00

Fourth
10,625,000.00
109,062,500.00
110,937,500.00
120,781,250.00
109,062,500.00

Fourth
10,468,750.00
11,093,750.00
11,093,750.00
24,843,750.00
10,156,250.00

Fourth
106,718,750.00
108,125,000.00
109,687,500.00
119,218,750.00
106,875,000.00

Fourth
10,312,500.00
11,093,750.00
11,093,750.00
24,843,750.00
10,156,250.00

DONE
TestBase
10,781,250.00
15,312,500.00

First
10,312,500.00
15,781,250.00

Second
11,250,000.00
15,468,750.00

Third
10,312,500.00
14,531,250.00

Fourth
9,687,500.00
125,625,000.00
120,312,500.00
131,093,750.00
118,281,250.00

Fourth
8,593,750.00
12,656,250.00
12,968,750.00
27,343,750.00
12,187,500.00

Fourth
117,343,750.00
118,906,250.00
119,687,500.00
131,406,250.00
121,562,500.00

Fourth
8,593,750.00
12,656,250.00
12,968,750.00
28,125,000.00
12,031,250.00

DONE
TestBase
8,125,000.00
12,968,750.00

First
7,656,250.00
12,968,750.00

Second
8,281,250.00
15,468,750.00

Third
7,187,500.00
13,437,500.00

Fourth
7,812,500.00
12,812,500.00
5,156,250.00
25,625,000.00
6,093,750.00

Fourth
12,812,500.00
15,156,250.00
6,093,750.00
24,687,500.00
6,250,000.00

Fourth
113,750,000.00
115,156,250.00
109,062,500.00
126,718,750.00
109,062,500.00

Fourth
6,875,000.00
12,031,250.00
5,156,250.00
22,968,750.00
5,156,250.00

DONE

(先给大家说一下,大家可以注意一下.NET 2.0 RELEASE的数据,相比较起来是一个比较奇特的数据。)

我一开始万万没有想到,a = b 这样的简单赋值语句还会跟a这个变量声明的位置有关系(都是全局变量的情况下),或者说跟a所在的类有关系。以为不就是把b赋给a嘛,汇编里面一句mov或者lea就搞定了,如何获得b才是影响性能的关键。结果发现.NET下面并不是这么简单的事情,至少会有MarshalByRefObject这个特例。以前也听说过MarshalByRefObject会非常的影响性能,但是文章里面没有提及,自己也没有考究过。心里面想,这个MarshalByRefObject跟我有什么关系?现在想想好象还是有一点点关系的,比如你在Form上面有一个全局变量(包括你放在窗口上面的所有控件都是本地变量),你对他进行赋值就会有性能损耗。然而值得庆幸的是,我们很多时候都不会对一个全局变量不停的进行复制操作,比如我们总不可能对一个Button myButton这个全局变量循环赋值100000000次吧?顶多我们可能对Hashtable ht这个全局变量不停的添加内容,还好Hashtable等集合类型都不是从MarshalByRefObject里面继承出来的,如果是那样的话真是要疯掉了……

这个发现也许有用,也许没有用,至少在VS2k5里面已经没有什么太大意义了。但是在.NET CF里面就不好说了,虽然我从直观上认为不可能做出太多优化,并且那里面的MarshalByRefObject里面没有任何内容,不知道.NET CF还会不会对其进行特殊对待呢?

posted on 2004-10-29 13:48:00 by sumtec  评论(20) 阅读(6633)

 
2004年10月28日
刚刚发完上一篇Post,在cnblogs上面msolap就提出了Console下面结果相反的情况,继而有人用实验证实了这一结果。我立刻稍微实验了一下,发现果然如此,更进一步的发现了一些不可思议的事情。

事实上我在另一台机器上面做了一个完全相同的Console实验(一台普通的P4 1.8G 768MB Ram),在VS2k3里面的Debug模式就得到了反转的结果——ToString比强制转换要快,虽然快的并不多,大概也就几十毫秒的时间。经过相当长的时间的困惑之后(真XX的不可思议),终于发现了问题的关键所在(可能而已,不敢确定,仍需实验)。

在这里给所有愿意自己亲手做实验的朋友们一个提示:
首先不要用static的方式,请在一个实例里面进行实验,例如:
private class Tester
{
            
private string testString;
            
private const int testCount = 100000000;
            
public void TestConsole()
            
{
                  
//  test it!
            }

}

先别急,光是这样的话,还是跟原来一样的,我们接着改造:请把Tester改造成从Form继承!没错,就这么做,看看结果如何?

做完了实验请你说说你有什么想法?我是有挺多的想法的,不过现在没有时间,改天再聊。

posted on 2004-10-28 18:24:00 by sumtec  评论(16) 阅读(4752)

 
首先先说一句不好意思,昨天的Post里面忘了给大家介绍一个基本的前提:那个object o 里面的数据我可以确保它是string类型,也只可能是string类型。这一个研究并不是完全没有意义的,虽然在我实验之前确实也认为应该是强制转换的速度快,但是没有实际的比较问题还是比较难以说明的。也许有人就会问了,你凭什么认为object o里面的就是string呢?不好意思,这是我限定的条件,并且是有实际被应用情形的:
考虑现在我有一个private的ArrayList用于保存用户在TextBox里面输入的字符串,很明显TextBox.Text里面就是返回一个string类型的对象,并且也不可能是null(或者说我通过在初始化的时候给它赋一个string.Empty来避免出现null的情况)。现在我需要将这些保存在那个ArrayList里面的字符串通过BinaryWriter保存到一个自定义格式的二进制文件里面,当然也可能有其他的计算需要我把ArrayList里面的字符串数据进行计算或者处理,这里为了简便随便说了一个也许不是很完整也不是很合理的的例子,但是我相信大家应该明白我的意思了。Hmm,也许还需要另外一个限定,就是没有Generic的支持,比如说这个东西需要在.NET Compact Framework里面实现,而.NET CF即使是到了2.0的版本也不会有Generic的支持。

但是上一次Ninputer说了:
假如o里面原来就是String类型,那么循环100000000的结果是
(string)o 为610ms
o.ToString() 为670ms
这种差距几乎没有意义。

假如o里面是除string之外的其他类型,那么o根本不能直接cast成String。


而同时也有人说,强制转换比ToString要快。那么实际情况是如何呢?请大家先来看看我的代码:
        private string testString;
        
private const int testRound = 100000000;
        
private void button1_Click(object sender, System.EventArgs e)
        
{
            
object o = "Hello! this is a test!";
            
int i;
            
long dt;

            dt 
= DateTime.Now.Ticks;
            
for (i = 0; i < testRound; i++)
            
{
                testString 
= (string)o;
            }

            dt 
= DateTime.Now.Ticks - dt;
            textBox1.Text 
= dt.ToString("#,###,###,###");
            Application.DoEvents();

            dt 
= DateTime.Now.Ticks;
            
for (i = 0; i < testRound; i++)
            
{
                testString 
= o.ToString();
            }

            dt 
= DateTime.Now.Ticks - dt;
            textBox2.Text 
= dt.ToString("#,###,###,###");
            Application.DoEvents();
        }


这个实验我分别在VS2k3和VS2k5里面实验过,并且分别用Debug和Release两种模式去测试,测试结果如下:
DEBUG: (强制转换:ToString)
@2k3
11,406,250 : 123,125,000

@2k5
10,468,750 : 123,906,250


RELEASE:
@2k3
12,500,000 : 118,593,750

@2k5
7,031,250  : 13,906,250


大家首先会发现如下结论:
1、强制转换的速度在0.7秒到1.3秒之间,而ToString则基本上处于10秒以上的级别(有特例)。所以我极度怀疑Ninputer的测试结果是错误的,因为他的两个结果都是在0.6秒左右,当然还有一个特例跟Ninputer的数据相对接近,但是请继续看下面的结论。

2、这里有一个唯一的特例,就是VS2k5的Release模式,大家可以看到ToString的速度从10秒级别急剧减少为1秒级别,这个就是我题目所说的意外惊喜。但是即使是这样,ToString仍然比强制转换慢了50%,同时该数据还是P4HT 2.8GHz 1G Ram的机器上面测得,如果要ToString达到Ninputer所说的数据,我估计得要P4HT 4GHz以上的CPU,或者P4HT双CPU才有机会。还有,Ninputer的数据仅仅相差10%左右,这个跟我的实测数据完全不吻合。因此我估计Ninputer的测试很可能是因为代码不合理,被JIT短路了(优化了),如果将转换结果保存在全局变量里面,也许就不是这个结果了。
      关于为什么VS2k5的ToString在Release模式下面会有这么大的性能提升,我想还需要另外做实验去证明,现在还不好胡乱发言(虽然已经有自己的想法了)。

3、大家如果更加仔细的去看强制转换的数据,就会发现VS2k5无论在哪一种情况下面,都会比VS2k3的数据要快那么一点点,大约10%到30%不等。关于这个我可以马上给大家一个解释,这个实际上是.NET Framework 1.x和2.0之间的问题,但是这需要观察JIT之后的Native Asm,难度有点高。请看片断:

.NET Framework 1.1:
                testString = (string)o;
00000063  mov         edx,dword ptr [ebp-0Ch]
00000066  mov         ecx,79BF9AF8h
0000006b  call        71E608BC
00000070  lea         edx,[ebx+000000ECh]
00000076  call        7204C590

.NET Framework 2.0:
                testString = (string)o;
0000007c  test        ebx,ebx
0000007e  je          0000009A
00000080  cmp         dword ptr [ebx],788ED668h
00000086  jne         0000008C
00000088  mov         eax,ebx
0000008a  jmp         00000098
0000008c  mov         edx,ebx
0000008e  mov         ecx,788ED668h
00000093  call        75621D76

00000098  jmp         0000009C
0000009a  mov         eax,ebx
0000009c  mov         esi,dword ptr [ebp-40h]
0000009f  lea         edx,[esi+00000104h]
000000a5  call        7561BBB0


在这里大家可以清楚地看到,.NET Framework 1.1 仅仅是简单的将强制转换交给一个函数来处理(红色部分),但是2.0则首先进行了一些简单的处理,例如先判断是否是null(test ebx, ebx),以及判断o是否就是恰好string类型而不是string的子类型(cmp dword ptr [ebx], 788ED668h)。当然了,string是一个sealed的类型所以明显不可能有子类型,如果我要求强制转换成Form呢?如果这两项都失败了,才会和1.0里面一样调用一个函数来判断。所调用的函数实际上是将ebx所引用的对象里面的类型向上查找其父类型,看看有没有string类型(788ED668h),可以预见这是一个相对较为耗时的操作。而在我们实际的应用里面,如果进行强制转换通常都是直接转化成他的实际类型的,因此这一小段代码能够较为明显的提高系统的性能。不过这里也可以看到,优化过程还是做的不够,应该将86到8a这三句简化成一句je 0000009A。

我们再来看看ToString的情况。由于ToString是一个virtual的函数,被string类型override了,因此必然需要查v-table来进行正确调用。通过查看.NET Framework 1.1 所JIT出来的代码,可以清楚地看到这一点:

                testString = o.ToString();
00000108  mov         edi,ebx
0000010a  mov         ecx,dword ptr [ebp-0Ch]
0000010d  mov         eax,dword ptr [ecx]
0000010f  call        dword ptr [eax+28h]
00000112  mov         esi,eax
00000114  push        esi 
00000115  mov         ecx,edi
00000117  mov         edx,995358h
0000011c  call        71E7021D



这里ebx指向object o的类型描述,ebp - 0Ch指向object o对象本身(其实这里的负偏移12个字节就是类型描述,两者实际上是完全紧挨着的),其第一个dword就是string类型本身的数据(不是实例的数据)。我并不是非常清楚里面的一些实现,通过观察,估计可能里面包括了本类型的一些描述,父类型和接口的描述,里面偏移0x28应该就是vtable的实质内容(估计),其第一个就是指向ToString实际函数地址的指针。后面一个调用估计应该是一个赋值调用,和ToString调用可能没有太大的关系。为了证实我的想法,我甚至跟踪到第一个调用内部,发现确实是到了string.ToString()里面(大家可以看Disassembly页的Address下拉框,里面现实的是System.String.ToString),其代码如下:
00000000  push        esi 
00000001  mov         esi,ecx
00000003  mov         eax,esi
00000005  pop         esi 
00000006  ret
 

分析到这里,我仍然不能够确定为什么ToString比强制转换性能相差10倍的原因。如果非要我估计一下,那么我会这么比较:
强制转换一共经历了:两个“比较+条件转移”,一个寄存器间赋值,一个索引器加偏移到寄存器赋值,一个普通调用(内部经历过程已经在前面分解了),一个赋值调用(内部无法分析)。
ToString一共经历了:5个寄存器间赋值,一个常数到寄存器赋值,两个间接(加偏移)地址到寄存器赋值,一个寄存器压栈,一个寄存器加偏移量间接地址调用(内部已经在前面分解了),以及一个赋值调用(内部无法分析)。

除去无法分析的部分,ToString比强制转换多出将近一倍的汇编指令,并且还包括比较耗时的间接地址赋值调用指令。光是这个差别,估计就应该有至少150%的性能损失。但是现在性能相差接近10倍,难道是仅仅因为那几句间接寻址的指令造成的?那就有点不可思议了。其实我有点怀疑ToString后面第二个调用内部有一些损失性能的指令(也许是多余的指令),如果我们看看.NET 2.0 Release版本的数据,也许是应该这么解释吧?有关这个问题需要做更多的研究……

嗯,今天的Post的正式内容到此结束。PS一下:
1、twodays在cnblogs里面的Post里面回复说:
“o.ToString() 实际上是调用了从object继承出来的ToString()虚函数,这一个虚函数在string类型里面被重写,代码如下:”

不对,因为你的o这会儿本来类型就是object,而不是string,所以他调用的实际上应该是object的ToString方法里面的内容,经过Reflector查看,里面的内容是:
return this.GetType().FullName;


这里我可以明确地告诉你,你错了(包括后面表示赞同的“小新0574”),至于为什么我会另外在一篇Post里面给出解释,因为这两个问题的难度级别相差太大了,在这里写也许很多朋友已经看得迷迷糊糊的了,继续看下去效果不好。

2、我记得有人在上一个Post里面问过as操作性能如何,我也做过试验了,性能没有差别,至少用肉眼观察数据结果是看不出来的。也许在Disassembly层会有细微差别,但是我没有兴趣再研究下去了。

3、有人说自己写的类里面ToString()可能会抛出Exception的问题,其实我是这么认为的。如果你在代码里面抛出Exception的话,那应该是一件相当遗憾的事情。因为ToString()没有任何参数,不可能因为参数问题引起异常。同时ToString函数的含义是,安全的返回关于该实例的信息,因此从感情上来说就不应该出现Exception的情况。很多时候我们就是利用ToString的一些特性来完成一些简单的事情,例如在ListBox里面加入一个不是String的对象,但显示的是用户能够理解的信息,这样用户方便选择,我们则方便编写代码。如果你的类会在ToString里面抛出异常,那么整个程序就会因此而不稳定。总之我认为在ToString() 里面抛出异常并不是这个函数存在的本意。

posted on 2004-10-28 14:32:00 by sumtec  评论(18) 阅读(3868)

 
2004年10月27日

假设我有一个object类型的变量o,需要转化成string类型的变量s。那么请问下列方式当中的那一个方案更加有效率:

1、

= (string) o;

2、
= o.ToString();

相关提示:
1、(string) 强制转换对应一句IL语句castclass,在IL中大概表现为:
      L_0019: ldloc.0
      L_001a: castclass string
      L_001f: stfld string WindowsApplication1.Form1::testString

这样的形势,注意强制转换只增加L_001a这一句IL语句。

2、o.ToString() 实际上是调用了从object继承出来的ToString()虚函数,这一个虚函数在string类型里面被重写,代码如下:
public override string ToString()
{
      return this;
}
如果效率有差别,差别是多大呢?答案在于实践当中。

posted on 2004-10-27 16:37:00 by sumtec  评论(12) 阅读(5321)

 

今天在做一个实验,突然发现一个不寻常的地方:VS2k5 C# 在编译for循环的时候,IL代码跟以前的方式不一样:

      L_0029: ldloc.1
      L_002a: ldc.i4 100000000
      L_002f: clt
      L_0031: stloc.s flag1
      L_0033: ldloc.s flag1
      L_0035: brtrue.s L_0018

这个for循环的C#语句大致为
for (num1 = 0; num1 < 100000000; num1++)

上面IL里面所显示的正是比较num1是否小于100000000的片断,如果在VS2k3里面,则应该编译成:
      L_0029: ldloc.1
      L_002a: ldc.i4 100000000
      L_002f: blt.s L_0018

仔细再研究一下,发现这个变化只在Debug模式里面出现,也就是说这个变化并不是为了效率考虑的。但是原来的方式也能够比较容易的进行调试跟踪,实在搞不清楚为什么要用clt的方式。更加奇怪的是,在clt比较得出结果之后,还要弦弹出来保存到一个flag1的本地变量里面,然后再重新压栈。想了想,可能还是为了“更”方便的进行调适吧:比如说在IL级别进行调试的时候,可能看堆栈是不方便的,同时看两个变量的关系也是让人眼花的(n位数字比较,光是对齐数字就头痛了)。通过这样的IL变化,我们可以简单的看看flag1到底是什么结果。如果发现不对了,还可以在跳转之前把当前执行语句移到其他地方,或者修改转移条件,甚至不需要修改循环变量。这些在以前估计是不可能的,因为blt直接就判断并跳走了,就算可以修改转移条件,也必然影响循环变量i。如果考虑的更深入一点,我们可以简单的修改一条IL指令,使得flag恒为true或者false,强制控制流按照我们希望的方式去走,但是又不影响所有源代码里面的变量,这样子也许能够轻易的进行全覆盖的单元测试。不过这么强的能力目前好像还没有看到C#里面有相应的操作项,所以一切都只是猜测。真正的原因也许需要查找更多的资料,或者问问C#项目组的人员才知道了。

顺便说一下,最新版本的Reflector好像有点奇怪,在同一个函数里面IL结构完全一样的两个循环,前面一个会被翻译成while,后面一个会被翻译成for,也不知道是从什么时候开始的。因为这个是在看VS2k5 C#的Release模式编译出来的程序发现的,我一开始还以为这是VS2k5的优化成果呢……

posted on 2004-10-27 16:16:00 by sumtec  评论(7) 阅读(3062)

 
【第1页/共5页,61条】
首页
前页
1

Powered by: Joycode.MVC引擎 0.5.1.8