🤙

【Roblox】pcallの書き方とパフォーマンス

2024/09/20に公開

はじめに

今回はちょっとした小ネタで、個人的に気になっていた事を調査した件です。

Luaにはpcall()という保護モードで関数を実行するためのライブラリ関数があり、この関数を介して実行した場合は、なんらかのエラーが発生してもScriptは停止せず、成否と関数の戻り値かエラーメッセージを返します。
RobloxではDataStoreへのアクセスなど、失敗する可能性のある非同期通信処理の際によく利用されます。

さてこのpcall()、Roblox公式のサンプルなどではよく以下のような書き方がされています。

local success, result = pcall(function()
	return Testfunc()
end)

しかしこの書き方、新たに一時的な関数を定義し、その中で実行したい関数を実行し、戻り値をそのまま返す、という書き方になっていて、なんだか無駄が多く感じられます。

もっと無駄のない書き方は無いのかとpcall()のリファレンスを確認してみると

pcall bool
Calls the function func with the given arguments in protected mode. This means that any error inside func is not propagated; instead, pcall() catches the error and returns a status code. Its first result is the status code (a boolean), which is true if the call succeeds without errors. In such case, pcall() also returns all results from the call, after this first result. In case of any error, pcall() returns false plus the error message.

Parameters
func: function
The function to be called in protected mode.
args: Tuple
The arguments to send to func when executing.

Returns
bool
Variant

本来の使用方法としては、第一引数に実行したい関数を、第二引数以降にその関数に渡したい引数を入れればいいようです。つまり、

local success, result = pcall(Testfunc)

でもよい、という事です。
こちらの方が読みやすいし、より高速に動作しそうに見えます。

というわけで、どのくらいパフォーマンスに影響するのか、実際に検証してみました。

バージョン:0.641.0.6410737

1. 検証方法

以下のようなScriptを用意し、検証対象のコメントアウトを外して、5回ずつRoblox Studio上で実行しました。

wait(5)	-- 起動時の処理に影響されないよう一応のWait

-- 何もしない関数を定義.
function Testfunc()
end

local start = os.clock()
for i = 1, 10000000, 1 do
	-- パターンA 直接関数を引数に与える.
	--pcall(Testfunc)
	
	-- パターンB 新たな関数を定義し、その中で関数を呼び出す.
	--pcall(function()
	--	return Testfunc()
	--end)

end

print(os.clock() - start)

2. 結果

回数 A B
1 0.3170745999996143 0.40249299999777577
2 0.32508769999913056 0.4004648999980418
3 0.3027905000017199 0.39884560000064084
4 0.3022718999964127 0.40002219999951194
5 0.30152390000148444 0.4028610000023036
平均 約0.309749 約0.400937

新たな関数を定義しその中で関数を呼び出す場合に比べて、直接関数を引数に与える場合は、およそ3/4の時間で実行できました。
やはり、直接与える方が良いようです。

3. 追加検証

せっかくなので、こんなパターンも検証してみました。

wait(5)	-- 起動時の処理に影響されないよう一応のWait

-- 何もしない関数を定義.
function Testfunc()
end

-- 何もしない関数を呼び出すだけの関数を定義.
function Testfunc2()
	return Testfunc()
end

local start = os.clock()
for i = 1, 10000000, 1 do
	-- パターンC 「何もしない関数を呼び出すだけの関数」を直接引数に与える.
	pcall(Testfunc2)
end

print(os.clock() - start)

4. 追加検証結果

回数 A B C
1 0.3170745999996143 0.40249299999777577 0.3912010000021837
2 0.32508769999913056 0.4004648999980418 0.3987610999975004
3 0.3027905000017199 0.39884560000064084 0.39487219999864465
4 0.3022718999964127 0.40002219999951194 0.4106105999999272
5 0.30152390000148444 0.4028610000023036 0.4039740999978676
平均 約0.309749 約0.400937 約0.399884

パターンCはパターンBとほぼ変わりません。
どうやら、一回余分に関数を介することによるオーバーヘッドが、パフォーマンスの差の主な要因のようです。

5. 実際にRobloxで利用する際の書き方

例えば、DataStoreのGetAsync()pcall()で呼び出す場合、よく見かけるのは

local success, result = pcall(function()
	return dataStore:GetAsync(key)
end)

という書き方です。
GetAsync()の前の:は、テーブルのメソッドを呼び出す際の第一引数にテーブル自身を与えることを表す省略記法ですから、以下のように書き直すことができます。

local success, result = pcall(dataStore.GetAsync, dataStore, key)

6. まとめ

  • pcall()に与える関数を、その場で定義した関数の中で呼び出すと、パフォーマンスが悪化する
  • 関数を一回余分に呼び出すオーバーヘッドが原因

今回はpcall()を題材にしましたが、関数呼び出しのオーバーヘッドはあらゆる場面に関係することです。
可読性や保守性等と相談しつつ、不要な関数呼び出しは省き、普段からパフォーマンスを意識して制作を行いましょう!

7. 参考

https://create.roblox.com/docs/reference/engine/globals/LuaGlobals#pcall

ランド・ホー Roblox開発チーム

Discussion