Luaプログラムのパフォーマンスを向上させるにはどうすればよいですか?
-
03-07-2019 - |
質問
Luaのパフォーマンスについて、および回答の質問:
Luaのパフォーマンスを高く保つための一般的なヒントを学びましたか?つまり、テーブルの作成を知っており、新しいテーブルを作成するよりもテーブルを再利用し、「local print = print」などを使用してグローバルアクセスを回避します。
これは Luaパターン、ヒント、トリックとは少し異なる質問です。パフォーマンスに特に影響を与える回答や、(可能であれば)パフォーマンスに影響を与える理由の説明など。
回答ごとに1つのヒントが理想的です。
解決
他の回答やコメントへの回答:
プログラマーとして、一般的に時期尚早な最適化を避けるべきであることは事実です。 しかし。これは、コンパイラがあまり最適化しない、またはまったく最適化しないスクリプト言語にはあまり当てはまりません。
だから、Luaで何かを書いて、それが非常に頻繁に実行され、タイムクリティカルな環境で実行されるか、しばらく実行される可能性があるときはいつでも、避ける(およびそれらを避けてください)。
これは、私が時とともに見つけたもののコレクションです。ネット上で見つけたものもありますが、インターウェブが懸念される場合は疑わしい性質であるため、すべて自分でテストしました。また、Lua.orgでLuaパフォーマンスペーパーを読みました。
一部の参照:
グローバルを避ける
これは最も一般的なヒントの1つですが、もう一度説明しても害はありません。
グローバルは、名前でハッシュテーブルに保存されます。それらにアクセスするには、テーブルインデックスにアクセスする必要があります。 Luaにはかなり良いハッシュテーブルの実装がありますが、ローカル変数にアクセスするよりもずっと遅いです。グローバルを使用する必要がある場合、その値をローカル変数に割り当てます。これは、2番目の変数アクセスで高速です。
do
x = gFoo + gFoo;
end
do -- this actually performs better.
local lFoo = gFoo;
x = lFoo + lFoo;
end
(単純なテストでは異なる結果が得られることはありません。たとえば、 local x; for i = 1、1000 do x = i; end
ここでforループヘッダーは実際にはループ本体よりも時間がかかります、したがって、プロファイリングの結果が歪む可能性があります。)
文字列の作成を避ける
Luaは作成時にすべての文字列をハッシュします。これにより、すべての文字列が内部で1回のみ保存されるため、テーブルでの比較と使用が非常に高速になり、メモリ使用量が削減されます。ただし、文字列の作成はより高価になります。
過剰な文字列の作成を避けるための一般的なオプションは、テーブルを使用することです。たとえば、長い文字列を組み立てる必要がある場合は、テーブルを作成し、個々の文字列をそこに入れてから、 table.concat
を使用して once
-- do NOT do something like this
local ret = "";
for i=1, C do
ret = ret..foo();
end
foo()
が文字 A
のみを返す場合、このループは""
、< code>&quot; A&quot; 、&quot; AA&quot;
、&quot; AAA&quot;
など。各文字列はハッシュされ、アプリケーションが終了するまでメモリに常駐します。 -ここに問題がありますか?
-- this is a lot faster
local ret = {};
for i=1, C do
ret[#ret+1] = foo();
end
ret = table.concat(ret);
このメソッドはループ中に文字列をまったく作成せず、文字列は関数 foo
で作成され、参照のみがテーブルにコピーされます。その後、concatは2番目の文字列&quot; AAAAAA ...&quot;
を作成します( C
の大きさによって異なります)。 #ret + 1
の代わりに i
を使用することができますが、多くの場合、このような便利なループがなく、使用できないことに注意してください。使用できるイテレータ変数。
lua-users.orgのどこかで見つけた別のトリックは、文字列を解析する必要がある場合にgsubを使用することです
some_string:gsub(".", function(m)
return "A";
end);
これは最初は奇妙に見えますが、利点はgsubが文字列「一度に」を作成することです。 Cでは、gsubが戻ったときにluaに戻された後にのみハッシュされます。これにより、テーブルの作成が回避されますが、関数のオーバーヘッドが増える可能性があります(とにかく foo()
を呼び出す場合ではなく、 foo()
が実際に式の場合)
関数のオーバーヘッドを回避
可能な場合、関数の代わりに言語構造を使用します
関数 ipairs
テーブルを反復するとき、ipairsからの関数オーバーヘッドはその使用を正当化しません。テーブルを反復するには、代わりに
を使用しますfor k=1, #tbl do local v = tbl[k];
関数呼び出しのオーバーヘッドなしでまったく同じことを行います(ペアは実際に別の関数を返し、テーブルのすべての要素に対して呼び出されますが、 #tbl
は一度だけ評価されます)。 yであってもずっと速い値が必要です。そうしないと...
Lua 5.2の注:5.2では、メタテーブルに __ ipairs
フィールドを実際に定義できます。これは does ipairs
は、場合によっては便利です。ただし、Lua 5.2では __ len
フィールドもテーブルで機能するため、上記のコードを ipairs
よりも still 好む場合があります。 __len メタメソッドは1回だけ呼び出されますが、 ipairs
の場合、反復ごとに追加の関数呼び出しが行われます。
関数 table.insert
、 table.remove
table.insert
および table.remove
の単純な使用は、代わりに#
演算子を使用して置き換えることができます。基本的に、これは単純なプッシュおよびポップ操作用です。以下に例を示します。
table.insert(foo, bar);
-- does the same as
foo[#foo+1] = bar;
local x = table.remove(foo);
-- does the same as
local x = foo[#foo];
foo[#foo] = nil;
シフト(例: table.remove(foo、1)
)で、スパーステーブルで終わることが望ましくない場合は、もちろんテーブル関数を使用することをお勧めします。
SQL-INの比較にテーブルを使用する
次のような決定をコード内で行う場合としない場合があります
if a == "C" or a == "D" or a == "E" or a == "F" then
...
end
これは完全に有効なケースですが、(私自身のテストから)4つの比較から開始し、テーブル生成を除外すると、これは実際に高速になります。
local compares = { C = true, D = true, E = true, F = true };
if compares[a] then
...
end
また、ハッシュテーブルのルックアップ時間は一定であるため、追加の比較を行うたびにパフォーマンスが向上します。一方、「ほとんどの時間」の場合1つまたは2つの比較が一致する場合は、ブール値の方法または組み合わせを使用した方がよい場合があります。
頻繁にテーブルを作成しない
これについては、 Luaパフォーマンスのヒントで詳細に説明されています。基本的に問題は、Luaが必要に応じてテーブルを割り当てることであり、この方法で実行すると、コンテンツをクリーンアップして再入力するよりも実際に時間がかかります。
ただし、Lua自体はテーブルからすべての要素を削除する方法を提供していないため、これは少し問題です。 pairs()
はパフォーマンスビーストそのものではありません。この問題のパフォーマンステストはまだ行っていません。
可能であれば、テーブルをクリアするC関数を定義します。これはテーブルの再利用に適したソリューションです。
同じことを何度も繰り返さないでください
これが最大の問題だと思います。解釈されない言語のコンパイラは、多くの冗長性を簡単に最適化できますが、Luaはそうしません。
メモ
テーブルを使用すると、これはLuaで非常に簡単に実行できます。引数が1つの関数の場合は、テーブルと__indexメタメソッドに置き換えることもできます。これにより透明度が失われますが、関数呼び出しが1つ少ないため、キャッシュされた値のパフォーマンスが向上します。
これは、メタテーブルを使用した単一の引数のメモ化の実装です。 (重要:このバリアントは、nil値の引数をサポートしていません が、既存の値に対しては非常に高速です。)
function tmemoize(func)
return setmetatable({}, {
__index = function(self, k)
local v = func(k);
self[k] = v
return v;
end
});
end
-- usage (does not support nil values!)
local mf = tmemoize(myfunc);
local v = mf[x];
実際には、複数の入力値に対してこのパターンを変更できます
部分アプリケーション
この考え方は、「キャッシュ」するメモ化に似ています。結果。ただし、ここでは、関数の結果をキャッシュする代わりに、ブロック内の計算関数を定義するコンストラクター関数に計算を入れて、中間値をキャッシュします。実際には、クロージャーの巧妙な使用と呼んでいます。
-- Normal function
function foo(a, b, x)
return cheaper_expression(expensive_expression(a,b), x);
end
-- foo(a,b,x1);
-- foo(a,b,x2);
-- ...
-- Partial application
function foo(a, b)
local C = expensive_expression(a,b);
return function(x)
return cheaper_expression(C, x);
end
end
-- local f = foo(a,b);
-- f(x1);
-- f(x2);
-- ...
この方法により、プログラムフローにあまり影響を与えずに、作業の一部をキャッシュする柔軟な関数を簡単に作成できます。
これの極端な変種は Currying ですが、実際には他の何よりも関数型プログラミングを模倣する方法です。
これは、コードを省略した、より広範な(「現実の世界」)例です。それ以外の場合は、簡単にu
他のヒント
luaプログラムが本当に遅すぎる場合は、Luaプロファイラーを使用して高価なものをクリーンアップするか、Cに移行します。
最適化の第一法則:しないでください。
ipairsとpairのどちらかを選択でき、その違いの影響を測定できる問題を見つけたいです。
簡単に実行できる簡単な方法は、各モジュール内でローカル変数を使用することを忘れないことです。一般的に、次のようなことをする価値はありません
local strfind = string.find
そうでないことを示す測定値が見つからない場合。
- 最も使用される関数をローカルに作成する
- ハッシュセットとしてテーブルをうまく利用する
- 再利用によるテーブル作成の削減
- luajit!の使用
テーブルを短くし、テーブルが大きいほど検索時間が長くなります。 また、同じ行で、数値インデックステーブル(=配列)の反復処理は、キーベースのテーブルよりも高速です(したがって、ipairsはペアよりも高速です)
テーブルの配列フィールドを使用することは、あらゆる種類のキーを持つテーブルを使用するよりもはるかに高速であることも指摘する必要があります。 (ほぼ)すべてのLua実装(LuaJを含む)が呼び出された「配列部分」を保存します。テーブル配列フィールドによってアクセスされ、フィールドキーを格納せず、ルックアップもしません;)。
struct
、C ++ / Java class
など、他の言語の静的な側面を模倣することもできます。ローカルと配列で十分です。