〜 グラフィック 〜
pget命令よりも高速に一点の色を取得 (要GDI32.DLL)

このTipsは如何に短時間で一点の色を取得するかを検証しているものとなります。

「HSPのpget命令は遅い」と巷では耳にすることがあります。

1.pgetで所定の座標を指定して色を取り出す
2.システム変数に入った色を別の変数に退避する
3.特定の色との比較やら何らかの処理を行う

他のやり方でも処理工程数的には縮められそうもないのに何故遅いといわれるのか?
pget命令の元となるAPI関数GetPixelを使って違いを実際に計測してみましょう。
API関数GetPixelについてはコチラを参照してください。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
	ll_libload kernel, "kernel32.dll"
	ll_getproc GetTickCount, "GetTickCount", kernel
	ll_libload gdi, "gdi32.dll"
	ll_getproc GetPixel, "GetPixel", gdi

#module
#deffunc getoptime val
	mref time, 16
	ll_callfnv GetTickCount@
	ll_ret time
	return
#global

	// 準備
	randomize
	redraw 0
	repeat 10000
		rnd col, 2 : rnd col.1, 2 : rnd col.2, 2
		color col * 0xFF, col.1 * 0xFF, col.2 * 0xFF
		rnd i, winx - 250 : rnd i.1, winy
		pset i + 250, i.1
	loop
	redraw 1
	// pgetで取得
	repeat 10
		getoptime tms.cnt
		col = 0
		repeat winx * winy
			pget cnt \ winx, cnt / winx
			if rval + (gval << 8) + (bval << 16) = 0x0000FF : col++
		loop
		getoptime tme.cnt
		tm = tme.cnt - tms.cnt
		color
		mes "" + cnt + ". " + tm + "ms 対象色の数=" + col
	loop
	i = 0 : repeat 10 : i += tme.cnt - tms.cnt : loop : i = i / 10
	mes "\npget命令の平均 " + i + "ms\n"
	// APIで取得
	mref bmscr, 67
	repeat 10
		getoptime tms.cnt
		col = 0
		repeat winx * winy
			i = bmscr.4, cnt \ winx, cnt / winx
			ll_callfunc i, 3, GetPixel@
			ll_ret colors
			if colors = 0x0000FF : col++
		loop
		getoptime tme.cnt
		tm = tme.cnt - tms.cnt
		color
		mes "" + cnt + ". " + tm + "ms 対象色の数=" + col
	loop
	i = 0 : repeat 10 : i += tme.cnt - tms.cnt : loop : i = i / 10
	mes "\nGetPixelの平均 " + i + "ms\n"
	stop
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#uselib "kernel32.dll"
#cfunc GetTickCount "GetTickCount"
#uselib "gdi32.dll"
#cfunc GetPixel "GetPixel" int, int, int

	// 準備
	randomize
	redraw 0
	repeat 10000
		color rnd(2) * 0xFF, rnd(2) * 0xFF, rnd(2) * 0xFF
		pset rnd(ginfo_winx - 250) + 250, rnd(ginfo_winy)
	loop
	redraw 1
	// pgetで取得
	repeat 10
		tms.cnt = GetTickCount()
		col = 0
		repeat ginfo_winx * ginfo_winy
			pget cnt \ ginfo_winx, cnt / ginfo_winx
			if ginfo_r + (ginfo_g << 8) + (ginfo_b << 16) = 0x0000FF : col++
		loop
		tme.cnt = GetTickCount()
		color
		mes strf("%02d回目 %dms 対象色の数=%d", cnt + 1, tme.cnt - tms.cnt, col)
	loop
	foreach tms : i += tme.cnt - tms.cnt : loop
	mes strf("\npget命令の平均 %dms\n", i / length(tms))
	// APIで取得
	i = 0
	mref bmscr, 67
	repeat 10
		tms.cnt = GetTickCount()
		col = 0
		repeat ginfo_winx * ginfo_winy
			if GetPixel(bmscr.4, cnt \ ginfo_winx, cnt / ginfo_winx) = 0x0000FF : col++
		loop
		tme.cnt = GetTickCount()
		color
		mes strf("%02d回目 %dms 対象色の数=%d", cnt + 1, tme.cnt - tms.cnt, col)
	loop
	foreach tms : i += tme.cnt - tms.cnt : loop
	mes strf("\nGetPixelの平均 %dms", i / length(tms))
上記スクリプトはランダムに配置したドットの中から赤色が何点あるかをカウントするものです。
ウィンドウサイズである640×480回分、合計307200回繰り返すと管理人の環境だと、
HSP2のpget命令は平均434ミリ秒、API関数は平均423ミリ秒。
HSP3のpget命令は平均643ミリ秒、API関数は平均471ミリ秒に終わりました。
pget命令同士でも「遅くなったと言われるHSP3」はHSP2よりも200ミリ秒遅い結果でした。
API関数GetPixelを使ったやり方だと、
遅くなったHSP3の場合はHSP2のpget命令の平均タイムに近い400ミリ秒台後半にまで縮まり、
HSP2では確かにpget命令よりも速くはなりましたが10ミリ秒ほどしか縮まりませんでした。

コレ以上縮めることはできないのでしょうか?
「大きな画像の全部または大部分を取得するために繰り返し色取得命令を使うこと」に問題がありそうなので、
pget命令やAPI関数GetPixelの取得元となる「座標ごとの輝度を一元管理しているメモリ領域」
即ちVRAMに直接アクセスすれば更に縮小することができそうですね。
VRAMはHSP2・HSP3共にmref命令で取得することが可能で、
その方法で同じように赤色が何点あるかをカウントするプログラムを組んでみました。
尚、VRAMの説明と扱い方についてコチラを参照してください。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
	ll_libload kernel, "kernel32.dll"
	ll_getproc GetTickCount, "GetTickCount", kernel

#module
#deffunc getoptime val
	mref time, 16
	ll_callfnv GetTickCount@
	ll_ret time
	return
#global

	// 準備
	randomize
	redraw 0
	repeat 10000
		rnd col, 2 : rnd col.1, 2 : rnd col.2, 2
		color col * 0xFF, col.1 * 0xFF, col.2 * 0xFF
		rnd i, winx - 250 : rnd i.1, winy
		pset i + 250, i.1
	loop
	redraw 1
	// pgetで取得
	repeat 10
		getoptime tms.cnt
		col = 0
		repeat winx * winy
			pget cnt \ winx, cnt / winx
			if rval + (gval << 8) + (bval << 16) = 0x0000FF : col++
		loop
		getoptime tme.cnt
		tm = tme.cnt - tms.cnt
		color
		mes "" + cnt + ". " + tm + "ms 対象色の数=" + col
	loop
	i = 0 : repeat 10 : i += tme.cnt - tms.cnt : loop : i = i / 10
	mes "\npget命令の平均 " + i + "ms\n"
	// VRAMから取得
	mref vram, 66
	ginfo 6
	i.0 = (prmx * 3 + 3) & 0xFFFFFFFC
	repeat 10
		getoptime tms.cnt
		col = 0
		repeat prmx * prmy
			i.1 = (prmy - 1 - (cnt / prmx)) * i + (cnt \ prmx * 3)
			peek i.2, vram, i.1 + 2
			peek i.3, vram, i.1 + 1
			peek i.4, vram, i.1
			if i.2 + (i.3 << 8) + (i.4 << 16) = 0x0000FF : col++
		loop
		getoptime tme.cnt
		tm = tme.cnt - tms.cnt
		color
		mes "" + cnt + ". " + tm + "ms 対象色の数=" + col
	loop
	i = 0 : repeat 10 : i += tme.cnt - tms.cnt : loop : i = i / 10
	mes "\nVRAMの平均 " + i + "ms"
	stop
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#uselib "kernel32.dll"
#cfunc GetTickCount "GetTickCount"

	// 準備
	randomize
	redraw 0
	repeat 10000
		color rnd(2) * 0xFF, rnd(2) * 0xFF, rnd(2) * 0xFF
		pset rnd(ginfo_winx - 250) + 250, rnd(ginfo_winy)
	loop
	redraw 1
	// pgetで取得
	repeat 10
		tms.cnt = GetTickCount()
		col = 0
		repeat ginfo_winx * ginfo_winy
			pget cnt \ ginfo_winx, cnt / ginfo_winx
			if ginfo_r + (ginfo_g << 8) + (ginfo_b << 16) = 0x0000FF : col++
		loop
		tme.cnt = GetTickCount()
		color
		mes strf("%02d回目 %dms 対象色の数=%d", cnt + 1, tme.cnt - tms.cnt, col)
	loop
	foreach tms : i += tme.cnt - tms.cnt : loop
	mes strf("\npget命令の平均 %dms\n", i / length(tms))
	// VRAMから取得
	mref vram, 66
	i = (ginfo_sx * 3 + 3) & 0xFFFFFFFC
	repeat 10
		tms.cnt = GetTickCount()
		col = 0
		repeat ginfo_sx * ginfo_sy
			i.1 = ((ginfo_sy - 1 - cnt / ginfo_sx) * i) + cnt \ ginfo_sx * 3
			if peek(vram, i.1 + 2) + (peek(vram, i.1 + 1) << 8) + (peek(vram, i.1) << 16) = 0x0000FF : col++
		loop
		tme.cnt = GetTickCount()
		color
		mes strf("%02d回目 %dms 対象色の数=%d", cnt + 1, tme.cnt - tms.cnt, col)
	loop
	i = 0 : foreach tms : i += tme.cnt - tms.cnt : loop
	mes strf("\nVRAMの平均  %dms", i / length(tms))
結果、VRAMでの取得はHSP2が330ミリ秒、HSP3は325ミリ秒と逆転しました。
逆転はたまたまなのかもわかりませんが、僅差に縮まったことは確かでしょう。

処理回数を減らすことで速度向上するなら、peek命令をwpeek命令やlpeek命令に変えることで
コレ以上に処理時間を縮めることもできるでしょう。
右端の座標を指定するときにパディングデータも含めてしまうとエラーになるので、
その時だけは通常のpeek命令で取得するようにしなければなりません。
HSP2は予め変数のポインタをll_getptr命令で取得必要がありますが、
ll_peek命令を使うことで3バイト分だけ指定して取得することが出来ます。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
	ll_libload kernel, "kernel32.dll"
	ll_getproc GetTickCount, "GetTickCount", kernel

#module
#deffunc getoptime val
	mref time, 16
	ll_callfnv GetTickCount@
	ll_ret time
	return
#global

	// 準備
	randomize
	redraw 0
	repeat 10000
		rnd col, 2 : rnd col.1, 2 : rnd col.2, 2
		color col * 0xFF, col.1 * 0xFF, col.2 * 0xFF
		rnd i, winx - 250 : rnd i.1, winy
		pset i + 250, i.1
	loop
	redraw 1
	// pgetで取得
	repeat 10
		getoptime tms.cnt
		col = 0
		repeat winx * winy
			pget cnt \ winx, cnt / winx
			if rval + (gval << 8) + (bval << 16) = 0x0000FF : col++
		loop
		getoptime tme.cnt
		tm = tme.cnt - tms.cnt
		color
		mes "" + cnt + ". " + tm + "ms 対象色の数=" + col
	loop
	i = 0 : repeat 10 : i += tme.cnt - tms.cnt : loop : i = i / 10
	mes "\npget命令の平均 " + i + "ms\n"
	// 取得回数減少
	mref vram, 66
	ginfo 6
	ll_getptr vram : ll_ret vram_adr
	i.0 = (prmx * 3 + 3) & 0xFFFFFFFC
	repeat 10
		getoptime tms.cnt
		col = 0
		repeat prmx * prmy
			i.1 = (prmy - 1 - (cnt / prmx)) * i + (cnt \ prmx * 3)
			ll_peek i.2, vram_adr + i.1, 3
			if i.2 = 0xFF0000 : col++
		loop
		getoptime tme.cnt
		tm = tme.cnt - tms.cnt
		color
		mes "" + cnt + ".  " + tm + "ms 対象色の数=" + col
	loop
	i = 0 : repeat 10 : i += tme.cnt - tms.cnt : loop : i = i / 10
	mes "\n取得回数減少版VRAMの平均 " + i + "ms"
	stop
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#uselib "kernel32.dll"
#cfunc GetTickCount "GetTickCount"

	// 準備
	randomize
	redraw 0
	repeat 10000
		color rnd(2) * 0xFF, rnd(2) * 0xFF, rnd(2) * 0xFF
		pset rnd(ginfo_winx - 250) + 250, rnd(ginfo_winy)
	loop
	redraw 1
	// pgetで取得
	repeat 10
		tms.cnt = GetTickCount()
		col = 0
		repeat ginfo_winx * ginfo_winy
			pget cnt \ ginfo_winx, cnt / ginfo_winx
			if ginfo_r + (ginfo_g << 8) + (ginfo_b << 16) = 0x0000FF : col++
		loop
		tme.cnt = GetTickCount()
		color
		mes strf("%02d回目 %dms 対象色の数=%d", cnt + 1, tme.cnt - tms.cnt, col)
	loop
	foreach tms : i += tme.cnt - tms.cnt : loop
	mes strf("\npget命令の平均 %dms\n", i / length(tms))
	// 取得回数減少版VRAMから取得
	mref vram, 66
	i = (ginfo_sx * 3 + 3) & 0xFFFFFFFC
	repeat 10
		tms.cnt = GetTickCount()
		col = 0
		repeat ginfo_sx * ginfo_sy
			i.1 = ((ginfo_sy - 1 - cnt / ginfo_sx) * i) + cnt \ ginfo_sx * 3
			if cnt \ ginfo_sx = ginfo_sx - 1 {
				if peek(vram, i.1 + 2) + (peek(vram, i.1 + 1) << 8) + (peek(vram, i.1) << 16) = 0x0000FF : col++
			} else {
				if (lpeek(vram, i.1) & 0xFFFFFF) = 0xFF0000 : col++
			}
		loop
		tme.cnt = GetTickCount()
		color
		mes strf("%02d回目 %dms 対象色の数=%d", cnt + 1, tme.cnt - tms.cnt, col)
	loop
	i = 0 : foreach tms : i += tme.cnt - tms.cnt : loop
	mes strf("\n取得回数減少版VRAMの平均  %dms", i / length(tms))
取得回数を減少させた結果、HSP2が218ミリ秒、HSP3は280ミリ秒でした。
なんと、HSP2では100ミリ秒も縮まってかなり高速化できたようです。
再度HSP2とHSP3の処理時間に開きが出たのは、
右端部取得時にpeek命令を3回繰り返しているか、ll_peek命令1回で取得しているかの違いでしょう。

今回の検証はココまでとして最終結果だけ残しておきます。
HSP2のpget命令から取得回数減少版VRAMのやり方で、434ミリ秒から218ミリ秒に、
HSP3のpget命令から取得回数減少版VRAMのやり方で、643ミリ秒から280ミリ秒になりました。
コレが最速なやり方かどうかはわかりません…というか、恐らく最速ではないと思います。
以上で検証したように、処理時間削減には取得回数を如何に減らすかがポイントであるようなので、
処理方法に応じて最適なやり方を選択するようにしてください。
尚、どのような時でもpget命令よりVRAMの方が速いワケではないことを覚えておいてください。
今回のように、ウィンドウ内全てのピクセルデータを調べる必要がある場合、
言い換えると何万回・何十万回も取得処理を繰り返す必要がある場合にVRAMでの取得は有効ですが、
そんなに取得する必要がない場合はpget命令の方が圧倒的に速くなる可能性もあります。
「今回取得しようとしているもの」がどういうものかを見極めて臨機応変すると良いでしょう。