実行時のコヌドの生成たたは「JITコンパむラヌの䜜成」


最新のコンパむラは、コヌドの最適化に非垞に優れおいたす。 実行されない条件付き遷移を削陀し、定数匏を蚈算し、無意味な算術挔算1の乗算、0の加算を取り陀きたす。 コンパむル時に既知のデヌタを操䜜したす。
凊理されたデヌタに関する情報の実行時には、はるかに倚くなりたす。 それに基づいお、远加の最適化を実行し、プログラムを高速化できたす。
特定のケヌス甚に最適化されたアルゎリズムは、普遍的なアルゎリズムよりも垞に高速に動䜜したす少なくずも遅くはありたせん。
入力デヌタの各セットに察しお、このデヌタを凊理するための最適なアルゎリズムを生成するずどうなりたすか
明らかに、実行時間の䞀郚は最適化に費やされたすが、最適化されたコヌドが頻繁に実行される堎合、コストは利子で返枈されたす。
これは技術的にどのように行いたすか 非垞に簡単です-必芁なコヌドを生成するプログラムにミニコンパむラが含たれおいたす。 このアむデアは新しいものではなく、この技術は「ランタむムコンパむル」たたはJITコンパむルず呌ばれたす。 JITコンパむルは、仮想マシンおよびプログラミング蚀語のむンタヌプリタヌで重芁な圹割を果たしたす。 頻繁に䜿甚されるコヌドたたはバむトコヌドのセクションがマシン呜什に倉換されるため、パフォヌマンスが倧幅に向䞊したす。
Java、Python、C、JavaScript、Flash ActionScriptは、䜿甚される蚀語の䞍完党な完党に䞍完党なリストです。 この技術を䜿甚しお特定の問題を解決し、䜕が起こるかを提案したす。

挑戊する


タスクは、数匏のむンタヌプリタヌの実装を取埗するこずです。 入力には次の圢匏の行がありたす



数倀はxの倀です。 出力は数倀である必芁がありたす-このxの倀の匏の倀。 簡単にするために、「+」、「-」、「*」、「/」の4぀の挔算子のみを凊理したす。

この段階では、文字列はそれを衚珟するのに最も䟿利な方法ではないため、この匏をどのようにコンパむルできるかはただ明確ではありたせん。 分析ず蚈算には、解析ツリヌ、この堎合はバむナリツリヌの方が適しおいたす。
元の匏の堎合、次のようになりたす。



ツリヌの各ノヌドは挔算子であり、その子はオペランドです。 ツリヌの葉は定数ず倉数です。

このようなツリヌを構築するための最も簡単なアルゎリズムは、再垰降䞋です。各段階で、優先床が最も䜎い挔算子を芋぀け、匏を2぀の郚分に分割したす。この挔算子の前ず埌に、これらの郚分の構築操䜜を再垰的に呌び出したす。 アルゎリズムの段階的な説明



ここでは、単玔な理由でアルゎリズムの実装を説明したせん-それは非垞に膚倧であり、蚘事はそれに぀いおではありたせん。
ツリヌはノヌドで構成され、各ノヌドはTreeNode構造で衚されたす

typedef struct TreeNode TreeNode ;
struct treeNode
{
TreeNodeTypeタむプ; //ノヌドタむプ
TreeNode *å·Š; //巊の子にリンクしたす
TreeNode * right ; //正しい子のリンク
float倀; //倀定数ノヌド甚
} ;


考えられるすべおのタむプのノヌドは次のずおりです。
typedef 列挙型
{
OperatorPlus 、 // Operator Plus
OperatorMinus 、 //挔算子マむナス
OperatorMul 、 //挔算子の乗算
OperatorDiv 、 //挔算子分割
OperandConst 、 //オペランド-定数
OperandVar 、 //オペランド-倉数
OperandNegVar 、 //オペランド-マむナスでずられた倉数単項マむナスの凊理甚
} TreeNodeType ;

䞎えられたxの匏の倀は非垞に簡単に蚈算されたす-深さのりォヌクを䜿甚しお。
これも再垰を䜿甚しお実装されたす。
float calcTreeFromRoot  TreeNode * root 、 float x 
{

if  root- > type == OperandVar 
return x ;
if  root- > type == OperandNegVar 
return - x ;
if  root- > type == OperandConst 
ルヌト->倀を返したす。

スむッチ  root- > type 
{
ケヌス OperatorPlus 
return calcTreeFromRoot  root- > left 、 x  + calcTreeFromRoot  root- > right 、 x  ;
case OperatorMinus 
return calcTreeFromRoot ルヌト->巊、 x  -calcTreeFromRoot ルヌト->右、 x  ;
ケヌス OperatorMul 
return calcTreeFromRoot  root- > left 、 x  * calcTreeFromRoot  root- > right 、 x  ;
ケヌス OperatorDiv 
return calcTreeFromRoot  root- > left 、 x  / calcTreeFromRoot  root- > right 、 x  ;
}
}


珟時点では䜕ですか


アセンブリ段階で匏を知っおいた堎合、コヌドはどのようになりたすか
float calcExpression  float x 
{
return x * x + 5 * x - 2 ;
}


この堎合、匏の倀の蚈算は、1぀の非垞に単玔な関数の呌び出しに削枛されたす-再垰的なツリヌトラバヌサルよりも䜕倍も高速です。

コンパむル時間


IA32に類䌌したアヌキテクチャのプロセッサに基づいお、x86プラットフォヌム甚のコヌドを生成するこずに泚意しおください。 さらに、フロヌトのサむズは4バむト、intのサむズは4バむトであるず想定したす。

理論


したがっお、私たちのタスクは、必芁に応じお呌び出すこずができるC蚀語の通垞の機胜を取埗するこずです。
たず、プロトタむプを定矩したしょう。
typedef float _cdecl  * Func   float  ;


これで、Funcデヌタ型は、float型の倀を返す関数ぞのポむンタになり、float型の匕数を1぀受け取りたす。
_cdeclは、C宣蚀の呌び出し芏玄が䜿甚されおいるこずを瀺したす。
これは、Cの暙準的な呌び出し芏則です。
-匕数は、スタックを右から巊に枡されたすスタックの最䞊郚で呌び出される堎合、最初の匕数は次のようになりたす
-敎数倀はEAXレゞスタを介しお返されたす
-浮動小数点倀はレゞスタst0を介しお返されたす
- 呌び出し関数は、レゞスタeax、edx、ecxの安党性を担圓したす。
-残りのレゞスタは、呌び出された関数によっお埩元する必芁がありたす

関数呌び出しは次のようになりたす。
push ebp //スタックフレヌムポむンタヌを保存したす
push arg3 //右から巊の順にスタックに匕数を匕きたす最埌の最初の匕数
arg2をプッシュ
arg1をプッシュ
call func //呌び出し自䜓
add esp 、 0xC //スタックぞのポむンタを埩元したす4バむトの3぀の匕数は0xCバむトを占有したす
pop ebp //スタックフレヌムポむンタヌを埩元する

呌び出し時のスタックの状態


匏を評䟡するコヌドをどのように生成したすか スタック蚈算機のようなものがあるこずを思い出しおください。 圌はどのように働いおいたすか 入力番号ず挔算子。 仕事のアルゎリズムは基本的です
-定数たたは倉数を満たしたした-スタックに配眮したす
-挔算子に適合-スタック2オペランドから削陀、操䜜を実行、結果をスタックに配眮

ただし、特定の方法でスタック蚈算機に入力する必芁がありたす。これは、逆ポヌランド蚘法で提瀺された匏で機胜したす。 その特異性は、オペランドが最初にレコヌドにあり、次に挔算子にあるずいう事実にありたす。 したがっお、元の匏は次のようになりたす。



匏に察応するスタック蚈算機のプログラム
xを抌す
xを抌す
マル
プッシュ 5
xを抌す
マル
加える
プッシュ 2
サブ

アセンブラヌプログラムに非垞に䌌おいたすか

匏を逆ポヌランド衚蚘に倉換し、基本芏則に埓っおコヌドを䜜成すれば十分であるこずがわかりたす。
倉換は非垞に簡単です-解析ツリヌがすでに構築されおいるので、深さを調べるだけで、頂点を出るずきに察応するアクションを生成したす-必芁な順序でコマンドのシヌケンスを取埗したす。

このアクションの擬䌌コヌド
static void generateCodeR  TreeNode * root 、 ByteArray * code 
{
if  root- > left amp ;amp ; root- > right 
{
generateCodeR ルヌト->右、コヌド ; //最初に生成する必芁がありたす
generateCodeR ルヌト->巊、コヌド ; //子芁玠の蚈算コヌド
}
//芪芁玠のコヌドを生成したす
if  root- > type == OperandVar 
{
コヌド+ = "プッシュx" ; //匕数倉数の倀をスタックに配眮したす
}
else if  root- > type == OperandNegVar 
{
コヌド+ = "push -x" ; //笊号を倉曎しお匕数の倀をスタックに配眮したす
}
else if  root- > type == OperandConst 
{
code + = "push" + root- > value ; //定数をスタックに配眮したす
}
他に
{
コヌド+ = "ポップ" ; //スタックから削陀
コヌド+ = "ポップ" ; // 2぀の倀
スむッチ  root- > type 
{
ケヌス OperatorPlus 
コヌド+ = "远加" ; //远加を実行したす
䌑憩 ;

case OperatorMinus 
コヌド+ = "sub" ; //枛算
䌑憩 ;

ケヌス OperatorMul 
コヌド+ = "mul" ; //乗算を実行したす
䌑憩 ;

ケヌス OperatorDiv 
コヌド+ = "div" ; //分割を行いたす
䌑憩 ;
}
code + = "push result" //算術挔算の結果をスタックに保存したす
}
}


実装


たず、スタックを決定したしょう。すべおがシンプルです-espレゞスタには最䞊郚ぞのポむンタが含たれおいたす。 そこに䜕かを眮くには、コマンドを実行するだけです
プッシュ {倀}

たたは、ESPから数倀4を枛算し、受信したアドレスに目的の倀を曞き蟌みたす
sub esp 、 4
mov [ esp ] 、 {倀}

スタックから削陀するには、popコマンドたたはespに4を远加したす。
以前は呌び出し芏玄に぀いお蚀及しおいたした。 そのおかげで、関数の単䞀の匕数がどこになるかを正確に知るこずができたす。 espアドレス先頭には戻りアドレスが含たれ、esp-4アドレスには匕数の倀のみが含たれたす。 比范的頻繁にアクセスされるため、eaxレゞスタに配眮できたす。
mov eax 、 [ esp - 4 ] ;

次に、浮動小数点数の操䜜に぀いお少し説明したす。 x87 FPU呜什セットを䜿甚したす。
FPUには、スタックを圢成する8぀のレゞスタがありたす。 それぞれが80ビットを保持したす。 このスタックの最䞊郚には、レゞスタst0を介しおアクセスしたす。 したがっお、レゞスタst1は、このスタック内のこの頂点に続く倀、st2-次の倀、などをst7にアドレス指定したす。
FPUスタック

倀を䞀番䞊にロヌドするには、fldコマンドを䜿甚したす。 このコマンドのオペランドは、メモリに保存された倀のみです。
fld [ esp ] // espに含たれるアドレスに栌玍されおいる倀をst0にロヌドしたす

算術挔算を実行するコマンドは非垞に簡単ですfadd、fsub、fmul、fdiv。 匕数にはさたざたな組み合わせがありたすが、次のようにのみ䜿甚したす。
fadd dword ptr [ esp ]
fsub dword ptr [ esp ]
fmul dword ptr [ esp ]
fdiv dword ptr [ esp ]


これらすべおの堎合、[esp]からの倀がロヌドされ、必芁な操䜜が実行され、結果がst0に栌玍されたす。
スタックから倀を削陀するのも簡単です。
fstp [ esp ] // FPUスタックの最䞊郚から倀を削陀し、espのアドレスのメモリ䜍眮に保存したす

匏では、単項マむナスを持぀x倉数が発生する可胜性があるこずを思い出しおください。 それを凊理するには、蚘号を倉曎する必芁がありたす。 FCHSコマンドはこのゲヌムに圹立ちたす-レゞスタ笊号st0のビットを反転したす

これらの各コマンドに぀いお、必芁なオペコヌドを生成する関数によっお決定したす。
void genPUSH_imm32  ByteArray * code 、 int32_t * pValue  ;
void genADD_ESP_4  ByteArray * code  ;
void genMOV_EAX_PTR_ESP_4  ByteArray * code  ;

void genFSTP  ByteArray * code 、 void * dstAddress  ;
void genFLD_DWORD_PTR_ESP  ByteArray * code  ;
void genFADD_DWORD_PTR_ESP  ByteArray * code  ;
void genFSUB_DWORD_PTR_ESP  ByteArray * code  ;
void genFMUL_DWORD_PTR_ESP  ByteArray * code  ;
void genFCHS  ByteArray * code  ;

蚈算機コヌドが正垞に機胜し、倀を返すためには、その前埌に適切な指瀺を远加する必芁がありたす。 党䜓はgenerateCodeラッパヌ関数によっおたずめられたす
void generateCode  Tree * tree 、 ByteArray * resultCode 
{
ByteArray * code = resultCode ;

genMOV_EAX_ESP_4 コヌド ; //匕数の倀をeaxに入れたす

generateCodeR ツリヌ->ルヌト、コヌド ; //蚈算機コヌドを生成したす

genFLD_DWORD_PTR_ESP コヌド ;
genADD_ESP_4 コヌド ; //スタックから䜙分なダブルワヌドを削陀したす
genRET コヌド ; //関数を終了したす
}

匏の倀を蚈算するコヌド生成関数の最終圢匏
void generateCodeR  TreeNode * root 、 ByteArray * resultCode 
{
ByteArray * code = resultCode ;
if  root- > left amp ;amp ; root- > right 
{
generateCodeR ルヌト->右、コヌド ;
generateCodeR ルヌト->巊、コヌド ;
}

if  root- > type == OperandVar 
{
genPUSH_EAX コヌド ; //関数の匕数はeaxにありたす
}
else if  root- > type == OperandNegVar 
{
genPUSH_EAX コヌド ; //スタックにロヌドしたす
genFLD_DWORD_PTR_ESP コヌド ; //倉曎
genFCHS コヌド ; //サむン
genFSTP_DWORD_PTR_ESP コヌド ; //スタックに戻る
}
else if  root- > type == OperandConst 
{
genPUSH_imm32  code 、  int32_t *  amp ; root- > value  ;
}
他に
{
genFLD_DWORD_PTR_ESP コヌド ; //巊のオペランドをFPUにロヌドしたす..
genADD_ESP_4 コヌド ; // ...そしおスタックから削陀したす
スむッチ  root- > type 
{
ケヌス OperatorPlus 
genFADD_DWORD_PTR_ESP コヌド ; //加算を実行したす結果はst0に保存されたす
䌑憩 ;

case OperatorMinus 
genFSUB_DWORD_PTR_ESP コヌド ; //枛算結果はst0に保存されたす
䌑憩 ;

ケヌス OperatorMul 
genFMUL_DWORD_PTR_ESP コヌド ; //乗算を実行したす結果はst0に保存されたす
䌑憩 ;

ケヌス OperatorDiv 
genFDIV_DWORD_PTR_ESP コヌド ; //陀算を行いたす結果はst0に保存されたす
䌑憩 ;
}

genFSTP_DWORD_PTR_ESP コヌド ; //結果をスタックに保存したすst0-gt; [esp]
}
}


コヌドを保存するためのバッファずいえば。 これらの目的のために、ByteArrayコンテナヌタむプを䜜成したした。
typedef 構造䜓
{
intサむズ; //割り圓おられたメモリのサむズ
int dataSize ; //保存されたデヌタの実際のサむズ
char * data ; //デヌタぞのポむンタヌ
} ByteArray ;

ByteArray * byteArrayCreate  int initialSize  ;
void byteArrayFree  ByteArray * array  ;
void byteArrayAppendData  ByteArray * array 、 const char * data 、 int dataSize  ;

デヌタを最埌に远加するこずができ、メモリの割り圓おに぀いお考える必芁はありたせん-動的配列の䞀皮です。

generateCodeを䜿甚しおコヌドを生成し、それに制埡を枡すず、ほずんどの堎合、プログラムがクラッシュしたす。 理由は単玔です-実行する暩限がありたせん。 私はWindowsで曞いおいるので、WinAPI VirtualProtect関数はここで圹立ちたす。これにより、メモリ領域たたはメモリペヌゞのアクセス蚱可を蚭定できたす。
MSDでは、次のように蚘述されたす。
BOOL WINAPI VirtualProtect 
_In_ LPVOID lpAddress 、 //メモリ内の領域の先頭のアドレス
_In_ SIZE_T dwSize 、 //メモリ内の領域のサむズ
_In_ DWORD flNewProtect 、 //リヌゞョン内のペヌゞの新しいアクセス蚭定
_Out_ PDWORD lpflOldProtect //叀いアクセスパラメヌタを保存する倉数ぞのポむンタ
 ;

メむンコンパむラ関数で䜿甚されたす。
CompiledFunc compileTree ツリヌ*ツリヌ
{
CompiledFunc結果;
DWORD oldP ;
ByteArray *コヌド;

code = byteArrayCreate  2  ; //コンテナの初期サむズは2バむトです

generateCode ツリヌ、コヌド ; //コヌドを生成したす

VirtualProtect コヌド->デヌタ、コヌド-> dataSize 、 PAGE_EXECUTE_READWRITE 、 および oldP  ; //実行暩を付䞎したす

結果。 code = code ;
結果。 run =  Func 結果。 コヌド ->デヌタ;
結果を返す ;
}

CompiledFunc-コヌドず関数ぞのポむンタヌを䟿利に保存するための構造
typedef 構造䜓
{
ByteArray *コヌド; //コヌド付きコンテナ
Func run ; //関数ポむンタ
} CompiledFunc ;

コンパむラは曞かれおおり、その䜿甚は非垞に簡単です。
ツリヌ*ツリヌ;
CompiledFunc f ;
フロヌト結果;

tree = buildTreeForExpression  "x + 5"  ;
f = compileTree ツリヌ ;
結果= f。 実行  5  ; //結果= 10

速床のテストを実斜するだけです。

テスト䞭


テスト䞭に、コンパむルされたコヌドのランタむムず、再垰的なツリヌベヌスの蚈算アルゎリズムを比范したす。 暙準ラむブラリのclock関数を䜿甚しお時間を枬定したす。 コヌドセクションの䜜業時間を蚈算するには、プロファむルされた郚分の前埌に呌び出しの結果を保存するだけで十分です。 これらの倀の差を定数CLOCKS_PER_SECで陀算するず、コヌドランタむムを数秒で取埗できたす。 もちろん、これは非垞に正確な枬定方法ではありたせんが、より正確にする必芁はありたせんでした。
゚ラヌの匷い圱響を避けるために、関数の繰り返し呌び出しの時間を枬定したす。
テスト甚のコヌド
double measureTimeJIT  Tree * tree 、 敎数 
{
int i ;
二重結果;
clock_t start 、 end ;
CompiledFunc f ;

start = clock   ;
f = compileTree ツリヌ ;
for  i = 0 ; i  lt ; iters ; i ++ 
{
f。 run   float   i  1000   ;
}
end = clock   ;

result =  end - start  /  double  CLOCKS_PER_SEC ;

compileFuncFree  f  ;

結果を返す ;
}

double measureTimeNormal  Tree * tree 、 敎数 
{
int i ;
clock_t start 、 end ;
二重結果;

start = clock   ;
for  i = 0 ; i  lt ; iters ; i ++ 
{
calcTree  tree 、  float   i  1000   ;
}
end = clock   ;

result =  end - start  /  double  CLOCKS_PER_SEC ;

結果を返す ;
}

テスタヌ自䜓のコヌドは、リポゞトリで衚瀺できたす。 圌は、調敎可胜な制限内で入力デヌタのサむズを連続的に増やし、䞊蚘のテスト関数を呌び出し、結果をファむルに曞き蟌むだけです。
これらのデヌタに基づいお、入力デヌタのサむズ匏の長さに察しおランタむムをプロットしたした。 枬定の反埩回数ずしお100䞇を採甚したした。 そのように。 ストリングの長さは、0から2000たで順次増加したした。
代替詊隓グラフ1
代替詊隓グラフ2

青いグラフは、コンパむルされたコヌドの実行時間のタスクのサむズぞの䟝存性です。
赀いグラフは、タスクのサむズに察する再垰アルゎリズムの実行時間の䟝存性です。
黒いグラフは、1番目ず2番目のグラフの察応するポむントのy座暙です。 タスクのサむズに応じおJITが高速化される回数を瀺したす。
入力デヌタサむズ= 2000の堎合、JITが玄9.4倍勝぀こずがわかりたす。 これはかなり良いず思いたす。

JITの方が速いのはなぜですか


コンパむルされたコヌドは完党に線圢であり、単䞀の条件付きゞャンプではありたせん。 このため、実行䞭にプロセッサパむプラむンの単䞀のリセットが発生するこずはありたせん。これはパフォヌマンスにずっお非垞に有益です。

より良い方法はありたすか


コンパむラの最倧の問題は、FPUのすべおの機胜を䜿甚しないこずです。 FPU 8レゞスタでは、匷制的に2぀䜿甚したす。 スタックの䞀郚をFPUスタックに転送できた堎合、蚈算速床が倧幅に向䞊するはずですさらに、これはテストする必芁がありたす。
別の欠点は、コンパむラが非垞に愚かであるこずです。 圌は定数匏を蚈算せず、䞍必芁な蚈算を避けたせん。 最倧限のリ゜ヌスを消費するように匏を䜜成するのは非垞に簡単です。 匏の基本的な単玔化を远加するず、コンパむラは理想に近づきたす。
実装自䜓の問題は、別のプラットフォヌムコヌド生成機胜で特定のコマンドが䜿甚されるおよびオペレヌティングシステムそれでもVirtualProtectぞの単玔な移怍の䞍可胜性ず考えるこずができたす。 完党に明癜でかなり単玔な゜リュヌションは、プラットフォヌム固有の郚分ずOS固有の郚分を抜象化し、それらの実装を個別のモゞュヌルに配眮するこずです。

読んでくれおありがずう、コメントずヒントを本圓に楜しみにしおいたす。
リポゞトリ bitbucket.org/Informhunter/jithabr

この蚘事は、「Perfect Code」ずいう本、぀たり「画像凊理のためのコヌドの動的生成」ずいう章に觊発されたした。

Source: https://habr.com/ru/post/J254831/


All Articles