JITコンパイルまたは最適化されたBrainfuckインタープリターの作成について少し

Brainfuck言語の本質は、テープのセルを常に通り抜け、その値を増減させることです。 サイクルでは、多くのネストされたサイクルを使用して、何かを数えて、一方の端から他方の端まで実行できます。 この言語の解釈が比較的遅いと推測することは難しくありません。 もちろん、現代のコンピューターではこれは実質的に目立ちませんが、...ちょっとしたテストをお勧めします:書いたインタープリターを受け取り、このトリッキーなコードを実行します:



>+>+>+>+>++<[>[<+++>- >>>>> >+>+>+>+>++<[>[<+++>- >>>>> >+>+>+>+>++<[>[<+++>- >>>>> >+>+>+>+>++<[>[<+++>- >>>>> +++[->+++++<]>[-]< <<<<< ]<<]>[-] <<<<< ]<<]>[-] <<<<< ]<<]>[-] <<<<< ]<<]>.
      
      







実行の終了を待っていましたか? それはすぐに見えたかもしれないほど速くなかったことに同意します。 さて、数秒以内にこのコードを実行するインタープリターを作成する方法を見てみましょう。



これは、JITコンパイルのアイデアに基づいています。 ウィキペディアによると、JITコンパイルは、プログラムの実行中にバイトコードを直接マシンコードにコンパイルすることにより、バイトコードを使用するソフトウェアシステムのパフォーマンスを向上させる技術です。 少し考えた後、マシンコードの必要な配列全体をすぐに構築し、運用中に置き換えない方がはるかに便利であると判断しました。 これの本質はあまり変わらないと思います。 しかし、順番に取得しましょう。



だから、誰もがすでに知っているように、Brainfuck言語の構文はたった8つの演算子で構成されています。 メモリセルのセットと現在のセルへのポインタがあります。 実際、プログラムの実行の結果として、Brainfuckコードをx86プロセッサーのアセンブラー命令(それらのマシンコード)に変換する必要があります。 これを行うには、アセンブリ言語、およびマシンコード(opcode)を生成するためのルールについてある程度知っておくと便利です。 Brainfuckの各コマンドを1つ以上のx86プロセッサー命令に置き換えます。



まず、メモリセルの配列を作成し、それへのポインタを取得する必要があります。 これは、動的メモリ割り当て機能を使用して実行できます。 そのため、Pascalプログラミング言語(およびサンプルのさらなるコードがその上にあります)では、これは次のように行うことができます。

 Var Stack : Pointer; Begin GetMem(Stack,30000); //    brainfuck - 30000 End;
      
      





次に、Brainfuckのコマンドを処理するための戦略を検討する必要があります。 私は次を使用しました:

セルの配列へのポインタがESIレジスタに格納されることに同意しましょう。

 mov esi, Stack
      
      







その後、残りのチームは次のことを行います。

ブレインファック アセンブラー オペコード
+ incバイト[esi] FE 06
- decバイト[esi] FE 0E
> 株式会社エシ 46
< dec esi 4E
[ cmpバイト[esi]、0 80 3E 00
je "] +1" 0F 84 xx xx xx xxまたは74 xx
] jmp "[" E9 xx xx xx xxまたはEB xx




チームについては「。」 および「、」の場合、シンボル出力手順と水シンボル関数が必要になります。 次のように実装できます。



 procedure WriteChar(S: Char); begin Write(S); end; function ReadChar: Char; var c : char; begin Read(c ); ReadChar := c; end;
      
      





次に、実行可能コードの先頭にあるEDXレジスタに、文字印刷手順のアドレスが含まれていることに同意できます。

 mov edx, offset WriteChar
      
      





EBXレジスターは、文字入力機能のアドレスです。

 mov ebx, offset ReadChar
      
      





次に、「。」を処理します 次のように可能です。

 mov al,[esi] 8A 06 ;  AL    cbw 66 98 ;    EAX push eax 50 ;     () call edx FF D2 ;  WriteChar
      
      





コマンド「、」は次のようになります。

 call ebx FF D3;  ReadChar mov [esi],al 88 06;      
      
      





簡単にするために、静的なExBuf配列を作成します。ここですべてのオペコードを配置し、配列の先頭に制御を移します。 ptr変数は、最後に入力されたオペコードへのポインターになります。

 Var ExBuf : Array [1..65535] of Byte; Ptr : Word; Tmp : LongInt;
      
      





コードブロックの最初の3つのコマンドは、合意したとおり、 ESIレジスタをセルポインターに、 EDXレジスタをWriteCharプロシージャポインターに、 EBXレジスタをReadChar関数ポインターに設定します。 次の方法で実行できます。

  ptr := 1; ExBuf[ptr] := $BE; inc(ptr); // mov esi,Stack asm mov edx,Stack mov Tmp,edx end; Move(Tmp,ExBuf[ptr],4); inc(ptr,4); ExBuf[ptr] := $BA; inc(ptr); // mov edx,offset WriteChar asm mov edx, offset WriteChar mov Tmp, edx end; Move(Tmp,ExBuf[ptr],4); inc(ptr,4); ExBuf[ptr] := $BB; inc(ptr); // mov ebx,offset ReadChar asm mov edx, offset ReadChar mov Tmp, edx end; Move(Tmp,ExBuf[ptr],4); inc(ptr,4);
      
      







すべて、初期化が実行されます。 次に、Brainfuckコード処理ルーチンを作成する必要があります。 最初のチームの場合、次のようになります。

 Procedure JITCode(S: Char); Begin Case S of '+': Begin ExBuf[ptr] := $FE; //inc byte ptr [esi] ExBuf[ptr+1] := $06; Inc(ptr,2); End; '-': Begin ExBuf[ptr] := $FE; //dec byte ptr [esi] ExBuf[ptr+1] := $0E; Inc(ptr,2); End; '>': Begin ExBuf[ptr] := $46; //inc esi Inc(ptr); End; '<': Begin ExBuf[ptr] := $4E; //dec esi Inc(ptr); End; '.': Begin ExBuf[ptr] := $8A; //mov al,[esi] ExBuf[ptr+1] := $06; Inc(ptr,2); ExBuf[ptr] := $66; //cbw ExBuf[ptr+1] := $98; Inc(ptr,2); ExBuf[ptr] := $50; //push eax Inc(ptr); ExBuf[ptr] := $FF; //call edx ExBuf[ptr+1] := $D2; Inc(ptr,2); End; ',': Begin ExBuf[ptr] := $FF; //call ReadChar ExBuf[ptr+1] := $D3; Inc(ptr,2); ExBuf[ptr] := $88; //mov [esi],al ExBuf[ptr+1] := $06; Inc(ptr,2); End; End;
      
      





プロシージャの入力に、Brainfuckコードを交互に渡します。これは、マシンコードに変換され、ExBuf配列に徐々に入力されます。

'['および ']'ループコマンドを使用すると、状況はやや複雑になります。 よく覚えているように、x86プロセッサには複数のアドレッシングモードがあり、それに応じていくつかのニアジャンプまたはショートジャンプコマンドがあります。 したがって、ジャンプ命令とラベルの間のオペコードの合計長が127バイトを超えない場合、ショートジャンプを使用できます。 それ以上の場合は、近い遷移(近い)のチームが既に必要になります。 これを次のように実装しました。「[」コマンドが検出されると、je near xxxコマンドを生成し、コマンドバッファーの現在の位置を追加の配列に格納します。 その後、コードでコマンド ']'が検出されると、ループの開始と終了(バイト長)の差を計算し、それが127バイト未満の場合、保存された位置に戻り、xxxの近くの古いjeをje short xxxで消去し、残りのコードを転送します前方に4バイト。 これを念頭に置いて、上記のJITCode手順を補足する必要があります。

  '[': Begin ExBuf[ptr] := $80; //cmp byte ptr [esi],0 ExBuf[ptr+1] := $3E; ExBuf[ptr+2] := $00; Inc(ptr,3); ExBuf[ptr] := $0F; //je near xxx ExBuf[ptr+1] := $84; Inc(ptr,2); bstack[bcnt] := ptr; inc(bcnt); Inc(ptr,4); End; ']': Begin Tmp := ptr - bstack[bcnt-1]; If Tmp > 122 then begin ExBuf[ptr] := $E9; //jmp Inc(ptr); Inc(Tmp); Move(Tmp,ExBuf[bstack[bcnt-1]],4); Tmp := -Tmp-9; Move(Tmp,ExBuf[ptr],4); Inc(ptr,4); Dec(bcnt); end else begin ExBuf[ptr] := $EB; //jmp short Inc(ptr); ExBuf[bstack[bcnt-1]-2] := $74; // je short xxx Dec(Tmp,2); Move(Tmp,ExBuf[bstack[bcnt-1]-1],1); Tmp := -Tmp-5; Move(Tmp,ExBuf[ptr],1); Inc(ptr); Move(ExBuf[bstack[bcnt-1]+4],ExBuf[bstack[bcnt-1]],ptr-bstack[bcnt-1]-4); Dec(ptr,4); Dec(bcnt); end; End;
      
      





それは事実上すべてであり、既成の実用的な実装です。 あまり残っていません-最後のretコマンド(手順から戻る)で書き留めます。 彼女のオペコードは0xC3です。

  ExBuf[ptr] := $C3; //retn
      
      





そして、制御をマシン命令の生成されたバッファに転送します。

  asm mov edx,offset ExBuf call edx end;
      
      







しかし、プログラムを最適化することで他に何ができるかを見てみましょう。 文字通り表面にあるアイデア:Brainfuckコードでは、同じタイプの繰り返しコマンド、たとえば++++++++++++または<<<<がよく見られます。 現在のセルを11増やすか、セルポインター4を左に動かすとはどういう意味ですか。 以下を示す追加の文字を入力できます。



<<<<< = pで置換#(前)ポインタをすぐに#値だけ左にシフト

>>>>> = nに置き換えられます#(次)ポインタをすぐに#個の値に移動します

+++++ = i#(inc)に置き換えられ、現在のセルをすぐに#に増やします

-= dに置き換え#(dec)現在のセルを#だけデクリメント

[-]、[+] =セル値を0に設定するz(ゼロ)に置き換えられます



このような前処理は、インタープリターの開始時に実行できます。 これにより、Brainfuckのソースコードが大幅に削減され、その結果、x86コードが削減される場合があります。 この処理の後、JITCodeプロシージャを補足するために残ります。

  'i': Begin ExBuf[ptr] := $80; //add byte ptr [esi],xx ExBuf[ptr+1] := $06; ExBuf[ptr+2] := Ord(Buf[i+1]); Inc(i); Inc(ptr,3); End; 'd': Begin ExBuf[ptr] := $80; //sub byte ptr [esi],xx ExBuf[ptr+1] := $2E; ExBuf[ptr+2] := Ord(Buf[i+1]); Inc(i); Inc(ptr,3); End; 'n': Begin ExBuf[ptr] := $83; //add esi,xx ExBuf[ptr+1] := $C6; ExBuf[ptr+2] := Ord(Buf[i+1]); Inc(i); Inc(ptr,3); End; 'p': Begin ExBuf[ptr] := $83; //sub esi,xx ExBuf[ptr+1] := $EE; ExBuf[ptr+2] := Ord(Buf[i+1]); Inc(i); Inc(ptr,3); End; 'z': Begin ExBuf[ptr] := $C6; //mov byte ptr [esi],0 ExBuf[ptr+1] := $06; ExBuf[ptr+2] := $00; Inc(ptr,3); End;
      
      







ソースコードとコンパイル済みのWin32バージョンは、 rghost.ru / 4249797からダウンロードできます。



プログラムにはキーがあります:

-c-プリプロセッサの後に中間バイトコードを含むOUT.BCファイルをディスク上に作成します。

-b-生成されたマシンコードの配列を含むBF.BINファイルをディスク上に作成します。 HIEWで開くと、通常のアセンブラーコマンドが表示されます。



皆さん、頑張ってください。



All Articles