2022/09/01

[note] Javascript普通執行與worker執行大量數據運算的速度差異測試

##ReadMore##

之前寫的某個東西,由於要處理大量數據的最大最小值比較,由於吃的時間有點凶,在想說能不能做到多核心處理。翻閱了很多文件,才知道即使用了像是forEach/Map這種東西,JavaScript還是單執行序處理。難過。

繼續研究之後,才發現有兩個東西,Promise和Worker。Promise看起來應該是還是用同一執行序執行,要善用到多核心,看來需要用到Worker,worker才真的是用到不同的執行序。實測用worker,真的可以把intel 9th H SKU CPU 16執行序塞滿。

但又發現一個問題,用worker本身其實很花時間,照網路上一些解釋,建立worker本身就要花時間,所以單執行序執行時間不夠長的,用worker不一定划算。所以我做了這個

dataset = 1;
dataqty = 86400*1;


console.time("for loop test");for(j=0;j<dataset;j++)for(i=0,t=0;i<dataqty;i++)t=(t+Math.random())/2;console.timeEnd("for loop test");


console.time("worker test");
response = "onmessage=function(e){for(i=0,t=0;i<"+dataqty+";i++)t=(t+Math.random())/2;postMessage(e.data);}";
wkResult = (new Array(dataset)).fill(false);
wkArr = (new Array(dataset).fill(1)).map((e, i) => { // if not fill, map will not action
	var wk = new Worker( 'data:application/javascript,' + encodeURIComponent(response) );
	wk.onmessage = (ee) => {
		wkResult[ee.data]=true;
		wkArr[ee.data].terminate(); // if not terminate, browser will out of memory
		if(!wkResult.includes(false)){
			console.timeEnd("worker test");
		}
	};
	wk.postMessage(i);
	return wk;
});
//var dateStart = new Date()
//while((new Date())-dateStart < 1000 & wkResult.includes(false)){} // cannot set while loop, worker will be set after main thread idle.
//console.log(wkResult.includes(false))
//console.timeLog("worker test");

我寫了一個亂數求平均,由於我目標要套用的資料,原始長度有可能會到一天,先假設dataqty=86400,資料先抓只有一筆,dataset就1。前面先用普通方式執行,一個for迴圈掃dataset,一個for迴圈掃時間軸。

後面改用worker去跑。原始資料必須在同數列內計算比較,所以只能用不同數列去拆worker。可以看到,為了建worker,程式碼多了超多出來。首先要先用data:建立一個js,然後worker去開成新worker物件,設定listener,設定條件,最後postMessage等著接訊息。超麻煩。

本來還想說用傳統的寫法,既然都丟worker了,應該可以先發下去然後用主程序設while等回復吧。結果不行,while下去程式死給你看,設while一秒就等一秒,設while十秒就等十秒。最後只能把計時結束放在onmessage內。

調整dataset和dataqty,可以測試不同的數列與不同資料數,到底差多少時間,但這樣太不自動了,所以我又改寫了一下

dataset = 1;
dataqty = 86400*1;

time_main = [];
for(s=0;s<=4;s++)
	for(q=1;q<6;q++)
		for(times=0;times<3;times++){
			var n=window.performance.now();
			dataset=[4,8,16,32,64][s];
			dataqty=86400*q;
			for(j=0;j<dataset;j++)for(i=0,t=0;i<dataqty;i++)t=(t+Math.random())/2;
			var d=[dataset, q, times, window.performance.now()-n];
			time_main.push(d);
		}
console.log(time_main.map(e=>e.join("\t")).join("\n"))



time_ws = [];
s=0;
q=1;
times=0;
function tester(s,q){
			var n=window.performance.now();
			dataset=[4,8,16,32,64][s];
			dataqty=86400*q;
			response = "onmessage=function(e){for(i=0,t=0;i<"+dataqty+";i++)t=(t+Math.random())/2;postMessage(e.data);}";
			wkResult = (new Array(dataset)).fill(false);
			wkArr = (new Array(dataset).fill(1)).map((e, i) => { // if not fill, map will not action
				var wk = new Worker( 'data:application/javascript,' + encodeURIComponent(response) );
				wk.onmessage = (ee) => {
					wkResult[ee.data]=true;
					wkArr[ee.data].terminate(); // if not terminate, browser will out of memory
					if(!wkResult.includes(false)){
						var d=[dataset, q, times, window.performance.now()-n];
						time_ws.push(d);
						times++;
						if(!(times<3))times=0,q++;
						if(!(q<6))q=1,s++;
						if(s<=4){
							tester(s,q);
							//console.log([s,q]);
						}else{
							console.log(time_ws.map(e=>e.join("\t")).join("\n"))
						}
					}
				};
				wk.postMessage(i);
				return wk;
			});
}
tester(s,q);

我的想法很簡單,掃描不同的dataset,不同的dataqty,然後測三次求平均。

對一般的方式,這很簡單,多加三個loop就結束了。

但進到用worker...喔...程式突然變得超複雜。由於要避免程式不必要的平行跑,必須在真的執行完所有worker時,才執行下一次。所以原本用來輸出的位置被塞滿滿的判斷式,同時也要建成一個function才比較好執行

比較結果如下,以i7-10880H,8c16t執行,單位為毫秒。


dataset48163264
dataqtymainworkermain <
worker
mainworkermain <
worker
mainworkermain <
worker
mainworkermain <
worker
mainworkermain <
worker
86400*11 14.221.7False 24.222.3False 30.641.3False 61.480.4False 122.7161.2False
2 11.714.3 15.322 30.575.1 60.884.8 122.1165.2
3 11.113.8 15.523.4 30.560.1 60.890.3 122.4208.3
mean 12.316.6 18.322.6 30.558.8 61.085.2 122.4178.2
86400*2 1 17.914.9False31.125.7True61.261.9True122.292.4True243.9198.0True
2 11.517.831.32761.345.5122.893.6244.7186.2
3 11.71530.825.361.753.9122.693.3244.6181.4
mean 13.715.931.126.061.454.8122.693.1244.4188.5
86400*31 17.718.5False45.731.4True91.854.0True183.4103.4True367.2210.4True
2 17.817.246.029.092.355.7183.9102.7369.4228.4
3 17.720.546.230.991.753.0181.7106.3365.7200.5
mean 17.718.746.030.491.954.2183.0104.1367.4213.1
86400*41 23.619.6True62.331.8True122.961.5True242.9119.4True488.6224.3True
2 24.418.662.135.3122.654.4242.9112.8437.7232.9
3 26.724.86231122.665.1243.9113.9459.9223.0
mean 24.921.062.132.7122.760.3243.2115.4462.1226.7
86400*51 30.621.3True78.335.7True152.865.2True306.2125.3True564.5253.8True
2 3222.577.334.1152.765.1307.4117.5560.3290.7
3 30.321.876.336151.662.0304.5124.0557.4292.0
mean 31.021.977.335.3152.464.1306.0122.3560.7278.8

可以看到,worker開多不一定能加速,取決於worker內的內容有多長。處理的資料不夠大,開worker可能還會損失因為建立worker所等待的時間,不過這麼大量的數據計算,是否要放在前端由使用者web browser執行就是很值得討論的了。blogger沒有簡易的table編輯器,這邊都我自己key HTML code手key數字的,排版就隨便了。

結論,使用worker前要先評估怎麼建立才能真正加速,必要在使用者端做很長的運算再來用worker。

沒有留言: