Lua性能最適化技術(二):基本事実
2806 ワード
どのコードを実行する前に、Luaはソースコードを内部フォーマットに翻訳(プリコンパイル)します.このフォーマットは、実際のCPUで実行されるマシンコードと同様の仮想マシン命令シーケンスです.その後、この内部フォーマットは、巨大なswitch構造を含むwhileサイクルからなるCコード解釈によって実行され、switchの各caseは命令に対応する.
5.0版からLuaがレジスタベースの仮想マシンを使用していることを他の場所で知っているかもしれません.ここでいう仮想マシン「レジスタ」は、後者が移植しにくく、数が限られているため、本物のCPUレジスタとは異なる.Luaは、レジスタを提供するために1つのスタック(配列および複数のインデックスによって実現される)を使用する.各アクティブな関数には、アクティブなレコード、すなわちスタック上の関数がレジスタを格納できるセグメントがあります.したがって、各関数には独自のレジスタがある[1].1つの関数は、各命令が1つのレジスタを参照するために8ビットしかないため、最大250個のレジスタを使用することができる.
レジスタ数が多いため,Luaプリコンパイラはすべての局所変数をレジスタに保存することができる.これにより、ローカル変数へのアクセスが非常に速くなるという利点があります.たとえば、aとbがローカル変数である場合、文
コマンドは1つだけ生成されます.
(aとbはレジスタにそれぞれ0と1に対応すると仮定する).対照的に、aとbがグローバル変数である場合、このコードは次のようになります.
したがって,Luaプログラミングにおいて最も重要な性能最適化方式として局所変数を用いることが簡単に得られる.
プログラムの性能を搾取したい場合は、この方法を使用できるところがたくさんあります.たとえば、長いループで関数を呼び出す場合は、この関数をローカル変数に事前に割り当てることができます.たとえば、次のコードがあります.
次のセクションより30%遅いです.
外部ローカル変数へのアクセス(または、関数の上の値)は、ローカル変数への直接アクセスほど速くはありませんが、グローバル変数へのアクセスよりも高速です.たとえば、次のコードクリップがあります.
fooの外でsinを宣言するように最適化できます.
2番目のセグメントコードは前者より30%速い.
他の言語のコンパイラに比べてLuaのコンパイラは非常に効率的であるが,コンパイラは依然として重労働である.したがって、ユーザーが入力したコードなど、このような動的に要求されるコードが本当に必要でない限り、実行時のコンパイル(loadstring関数の使用など)はできるだけ避けるべきです.コードを動的にコンパイルする必要があるのは、わずかな場合だけです.
たとえば、次のコードは、定数1~100000を返すいくつかの関数を含むテーブルを作成します.
このコードを実行するには1.4秒かかります.
閉パッケージを使用することで、動的コンパイルを回避できます.次のコードは、同じ作業を完了するのに10分の1の時間しかかかりません.
5.0版からLuaがレジスタベースの仮想マシンを使用していることを他の場所で知っているかもしれません.ここでいう仮想マシン「レジスタ」は、後者が移植しにくく、数が限られているため、本物のCPUレジスタとは異なる.Luaは、レジスタを提供するために1つのスタック(配列および複数のインデックスによって実現される)を使用する.各アクティブな関数には、アクティブなレコード、すなわちスタック上の関数がレジスタを格納できるセグメントがあります.したがって、各関数には独自のレジスタがある[1].1つの関数は、各命令が1つのレジスタを参照するために8ビットしかないため、最大250個のレジスタを使用することができる.
レジスタ数が多いため,Luaプリコンパイラはすべての局所変数をレジスタに保存することができる.これにより、ローカル変数へのアクセスが非常に速くなるという利点があります.たとえば、aとbがローカル変数である場合、文
a = a + b
コマンドは1つだけ生成されます.
ADD 0 0 1
(aとbはレジスタにそれぞれ0と1に対応すると仮定する).対照的に、aとbがグローバル変数である場合、このコードは次のようになります.
GETGLOBAL 0 0 ; a
GETGLOBAL 1 1 ; b
ADD 0 0 1
SETGLOBAL 0 0 ; a
したがって,Luaプログラミングにおいて最も重要な性能最適化方式として局所変数を用いることが簡単に得られる.
プログラムの性能を搾取したい場合は、この方法を使用できるところがたくさんあります.たとえば、長いループで関数を呼び出す場合は、この関数をローカル変数に事前に割り当てることができます.たとえば、次のコードがあります.
for i = 1, 1000000 do
local x = math.sin(i)
end
次のセクションより30%遅いです.
local sin = math.sin
for i = 1, 1000000 do
local x = sin(i)
end
外部ローカル変数へのアクセス(または、関数の上の値)は、ローカル変数への直接アクセスほど速くはありませんが、グローバル変数へのアクセスよりも高速です.たとえば、次のコードクリップがあります.
function foo (x)
for i = 1, 1000000 do
x = x + math.sin(i)
end
return x
end
print(foo(10))
fooの外でsinを宣言するように最適化できます.
local sin = math.sin
function foo (x)
for i = 1, 1000000 do
x = x + sin(i)
end
return x
end
print(foo(10))
2番目のセグメントコードは前者より30%速い.
他の言語のコンパイラに比べてLuaのコンパイラは非常に効率的であるが,コンパイラは依然として重労働である.したがって、ユーザーが入力したコードなど、このような動的に要求されるコードが本当に必要でない限り、実行時のコンパイル(loadstring関数の使用など)はできるだけ避けるべきです.コードを動的にコンパイルする必要があるのは、わずかな場合だけです.
たとえば、次のコードは、定数1~100000を返すいくつかの関数を含むテーブルを作成します.
local lim = 10000
local a = {}
for i = 1, lim do
a[i] = loadstring(string.format("return %d", i))
end
print(a[10]()) --> 10
このコードを実行するには1.4秒かかります.
閉パッケージを使用することで、動的コンパイルを回避できます.次のコードは、同じ作業を完了するのに10分の1の時間しかかかりません.
function fk (k)
return function () return k end
end
local lim = 100000
local a = {}
for i = 1, lim do a[i] = fk(i) end
print(a[10]()) --> 10