TL; DR较慢的循环归因于访问数组的“越界”,它要么迫使引擎以更少的优化甚至没有优化就重新编译函数,或者不以任何以这些优化开头的函数来编译函数(如果(JIT-)编译器在第一次编译“版本”之前检测到/怀疑此情况),请继续阅读下面的原因;
有人刚刚
有说这(完全惊讶没人已经这样做):
曾经有一段时间,当OP的片断将是一个初学编程的书旨在勾勒出一个事实上的例子/强调的是“阵列”在javascript是索引的起点为0,而不是1,因此可以用作常见“初学者错误”的示例(您不喜欢我如何避免使用“编程错误”这一短语
;)
):
越界数组访问。
示例1:使用基于0的索引(在ES262中始终),该元素是5个元素
的Dense Array
(连续的(指的是索引之间没有空隙)并且实际上每个索引处的元素)。
var arr_five_char=['a', 'b', 'c', 'd', 'e']; // arr_five_char.length === 5
// indexes are: 0 , 1 , 2 , 3 , 4 // there is NO index number 5
因此,我们并不是真正在谈论<
vs <=
(或“一个额外的迭代”)之间的性能差异,而是在谈论:
“为什么正确的代码段(b)比错误的代码段(a)运行得更快?”?
答案是2倍(尽管从ES262语言实施者的角度来看,两者都是优化形式):
- 数据表示:如何在内存中内部表示/存储数组(对象,哈希图,“实际”数字数组等)
- 功能性机器码:如何编译访问/处理(读取/修改)这些“数组”的代码
接受的答案足以充分解释第1项(正确的恕我直言),但是第2项:编译仅花费了2个字(“代码”)。
更准确地说:JIT编译,更重要的是JIT RE编译!
语言规范基本上只是一组算法的描述(“为实现定义的最终结果而执行的步骤”)。事实证明,这是描述语言的一种非常漂亮的方式。实现者可以将引擎用来实现指定结果的实际方法留给实施者,这给了很多机会来提出更有效的方法来产生定义的结果。符合规范的引擎应为任何定义的输入给出符合规范的结果。
现在,随着javascript代码/库/用法的增加,并记住“实际”编译器使用了多少资源(时间/内存/等),很明显,我们不能让用户访问网页的时间这么长(并要求他们拥有那么多资源)。
想象一下以下简单功能:
function sum(arr){
var r=0, i=0;
for(;i<arr.length;) r+=arr[i++];
return r;
}
完全清楚吧?不需要任何其他说明,对吗?返回类型是Number
,对吗?
好吧..不,不,不...这取决于您传递给命名函数参数的参数arr
...
sum('abcde'); // String('0abcde')
sum([1,2,3]); // Number(6)
sum([1,,3]); // Number(NaN)
sum(['1',,3]); // String('01undefined3')
sum([1,,'3']); // String('NaN3')
sum([1,2,{valueOf:function(){return this.val}, val:6}]); // Number(9)
var val=5; sum([1,2,{valueOf:function(){return val}}]); // Number(8)
看到问题了吗?然后考虑一下,这只是勉强刮除可能的大量排列...在完成之前,我们甚至不知道函数RETURN是哪种类型...
现在想象一下,实际上是在不同类型甚至输入的变体上使用了相同的功能代码,完全按字面意义(在源代码中)描述了动态地在程序中生成的“数组”。
因此,如果您要编译函数sum
JUST ONCE,那么唯一可以始终返回任何类型和所有类型输入的规范定义结果的唯一方式,显然,只有执行所有规范规定的main AND子步骤,才能保证符合规范的结果(如未命名的y2k之前的浏览器)。没有优化(因为没有任何假设),而且仍然存在缓慢的解释性脚本语言。
JIT编译(JIT在即时中)是当前流行的解决方案。
因此,您开始使用关于函数执行,返回和接受的假设来编译函数。
您想出了尽可能简单的检查方法,以检测该函数是否可能开始返回不符合规范的结果(例如因为接收到意外的输入)。然后,放弃先前的编译结果,然后重新编译为更详细的内容,决定如何处理已经拥有的部分结果(是否值得信任或可以再次进行计算以确保确定),将该函数重新绑定到程序中并再试一次。最终归结为规范中的逐步脚本解释。
所有这一切都需要时间!
所有浏览器都在其引擎上运行,对于每个子版本,您都会看到事情有所改善和消退。在历史上的某个时候,字符串是真正不可变的字符串(因此array.join比字符串连接快),现在我们使用绳索(或类似的绳索)来缓解问题。两者都返回符合规范的结果,这很重要!
长话短说:仅仅因为javascript语言的语义经常受到我们的支持(就像OP实例中的这个无声错误),并不意味着“愚蠢”的错误会增加我们编译器吐出快速机器代码的机会。假设我们编写了“通常”正确的指令:(编程语言的)“用户”当前必须遵循的口头禅是:帮助编译器,描述我们想要的内容,支持常见的习惯用法(从asm.js中获取提示以进行基本理解)浏览器可以尝试优化的内容以及原因)。
因此,谈论性能不仅对雷区来说很重要(而且由于上述雷区,我真的想最后指出(并引用)一些相关材料:
访问不存在的对象属性和超出范围的数组元素将返回该undefined
值,而不是引发异常。这些动态功能使使用JavaScript进行编程变得方便,但同时也使将JavaScript编译为有效的机器代码变得困难。
...
有效的JIT优化的重要前提是程序员以系统的方式使用JavaScript的动态功能。例如,JIT编译器利用以下事实:对象属性通常以特定顺序添加到给定类型的对象中,或者很少发生超出范围的数组访问。JIT编译器利用这些规律性假设在运行时生成高效的机器代码。如果代码块满足假设,则JavaScript引擎将执行高效的生成的机器代码。否则,引擎必须退回到较慢的代码或解释程序。
资料来源:
“JITProf:精确定位JIT不友好的JavaScript代码”
伯克利出版,2014年,由两宫,迈克尔·普拉德尔,科希克参议员
http://software-lab.org/publications/jitprof_tr_aug3_2014.pdf
ASM.JS(也不喜欢超出范围的数组访问):
提前编译
由于asm.js是JavaScript的严格子集,因此此规范仅定义了验证逻辑-执行语义只是JavaScript的语义。但是,经过验证的asm.js可以进行提前(AOT)编译。而且,由AOT编译器生成的代码可以非常高效,其特点是:
- 未装箱的整数和浮点数表示;
- 缺少运行时类型检查;
- 没有垃圾收集;和
- 高效的堆加载和存储(实现策略因平台而异)。
无法验证的代码必须通过传统方式重新执行,例如解释和/或即时(JIT)编译。
http://asmjs.org/spec/latest/
最后是https://blogs.windows.com/msedgedev/2015/05/07/bringing-asm-js-to-chakra-microsoft-edge/
是否有一小节关于消除边界时引擎内部性能的改进-检查(仅在循环外取消边界检查已经改善了40%)。
编辑:
请注意,有多个消息源谈到了JIT重新编译的不同层次,直至解释。
基于上述信息的关于OP片段的理论示例:
- 呼叫isPrimeDivisible
- 使用一般假设编译isPrimeDivisible(例如无界访问)
- 做工作
- BAM,突然数组访问超出范围(在最后)。
- 引擎说废话,让我们使用不同的(较少)假设重新编译isPrimeDivisible,并且该示例引擎不会尝试找出是否可以重用当前的部分结果,因此
- 使用较慢的功能重新计算所有工作(希望它完成了,否则重复一次,这次仅是解释代码)。
- 返回结果
因此,时间就是:
第一次运行(最终失败)+对每次迭代使用较慢的机器代码再次进行所有工作,再进行重新编译等。在该理论示例中,显然要花费> 2倍的时间 !
编辑2 :( 免责声明:基于以下事实的猜想)
我想得越多,我就越认为此答案实际上可以解释对错误代码段a(或代码段b的性能奖励)进行“惩罚”的更主要的原因,具体取决于您的想法),这正是我为什么称它为代码错误(代码段a)的原因:
假设这this.primes
是一个“密集数组”纯数字,或者是
- 源代码中的硬编码文字(已知的优秀候选者将成为“真实”数组,因为编译器在编译时就已经知道了一切)或
- 最有可能是使用
new Array(/*size value*/)
以递增顺序填充预大小()的数值函数生成的(另一个长期以来已知的候选对象成为“真实”数组)。
我们也知道primes
数组的长度被缓存为prime_count
!(表明其意图和固定大小)。
我们还知道,大多数引擎最初将Array作为修改时复制(需要时)传递给Array,这使得处理它们的速度更快(如果您不更改它们的话)。
因此,可以合理地假设Array primes
最有可能在内部已经是一个优化的数组,它在创建后不会更改(对于编译器来说很容易知道,是否没有代码在创建后修改该数组),因此已经存在(如果适用于引擎)以优化的方式存储,就好像它是一样Typed Array
。
正如我试图通过sum
函数示例弄清楚的那样,传递的参数会影响实际需要发生的情况,以及如何将特定代码编译为机器代码。将a传递String
给该sum
函数不应更改字符串,而应更改该函数的JIT编译方式!将数组传递给时,sum
应编译不同版本的机器代码(可能是这种类型的附加形状,或者称其为“形状”)。
primes
在编译器知道该函数甚至不会修改它的同时,将类Typed_Array的数组即时转换为something_else似乎有点不客气。
在这些假设下,有2种选择:
- 假设没有越界,作为数字压缩器进行编译,最后遇到越界问题,重新编译和重做工作(如以上编辑1中的理论示例所述)
- 编译器已经预先检测到(或怀疑?)超出范围的访问权限,并且该函数是JIT编译的,就好像传递的参数是一个稀疏对象导致了较慢的功能机器代码(因为它将进行更多的检查/转换/强制)等等。)。换句话说:该函数从来没有资格进行某些优化,而是像接收到“稀疏数组”(类似)参数那样进行编译。
我现在真的很奇怪这是哪两个!
<=
和<
是相同的,无论在理论上还是在所有现代处理器(和口译)实际执行。