为什么预计算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.

再次强制完成,因为查找太接近错误率。但校准后的查找几乎与时钟速度完美匹配?巧合..我不确定。

相关文章