web-dev-qa-db-ja.com

Luaプログラムのパフォーマンスを向上させるために何ができますか?

私はLuaのパフォーマンスについて質問し、 responses の質問:

Luaのパフォーマンスを高く保つための一般的なヒントを学びましたか?つまり、テーブルの作成を知っていて、新しいテーブルを作成するのではなくテーブルを再利用し、グローバルアクセスを回避するために「localprint = print」などを使用します。

これは Luaパターン、ヒント、コツ とは少し異なる質問です。パフォーマンスに特に影響を与える回答と、(可能であれば)パフォーマンスが影響を受ける理由の説明が必要だからです。

回答ごとに1つのヒントが理想的です。

24
Jon Ericson

他の回答やコメントのいくつかに応えて:

プログラマーとして、一般的に時期尚早の最適化を避けるべきであることは事実です。しかし。これは、コンパイラがあまり最適化しない、またはまったく最適化しないスクリプト言語には当てはまりません。

したがって、Luaで何かを記述し、それが非常に頻繁に実行される場合、タイムクリティカルな環境で実行される場合、またはしばらく実行される可能性がある場合は、avoidすることを知っておくとよいでしょう。(そしてそれらを避けてください)。

これは私が時間をかけて見つけたもののコレクションです。その一部はネットで見つけましたが、interwebsが懸念される場合は疑わしい性質があるため、すべて自分でテストしました。また、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のみを返す場合、このループは"""A""AA""AAA"などの一連の文字列を作成します。各文字列はハッシュされ、メモリに常駐します。アプリケーションが終了します-ここで問題を参照してください?

-- this is a lot faster
local ret = {};
for i=1, C do
  ret[#ret+1] = foo();
end
ret = table.concat(ret);

このメソッドは、ループ中に文字列をまったく作成しません。文字列は関数fooで作成され、参照のみがテーブルにコピーされます。その後、concatは2番目の文字列"AAAAAA..."を作成します(Cの大きさに応じて)。could#ret+1の代わりにiを使用しますが、多くの場合、そのような便利なループがなく、できるイテレータ変数がないことに注意してください。使用する。

Lua-users.orgのどこかで見つけたもう1つのトリックは、文字列を解析する必要がある場合に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は1回だけ評価されます)。値が必要な場合でも、はるかに高速です。そして、あなたがしなければ...

Lua 5.2に関する注記:5.2では、メタテーブルに__ipairsフィールドを実際に定義できます。これはdoes場合によってはipairsを便利にします。ただし、Lua 5.2では__lenフィールドがテーブルでも機能するため、still上記のコードをipairsよりも優先すると、__lenメタメソッドのみが呼び出されます。一度、ipairsの場合、反復ごとに追加の関数呼び出しを取得します。

関数table.inserttable.remove

table.inserttable.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 Performance Tips で詳しく説明されています。基本的に問題は、Luaがテーブルをオンデマンドで割り当てることです。この方法で行うと、実際には、コンテンツをクリーンアップして再度入力するよりも時間がかかります。

ただし、Lua自体はテーブルからすべての要素を削除するメソッドを提供しておらず、pairs()はパフォーマンスビースト自体ではないため、これは少し問題です。私はまだこの問題のパフォーマンステストを行っていません。

可能であれば、テーブルをクリアするC関数を定義します。これは、テーブルを再利用するための優れたソリューションになるはずです。

同じことを何度も繰り返すことは避けてください

これが最大の問題だと思います。インタープリター型言語のコンパイラーは多くの冗長性を簡単に最適化できますが、Luaはそうしません。

メモ化

テーブルを使用すると、Luaでこれを非常に簡単に行うことができます。単一引数関数の場合、それらをテーブルおよび__indexメタメソッドに置き換えることもできます。これにより透明性が失われますが、関数呼び出しが1つ少なくなるため、キャッシュされた値のパフォーマンスが向上します。

これは、メタテーブルを使用した単一の引数のメモ化の実装です。 (重要:このバリアントはnotnil値引数をサポートしますが、既存の値に対してはかなり高速です。)

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);
-- ...

このようにして、プログラムフローにあまり影響を与えることなく、作業の一部をキャッシュする柔軟な関数を簡単に作成できます。

これの極端な変形は カリー化 ですが、それは実際には他の何よりも関数型プログラミングを模倣する方法です。

これは、いくつかのコードが省略された、より広範な(「実世界」)例です。そうしないと、ここでページ全体を簡単に占めることになります(つまり、get_color_valuesは実際に多くの値チェックを実行し、混合値を受け入れることを認識します)

function LinearColorBlender(col_from, col_to)
    local cfr, cfg, cfb, cfa = get_color_values(col_from);
    local ctr, ctg, ctb, cta = get_color_values(col_to);
    local cdr, cdg, cdb, cda = ctr-cfr, ctg-cfg, ctb-cfb, cta-cfa;
    if not cfr or not ctr then
        error("One of given arguments is not a color.");
    end

    return function(pos)
        if type(pos) ~= "number" then
            error("arg1 (pos) must be in range 0..1");
        end
        if pos < 0 then pos = 0; end;
        if pos > 1 then pos = 1; end;
        return cfr + cdr*pos, cfg + cdg*pos, cfb + cdb*pos, cfa + cda*pos;
    end
end
-- Call 
local blender = LinearColorBlender({1,1,1,1},{0,0,0,1});
object:SetColor(blender(0.1));
object:SetColor(blender(0.3));
object:SetColor(blender(0.7));

ブレンダーが作成されると、関数は最大8つではなく1つの値をサニティチェックするだけでよいことがわかります。差の計算も抽出しましたが、あまり改善されないかもしれませんが、このパターンが何を達成しようとしているのかを示していると思います。

58
dualed

Luaプログラムが本当に遅すぎる場合は、Luaプロファイラーを使用して高価なものをクリーンアップするか、Cに移行してください。しかし、そこに座って待っていない場合は、時間が無駄になります。

最適化の最初の法則:しないでください。

Ipairsとpairsのどちらかを選択でき、その違いの影響を測定できる問題を見つけたいと思います。

簡単な成果の1つは、各モジュール内でローカル変数を使用することを忘れないことです。一般的に、次のようなことをする価値はありません

 local strfind = string.find 

あなたがそうでないことを告げる測定値を見つけることができない限り。

9
Norman Ramsey
  • 最もよく使われる関数をローカルにする
  • HashSetsとしてテーブルをうまく利用する
  • 再利用によるテーブル作成の削減
  • Luajitを使用しています!
4
Kknd

また、テーブルの配列フィールドを使用する方が、任意の種類のキーを持つテーブルを使用するよりもはるかに高速であることも指摘しておく必要があります。 (ほぼ)すべてのLua実装(LuaJを含む)は、テーブル配列フィールドによってアクセスされるテーブル内に呼び出された「配列部分」を格納し、フィールドキーを格納せず、それを検索しません;)。

struct、C++/Java classなどの他の言語の静的な側面を模倣することもできます。ローカルと配列で十分です。

2
Hydroper

テーブルを短くしておくと、テーブルが大きくなるほど検索時間が長くなります。また、同じ行で、数値インデックス付きのテーブル(=配列)を反復処理する方が、キーベースのテーブルよりも高速です(したがって、ipairはペアよりも高速です)

2
Robert Gould