我在JavaScript中发现了一个有趣的异常现象。其中心是我尝试通过预先计算sin(x)和cos(x)来加速三角变换计算,并简单地引用预先计算的值。
直观地说,人们会期望预计算比每次计算Math.sin()和Math.cos()函数更快。特别是如果你的应用程序设计只对trig函数的参数使用一组有限的值(在我的例子中,在[0°,360°]区间内的整数度数,这对我的目的来说足够了。)< / p>
所以,我做了一点测试。在预先计算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函数要快得多(或者至少,它们在我在汇编程序中编写类似代码的时候回来了!),但JavaScript似乎把它转变为头脑。我能想到的唯一原因是JavaScript在后台为数组访问增加了很多开销,但如果是这样的话,这会影响很多其他代码,看起来运行速度非常合理。
那么 究竟在这里发生了什么?
我在Google Chrome中运行此代码:
版本60.0.3112.101(官方构建)(64位)
在Windows 7 64位上运行。我还没有在Firefox中尝试过,看看是否会出现相同的异常结果,但这是待办事项列表的下一步。
任何对JavaScript引擎内部工作有深刻理解的人都请帮忙!
答案 0 :(得分:2)
两个完全相同的测试功能 在基准测试中运行它们,结果令人惊讶。
{
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.
基准测试人员几乎没有时间进行未经总结的测试(必须强制完成测试)。
优化器知道只需要未完成测试的循环的最后结果。它只适用于最后一次迭代所有其他结果都没有使用,所以为什么要这样做。
javascript中的基准测试充满了捕获量。使用质量基准测试程序,并了解优化程序可以执行的操作。
测试数组和罪恶。为了公平对待罪,我不会做弧度转换。
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.
由于查找过于接近错误率,因此再次进行了力完成。但校准的查找几乎与时钟速度完美匹配???巧合..我不确定。
答案 1 :(得分:1)
我认为这是你身边的基准问题。
(ns sre.plan.dsl.constraint
(:require [clojure.set :refer :all]
[clojure.algo.generic.functor :refer :all]))
(defrecord ConstraintLookup [free bound])
&#13;
var countElement = document.getElementById('count');
var result1Element = document.getElementById('result1');
var result2Element = document.getElementById('result2');
var result3Element = document.getElementById('result3');
var floatArray = new Array(360);
var typedArray = new Float64Array(360);
var typedArray2 = new Float32Array(360);
function init() {
for (var i = 0; i < 360; i++) {
floatArray[i] = typedArray[i] = Math.sin(i * Math.PI / 180);
}
countElement.addEventListener('change', reset);
document.querySelector('form').addEventListener('submit', run);
}
function test1(count) {
var start = Date.now();
var sum = 0;
for (var i = 0; i < count; i++) {
for (var j = 0; j < 360; j++) {
sum += Math.sin(j * Math.PI / 180);
}
}
var end = Date.now();
var result1 = "sum=" + sum + "; time=" + (end - start);
result1Element.textContent = result1;
}
function test2(count) {
var start = Date.now();
var sum = 0;
for (var i = 0; i < count; i++) {
for (var j = 0; j < 360; j++) {
sum += floatArray[j];
}
}
var end = Date.now();
var result2 = "sum=" + sum + "; time=" + (end - start);
result2Element.textContent = result2;
}
function test3(count) {
var start = Date.now();
var sum = 0;
for (var i = 0; i < count; i++) {
for (var j = 0; j < 360; j++) {
sum += typedArray[j];
}
}
var end = Date.now();
var result3 = "sum=" + sum + "; time=" + (end - start);
result3Element.textContent = result3;
}
function reset() {
result1Element.textContent = '';
result2Element.textContent = '';
result3Element.textContent = '';
}
function run(ev) {
ev.preventDefault();
reset();
var count = countElement.valueAsNumber;
setTimeout(test1, 0, count);
setTimeout(test2, 0, count);
setTimeout(test3, 0, count);
}
init();
&#13;
在我的测试中,一个数组无疑比一个未缓存的循环更快,而一个类型化的数组比这快得多。类型化数组避免了对数组和计算之间的数字进行装箱和取消装箱的需要。我看到的结果是:
Math.sin():652ms
数组():41毫秒
Float64Array():37ms
请注意,我正在求和并包含结果,以防止JIT优化掉未使用的纯函数。另外,<form>
<input id="count" type="number" min="1" value="100000">
<input id="run" type="submit" value="Run">
</form>
<dl>
<dt><tt>Math.sin()</tt></dt>
<dd>Result: <span id="result1"></span></dd>
<dt><tt>Array()</tt></dt>
<dd>Result: <span id="result2"></span></dd>
<dt><tt>Float64Array()</tt></dt>
<dd>Result: <span id="result3"></span></dd>
</dl>
而不是自己创建秒+毫秒。
答案 2 :(得分:0)
我同意这个问题可能取决于你如何初始化预先计算的数组
Jsbench显示预先计算的数组比使用Math.sin()
快13%
希望这有帮助!