该帖已经被评为良好帖
|
|
---|---|
作者 | 正文 |
发表时间:2007-07-20
Lich_Ray 写道 我的提醒白提醒了。
引用 还有,让prototype的版本的构造函数中有一次引用赋值,否则就成了只创建对象而没有构造函数执行的版本了。
因为你的第二个版本构造函数执行了100000次,LS!而你的第一个 function Darkness() { } 构造函数为空,在被 new 的时候无须执行!只要构造函数中有需要执行且不会被优化掉的语句,时间就会大幅增长。 我测试你的代码,前者3s,后者9s;前者构造函数中有简单引用赋值语句时5s。然后我注意到你的代码中有许多多余的空格,这会大大增加字符串比较的负担(因为它的算法时间复杂度是绝对的theta(n))。去掉之后变成: this.turnOn=function(){alert("bingo")} 时间竟然下降到5s。我觉得这不符合逻辑。排除可能存在的缓存问题后再测,6s。看来,后者花在检测上的时间才是主要矛盾。 你的这个代码最多能证明,在“超大规模”创建对象时最好还是让构造函数少做点事,快一点;但是,函数在内存中确实是只有一个。 PS: 至于你测的第二段代码要一分半钟,我估计是这个原因:首先,在浏览器中一次跑90秒是不可能的,它会跳出问是否继续执行的对话框,此时JS代码停止运行,但系统时间仍在继续。联想到你说第一个代码段耗时5秒,据此推测你的机器性能够戗。如果是浏览器IE,其内存管理问题很大,直接崩溃都有可能;如果是Firefox,在Geoko框架下创建对话框的耗时本来就大,再加上Java和JavaScript的垃圾收集器同时运行(关于Java垃圾收集器在极端情况下的性能问题参见《Effective Java》),会因为线程跳转过缓而加剧时间消耗。也就是说,因为一点机器性能不足而导致代码平白无故多跑很久。你觉得我的解释有道理吗? 如我前面所述,你几乎所有解释都是站不住脚的。 1. 目前的主要js引擎都不存在取消空函数调用的优化。 2. 不存在所谓字符串比较。 3. Browser里js执行的每次时间有较大波动(如相差50%)是很正常的,除非累积大量数据,你不能从中得出任何结论。 4. 这里根本不存在任何Java代码。况且Java的垃圾回收再烂,也不会出现你所说的这种情况。 你唯一猜对的一点,是执行那么久确实是与js的垃圾回收有关。因为jscript 5.7之前的版本,貌似在每若干次new之前都会调用GarbageCollection,从而导致new的性能随对象数量的增多而指数下降。 |
|
返回顶楼 | |
发表时间:2007-07-20
首先,这一个帖子中,Lich_Ray按照ECMA规范来推导,这个尝试是好的,但是ECMAScript的规范是出了名的佶屈聱牙,所以不幸的,Lich_Ray同志也未幸免。。。
Lich_Ray 写道 看来我就此开个 JavaScript 专栏一心和你们讨论这些问题算了。这帖子居然还有一群人跟在后面打“良好”“精华”,无语了…
火是要发的,问题也是要解决的。下面讲正经的。不仅仅讲给 keshin 一个人听,大家都来看下吧。我们开始从 === 运算符谈起。 请翻到 ECMA-262 e4 的 11.9.4 节,The Strict Equals Operator (===)。这里给出了 === 运算符的语法产生式和展开(注意,只是部分求值)步骤: EqualityExpression === RelationalExpression is evaluated as follows:
它指出,对两个产生式的求值结果调用规范中的算法 GetValue(V)。那么翻到这一节,8.7.1 GetValue(V)。
根据第一句,判断 Type(V) 是不是引用。现在要分情况讨论一下。先解决你上一帖提出的问题。在你给出的构造函数中,需要执行的是这样一句: this.turnOn = test; 那么,检查 Type(this.turnOn),是到一个 Object Type 的函数 test的引用。于是调用 GetBase(V) 获取到 trunOn 引用的基对象 this,此描述用函数的定义在 8.7 The Reference Type 中:
对象 this 不是 null,跳转第4步调用 8.6.2.1 节中 [[Get]](P) 算法,传递的属性名是 turnOn。根据算法
取到该属性的对应值,即内部记录的函数 test。现在我们知道了,是两个 test 在作比较。(友情提醒:请继续往下看。)那么返回 11.9.4 节。 现在只有 Step(5): Result(4) === Result(2). 是最重要的。旁边的(See below.)指出 === 的算法在下文,11.9.6 节 The Strict Equality Comparison Algorithm。在这一节我们看到了具体的算法。前面12步全是废话,看第13步: [list=13] 以上都ok。 实际上看到这里,你应该可以看出两个turnOn根本就指向的是同一个对象,即函数test。所以当然是相等的。不等才怪! 可惜的是,Lich同志不知道是没有细看源代码,还是实在按捺不住解说那个复杂的joined function object的冲动,就直接奔向这“只剩这一种可能了”。 Lich_Ray 写道 很明显,如果两个函数对象(只剩这一种可能了)可以被 joined,它们就相等。那么根据提示,翻到 13.1.2 Joined Objects。 下面是函数对象可被 joined 的条件:
这两句话罗嗦死了,其实就四个字:“同生同死”——要求可被 joined 的对象们的属性要产生都产生,要删除都删除。现在以此条件看 test 函数,符合以上条件,可被 joined,于是 === 运算符返回 true。 注意,你所摘录的只是Joined Objects的行为,而不是Joined的条件!所以你的逻辑首先有问题,就是因果颠倒了。其次,如前所说,这里根本没有两个test,只有一个test,然后有若干个对test的引用。 Lich_Ray 写道 解决下一个问题,你曾经写过的返回 false 的代码: function Light() { this.turnOn = function () { alert("bingo"); } } var lightA = new Light(); var lightB = new Light(); alert(lightA.turnOn === lightB.turnOn); 前面步骤相同不用看了,只看最后一个步骤, function () { alert("bingo"); } 函数们能否被 joined?当然不能,用肚脐眼都能看地很清楚,修改 lightA.turnOn 上的属性不会影响 lightB.turnOn。 这里是你本篇的最大错误。“修改lightA.turnOn的属性不会影响lightB.turnOn“,这是因为它们是两个不同的对象,因而也不可能是被join的,但是这是结果,而不是原因!而且就算反推回去,也是不成立的。因为join只是可选的,并非强制的。 事实上,规范给出了一个可以join的例子如下(大意): function A() { function B(x) { return x*x } return B } var a1 = A(); var a2 = A(); 规范说:a1和a2是equate的,可以被join起来,并且因为他们的[[scope]]也没有可以观察到的差异,所以甚至可以指向same object,而不是两个被join起来的object。 首先我要指出,规范本身也不是完美的。就我本人来看,让a1和a2指向同一个对象,是很奇怪的一件事情。而且这个例子中,[[scope]]是没有差异的,更明确的说function B是不依赖A的。如果按照这个标准看, function () { alert("bingo"); }当然也是可以被指向同一个对象的!这实际上意味着lightA.turnOn和lightB.turnOn可以是同一个对象,因此你修改lightA.turnOn的属性,也就等于修改lightB.turnOn的属性。 这很奇怪,但是你不能因为奇怪就反推出他们不能join。BTW,你没有特异功能,所以你还是不要用肚脐眼来看代码。 进一步,在B依赖A的情况下,B也是可以join的。因为B依赖A,不过就是多个B的[[scope]]不同,而这是允许的(见13.1.2 Joined Object的NOTE部分)。 所以如果严格根据规范,implmentation选择join或者不选择join,实际上会导致差异,而且是语义上的差异。所以我认为这是规范的一个错误!! 当然,要确认这一点,我是不够权威的,我打算有空的时候,写个信求教一下BE。 事实上,我所知的所有实现,都没有像规范所描述的join行为。也就是规范所举的那个a1和a2,在现实我所实验过的任何一个引擎里,都是不==的。 Lich_Ray 写道 问题解决完了,下面解释我提到过的函数相等性测试。第 13.1.1 节,Equated Grammar Productions 中的内容已经讲过,没看见拉倒。下面翻到课本: 这一节的算法中,用到函数相等性测试的只有 Step1
这一段话的意思很明显:如果函数相等性测试可用,就配合括号中的句子(If there is more than one object E satisfying these criteria, choose one at the implementation's discretion.)做优化;如果不可用,Step 1 忽略。 这里有一个隐含的内容:是否优化对语言语义无影响。这涉及编译原理的体系,即:优化措施属于实现层,实现层不可影响由语言规范所定义的语义层,即语言的外在表现。说白了,就是“规范是上帝,实现只能是上帝的羊羔,随你怎么折腾,不可僭越”,否则就是实现失败。所以我看到 keshin 这个第一帖中的代码时,想都不想就知道是语义的结果,跟实现无关。 如前所述,规范本身可能存在缺陷,导致语义可变。所以所有的实现都选择不join。 Lich_Ray 写道 以 Rhino 解释器(也就是 Firefox 的解释器啦)为例,一个函数对象(JavaScript 中函数没有对应原始类型,就是(is a) Object)在解释器中由两部分构成:一是可执行的继承 Scriptable,Callable 接口的 Java 类 BaseFunction 对象,外部 wrap 是 FunctionObject 类的对象。相等性测试通过的函数们,都是堆上的一个 BaseFunction,被存储为一个 FunctionNode,作为 JavaScript 函数执行时只调用一段代码块;但执行 === 比较时,只比较其在 JavaScript 中的外在表现,即 FunctionObject 是否相等,结果就是这样有些“奇怪”的表现。 Rhino的具体实现,与Joined Object,是没有直接关系的。或者说,Rhino实现的其实是所有equated的函数都是一个BaseFunction对象。实际上,所有的现实js实现都会采取类似的策略。 特别的,我们在SpiderMonkey(就是FF等用的js引擎)中可以看到这种痕迹: function A() { return function() {} } 你会发现,A().__proto__不等于Function.prototype(由于__proto__并没有说就是[[prototype]],所以也不能说spidermonkey不合规范)。 但是两次调用的结果是相同的。也就是 A().__proto__ == A().__proto__,并且A().__proto__.__proto__ == Function.prototype。 也就是说,SpiderMonkey会在每个function(设为X)里面预先产生一个共享的function(设为Y)。所有X里直接包含的function都会以Y作为__proto__,而这些不同的function对象,只有[[scope]]是不同的。这就是一种优化,而且很美妙的使用了js自己的prototype chain的机制。 Safari有__proto__属性,但是就没有上述现象(但是它仍可以在内部有一个类似的机制)。 Lich_Ray 写道 讲解结束,下面是我对这个问题的个人意见。 我给出的代码,从效率角度来看,不是最优的;但从上文的叙述中可以看出,可以这样理解:
我的话说完了。请注意,下面不是答疑时间。 PS: 刚刚才看到 keshin 原帖新加的内容。ECMA 的人说的非常正确,不过他们不敢解释太多;态度很客气,这一点让我非常佩服。 最后,回复keshin的不知道是哪位。他所描述的也就是一个实现的一般思路。但是说到规范的本意,只有问当时的参与制定者才能了解。例如BE同志。 |
|
返回顶楼 | |
发表时间:2007-07-20
前面已经说过的部分不再赘述。就解释一下红字部分。他不是说对于function A(){return function(){}},两次A()就可以join,两次new A()就不能join。不是这个意思。它是指,对于function A(){return new Function()},两次调用是不能join的。另外两次eval的call的结果也不能join。
keshin 写道: 如果是,我不知道,你是否指的是下面红色的部分。
13.1.1 Equated Grammar Productions 我很不希望,你指的是这部分。 |
|
返回顶楼 | |
发表时间:2007-07-20
最后一段的意思是,实践中对于[[scope]]没有差别的才join可能更高效,因为所有join的情况就变成了自己join自己,也就是使用同一个对象(因为连[[scope]]都一样,那就完全一样了)。
ie和ff都没有实现join,除了因为join的定义本身存在问题,还因为判断[[scope]]没有可观测的差异实际上并不容易。 至于说相等性测试对于函数无意义,我想你现在明白他的意思了,规范里是不会有这一条的。 keshin 写道: Step1 允许一个具体实现在在function A有一个不依赖于A的function B时进行优化。注意是允许 在给出的第一个例子中,规范也仅仅是说,允许但不需要把b1和b2 join。 再看最后一段,只有在具体实现能够证明两个function Object的scope属性的区别是看不出来的(真饶)情况下,两个function Object才可以join。根据这个规则,恐怕实现只能找到对象它自己是可已join的。 在回头看我们的相等性测试 两个值是同一对象,或者是可以join的,而关于这个join,规则本身也仅仅是一个非常宽松的规定。 再来看我引用的这对规范里面的这个例子,在客户端b1和b2能通过相等性测试么?很遗憾,至少在ie和ff,它都不能,也就是说 这两个客户端貌似没有实现这个规范。 至于说相等性测试对于函数是无意义的,一定返回false的,我希望谁能够告诉我具体在规范的哪里? |
|
返回顶楼 | |
发表时间:2007-07-20
Lich_Ray 写道 至于你最后说的 ECMA 的回复,他们说的很客观,意思是优化未必启动。我想了一下,觉得这可能和数据流分析的结果有关,而数据流分析会受运行时情况影响,而且是个概率算法,优化是否启动因此不确定。这是我的猜测,可能错得很彻底,因为我也不可能对 Rhino 研究很透;而且如果谈到这些,那就和你的初衷背离了不止一点了,刨根问底也要有个度嘛。 ECMA的人的回复所谓“优化未必启动”,并非在说join,而是说其局部变量在某些情况下可以被优化为用stack,而不是heap。理论上这种优化是可行的,而且是确定的,并非什么概率算法。但是实际中,我倒是确实不知道有哪些implementation是作这种优化的。也许Rhino的js to java byte code之后,jvm会作这种优化。 |
|
返回顶楼 | |
发表时间:2007-07-20
引用 说白了,就是必须是从同一个函数体产生出来的。这里哪里需要字符串比较呢?由此,你后面的解说也属无效。
... 如果你不是把所有创建的对象都放入数组。而是仅仅new XXX(); 则这些对象一被创建出来就可被垃圾回收。这样测试结果相差很少。 非常明显,这是我的猜测;因为我不能理解 Rhino 解释器的优化在捕获数组环境时这么有意思;所以我也不再想提这一点。 引用 就我所知的目前所有主流的js引擎,优化力度都是相当小的,不存在把空函数调用取消这样的优化。
这个就要推敲一下了。根据闭包算法,调用一个空函数无须链接逃逸变量,效率极高,即使不优化;所有我说的是“相当于没有”。 引用 你说的是没错。但是你的表达不严谨。既然不 ===,就不能说是“相等性”。
这个就是你的理解错误了:equated 译为“相等”是当然的。 引用 如果按照规范的话,等价function会被joined起来,并且===比较会是true。就这点而言,我认为规范本身是有问题的。
... 如前所述,规范本身可能存在缺陷,导致语义可变。所以所有的实现都选择不join。 规范本身一点问题没有,是你想的太天真了。语义上出现两可解释是不能满足等式推理的语言的可怜的特点,错不在规范。 引用 Browser里js执行的每次时间有较大波动(如相差50%)是很正常的,除非累积大量数据,你不能从中得出任何结论。
我已经重启计算机排除缓存问题,这个还是免提了吧。 引用 这里根本不存在任何Java代码。况且Java的垃圾回收再烂,也不会出现你所说的这种情况。
这个还是要斟酌一下;不过有一点可以肯定,我没碰到过要一分半钟这种问题。 引用 实际上看到这里,你应该可以看出两个turnOn根本就指向的是同一个对象,即函数test。所以当然是相等的。不等才怪! 其次,如前所说,这里根本没有两个test,只有一个test,然后有若干个对test的引用。 我认为这里逻辑没有错误。被join之后,对象当然只会剩下一个,于是我问被join吗,能所以返回true啊,我只是少些一句话啊。 引用 SpiderMonkey会在每个function(设为X)里面预先产生一个共享的function(设为Y)。所有X里直接包含的function都会以Y作为__proto__,而这些不同的function对象,只有[[scope]]是不同的。这就是一种优化,而且很美妙的使用了js自己的 prototype chain的机制。
说了这么多张帖,就这一张是有用的,其它的其实懂的人心里都很清楚,不要埋怨别人写的这里那里不好。 |
|
返回顶楼 | |
发表时间:2007-07-20
hax 写道: 前面已经说过的部分不再赘述。就解释一下红字部分。他不是说对于function A(){return function(){}},两次A()就可以join,两次new A()就不能join。不是这个意思。它是指,对于function A(){return new Function()},两次调用是不能join的。另外两次eval的call的结果也不能join。
keshin 写道: 如果是,我不知道,你是否指的是下面红色的部分。
13.1.1 Equated Grammar Productions 我很不希望,你指的是这部分。 呵呵,一天没来,想不到这个贴又加了那么多,另外我不知道你这块是说给我听的还是他听的,只想说,我想表达就是你这个意思。
另外很佩服你语言组织的逻辑能力,这和修为有关,我还得加强。
|
|
返回顶楼 | |
发表时间:2007-07-20
Lich_Ray 写道 引用 就我所知的目前所有主流的js引擎,优化力度都是相当小的,不存在把空函数调用取消这样的优化。
这个就要推敲一下了。根据闭包算法,调用一个空函数无须链接逃逸变量,效率极高,即使不优化;所有我说的是“相当于没有”。 首先空函数优化和闭包优化是不同层次的优化。而我说的是不存在空函数优化。其次你所说的这种对于闭包的优化,实际上不限于空函数,就是说它可以被视作与其若干的外层的function等所有scope无关,按照规范的语言来说,就是该function的[[scope]]与某个更外层上的function的[[scope]],没有可以观察到的差别。 但是,我认为,这种优化对于执行效率的提高是极其有限,仅仅能提高一些name resolve的速度,几乎可以忽略不计。它只是有助于更高效的垃圾回收。而现有的引擎是否会做这个优化,是非常可疑的。就我所知,只有SpiderMonkey会在非常罕见的条件下,才会执行这个优化。Rhino是不会的。其他引擎不详。 |
|
返回顶楼 | |
发表时间:2007-07-20
终于看完了,还是挺精彩的。
有些部分写得还是挺好。 |
|
返回顶楼 | |
发表时间:2007-07-20
Lich_Ray 写道 引用 你说的是没错。但是你的表达不严谨。既然不 ===,就不能说是“相等性”。
这个就是你的理解错误了:equated 译为“相等”是当然的。 你不要想当然。技术讲求精确性,特别是在说规范的时候。显然equal和equated的意思是不一样的,否则规范何必用两个词?我们通常把equal作为“相等”,那么对于equated,你就不能马马虎虎的说是“相等”。 equated强调“换算”的意思,是“视作等同”,或可译作“等价的”,但不是“相等的”(equal),更不能是“相同的”(same)。 而你对这几个词汇的使用是乱做一团。 正是因为你混淆了equated和equal的含义,所以才会闹出“识别相等要进行函数代码字符串比较”这样的笑话。 我说你表达不严谨已经是很客气了。 |
|
返回顶楼 | |