为什么预计算sin(X)比在Java脚本中使用Math.sin()要慢?
我在JavaScript中发现了一个有趣的异常现象。它集中在我通过预计算sin(X)和cos(X)并简单地引用预计算值来加速三角变换计算的尝试。
直观地说,预计算比每次计算Math.sin()和Math.cos()函数要快。尤其是如果您的应用程序设计将仅使用一组有限的值作为trig函数的参数(在我的例子中,区间为[0°,360°)中的整数度,这对我的目的来说已经足够了)。
所以,我运行了一个小测试。在预计算完sin(X)和cos(X)的值并将它们存储在360个元素的数组中之后,我编写了一个简短的测试函数,通过一个简单的测试HTML页面中的一个按钮来激活,以比较这两种方法的速度。一个循环将一个值乘以预计算的数组元素值,而另一个循环将一个值乘以Math.sin()。
我的预期是,预计算循环会比涉及对trig函数的函数调用的循环快得多。令我惊讶的是,预计算循环的速度慢。
下面是我编写的测试函数:
function MyTest()
{
var ITERATION_COUNT = 1000000;
var angle = Math.floor(Math.random() * 360);
var test1 = 200 * sinArray[angle];
var test2 = 200 * cosArray[angle];
var ref = document.getElementById("Output1");
var outData = "Test 1 : " + test1.toString().trim() + "<br><br>";
outData += "Test 2 : "+test2.toString().trim() + "<br><br>";
var time1 = new Date(); //Time at the start of the test
for (var i=0; i<ITERATION_COUNT; i++)
{
var angle = Math.floor(Math.random() * 360);
var test3 = (200 * sinArray[angle]);
//End i loop
}
var time2 = new Date();
//This somewhat unwieldy procedure is how we find out the elapsed time ...
var msec1 = (time1.getUTCSeconds() * 1000) + time1.getUTCMilliseconds();
var msec2 = (time2.getUTCSeconds() * 1000) + time2.getUTCMilliseconds();
var elapsed1 = msec2 - msec1;
outData += "Test 3 : Elapsed time is " + elapsed1.toString().trim() + " milliseconds<br><br>";
//Now comparison test with the same number of sin() calls ...
var time1 = new Date();
for (var i=0; i<ITERATION_COUNT; i++)
{
var angle = Math.floor(Math.random() * 360);
var test3 = (200 * Math.sin((Math.PI * angle) / 180));
//End i loop
}
var time2 = new Date();
var msec1 = (time1.getUTCSeconds() * 1000) + time1.getUTCMilliseconds();
var msec2 = (time2.getUTCSeconds() * 1000) + time2.getUTCMilliseconds();
var elapsed2 = msec2 - msec1;
outData += "Test 4 : Elapsed time is " + elapsed2.toString().trim() + " milliseconds<br><br>";
ref.innerHTML = outData;
//End function
}
我进行上述操作的动机是,乘以从数组中获取的预计算值比调用函数调用trig函数要快,但我得到的结果却异常有趣。
某些样本运行会产生以下结果(测试3是预计算的运行时间,测试4是Math.sin()运行时间):
运行1:
Test 3 : Elapsed time is 153 milliseconds
Test 4 : Elapsed time is 67 milliseconds
运行2:
Test 3 : Elapsed time is 167 milliseconds
Test 4 : Elapsed time is 69 milliseconds
运行3:
Test 3 : Elapsed time is 265 milliseconds
Test 4 : Elapsed time is 107 milliseconds
运行4:
Test 3 : Elapsed time is 162 milliseconds
Test 4 : Elapsed time is 69 milliseconds
为什么调用trig函数比引用数组中的预计算值快一倍,而预计算方法至少在直觉上应该更快一些?尤其是因为我使用整数参数来索引预计算循环中的数组,而函数调用循环还包括将度数转换为弧度的额外计算?
这里发生了一些有趣的事情,但目前,我不确定是什么。通常,对预计算数据的数组访问要比调用复杂的trig函数快得多(或者至少,在我用汇编语言编写类似代码的时候,它们是最快的!),但JavaScript似乎颠覆了这一点。我能想到的唯一原因是,JavaScript在幕后为数组访问增加了大量开销,但如果是这样的话,这将影响许多其他代码,这些代码似乎以完全合理的速度运行。
那么,这里到底发生了什么?
我在Google Chrome中运行此代码:
版本60.0.3112.101(官方版本)(64位)
在64位Windows 7上运行。我还没有在Firefox中尝试过,看看那里是否会出现同样的异常结果,但这是待办事项列表中的下一个任务。
任何对JavaScript引擎的内部工作原理有深入了解的人,请帮助!
解决方案
优化器已歪曲结果。
几乎完全相同的两个测试函数。
在基准测试中运行它们,结果令人惊讶。
{
func : function (){
var i,a,b;
D2R = 180 / Math.PI
b = 0;
for (i = 0; i < count; i++ ) {
// single test start
a = (Math.random() * 360) | 0;
b += Math.sin(a * D2R);
// single test end
}
},
name : "summed",
},{
func : function (){
var i,a,b;
D2R = 180 / Math.PI;
b = 0;
for (i = 0; i < count; i++ ) {
// single test start
a = (Math.random() * 360) | 0;
b = Math.sin(a * D2R);
// single test end
}
},
name : "unsummed",
},
结果
=======================================
Performance test. : Optimiser check.
Use strict....... : false
Duplicates....... : 4
Samples per cycle : 100
Tests per Sample. : 10000
---------------------------------------------
Test : 'summed'
Calibrated Mean : 173µs ±1µs (*1) 11160 samples 57,803,468 TPS
---------------------------------------------
Test : 'unsummed'
Calibrated Mean : 0µs ±1µs (*1) 11063 samples Invalid TPS
----------------------------------------
Calibration zero : 140µs ±0µs (*)
(*) Error rate approximation does not represent the variance.
(*1) For calibrated results Error rate is Test Error + Calibration Error.
TPS is Tests per second as a calculated value not actual test per second.
基准测试几乎没有任何时间用于未汇总测试(必须强制其完成)。
优化器知道只需要未加和测试的循环的最后结果。它只对最后一次迭代起作用,所有其他结果都没有使用,所以为什么要使用它们。
Java脚本中的基准测试充满了陷阱。使用质量基准,并知道优化器能做什么。
登录和查找测试。
测试数组和SIN。说句公道话,我没有做弧度转换。
tests : [{
func : function (){
var i,a,b;
b=0;
for (i = 0; i < count; i++ ) {
a = (Math.random() * 360) | 0;
b += a;
}
},
name : "Calibration",
},{
func : function (){
var i,a,b;
b = 0;
for (i = 0; i < count; i++ ) {
a = (Math.random() * 360) | 0;
b += array[a];
}
},
name : "lookup",
},{
func : function (){
var i,a,b;
b = 0;
for (i = 0; i < count; i++ ) {
a = (Math.random() * 360) | 0;
b += Math.sin(a);
}
},
name : "Sin",
}
],
和结果
=======================================
Performance test. : Lookup compare to calculate sin.
Use strict....... : false
Data view........ : false
Duplicates....... : 4
Cycles........... : 1055
Samples per cycle : 100
Tests per Sample. : 10000
---------------------------------------------
Test : 'Calibration'
Calibrator Mean : 107µs ±1µs (*) 34921 samples
---------------------------------------------
Test : 'lookup'
Calibrated Mean : 6µs ±1µs (*1) 35342 samples 1,666,666,667TPS
---------------------------------------------
Test : 'Sin'
Calibrated Mean : 169µs ±1µs (*1) 35237 samples 59,171,598TPS
-All ----------------------------------------
Mean : 0.166ms Totals time : 17481.165ms 105500 samples
Calibration zero : 107µs ±1µs (*);
(*) Error rate approximation does not represent the variance.
(*1) For calibrated results Error rate is Test Error + Calibration Error.
TPS is Tests per second as a calculated value not actual test per second.
再次强制完成,因为查找太接近错误率。但校准后的查找几乎与时钟速度完美匹配?巧合..我不确定。
相关文章