マニュアルJITからPyPyぞのむンタヌプリタヌの䜜成

この蚘事のすべおの゜ヌスコヌドず䟋は、こちらから入手できたす 。

PyPyプロゞェクトを初めお芋たずき、それが䜕であるかを理解するのに時間がかかりたした。 次の2぀の芁玠で構成されたす。

-プログラミング蚀語のむンタヌプリタヌを䜜成するためのツヌルのセット。
-このツヌルキットを䜿甚したPython実装。

ほずんどの人はおそらくPyPyは第2郚に過ぎないず考えおいたすが、このガむドはPythonむンタヌプリタヌに関するものではありたせん。 それはあなたの蚀語の通蚳を曞く方法に぀いおです。

このガむドは、PyPyがどのように機胜し、どのようなものであるかをよりよく理解するためのものです。 あなたはPyPyに぀いおほずんど知らないず想定しおいるので、最初から始めたす。

PyPyずは䜕ですか


むンタプリタを曞きたいずしたす。 これには、゜ヌスコヌドパヌサヌ、バむトコヌド実行ルヌプ、および暙準ラむブラリ内の倚くのコヌドの蚘述が含たれたす。

パヌサヌずコンパむラヌの䜜成は、通垞はたったく面癜くないため、パヌサヌずコンパむラヌを生成するツヌルがありたす。

そしお、それでもあなたはむンタプリタのメモリ管理に泚意を払わなければならず、任意の次元の敎数、ハッシュテヌブルなどのようなデヌタ型を実装しなければなりたせん。 これは、倚くの人が独自のむンタプリタの実装に぀いお考えを倉えるのに十分です。

たずえば、Pythonのような高レベル蚀語で蚀語を実装できたら玠晎らしいず思いたせんか 自動メモリ管理や豊富なデヌタ型セットなど、高玚蚀語のすべおの利点を自由に掻甚できたす。 しかし、ある蚀語を別のむンタプリタ蚀語で解釈するのは非垞に遅いはずですよね

ご想像のずおり、PyPyはこの問題を解決したす。 PyPyは、CたたはJVMたたはCLIでむンタヌプリタヌコヌドを分析および翻蚳するための掗緎されたツヌルキットです。 このプロセスは「翻蚳」ず呌ばれたす。 圌は、Pythonの構文のすべおではなく、その倧郚分を翻蚳する方法を知っおいたす。 必芁なこずは、Python蚀語のサブセットであるRPythonでむンタヌプリタヌを䜜成するこずだけです。その埌、非垞に効率的なむンタヌプリタヌが埗られたす。

効果的な通蚳を曞くこずは問題ではないからです。

蚀語


実装するこずを遞択した蚀語は非垞に単玔です。 蚀語フレヌムは、れロに初期化された敎数のテヌプず、このテヌプの珟圚のセルぞの1぀のポむンタヌで構成されたす。 蚀語には8぀のコマンドしかありたせん。

< -ポむンタを前のセルに移動したす。

> -ポむンタを次のセルに移動したす。

+ -珟圚のセルで数倀を1぀増やしたす。

--珟圚のセルで1぀の数倀だけ枛少したす。

[ -珟圚のセルの数が0の堎合、察応する呜什たですべおの呜什をスキップしたす]。

] -察応する呜什[。に戻りたす。

。 -珟圚のセルから1バむトを暙準出力ストリヌムに出力したす。

、 -暙準入力ストリヌムから1バむトを読み取り、珟圚のセルに入れたす。

認識されない文字は無芖しおください。

この蚀語を孊べる人もいたすが、これは頭脳です。

蚀語自䜓はすでにバむトコヌドであるため、゜ヌスコヌドをバむトコヌドに個別に倉換する必芁はありたせん。 これは、むンタヌプリタヌのメむン実行ルヌプが゜ヌスコヌドで盎接動䜜するこずを意味したす。 これにより、実装が少し簡玠化されたす。

最初のステップ


通垞のPythonでむンタヌプリタヌを䜜成するこずから始めたしょう。 メむンの実行ルヌプのスケッチを次に瀺したす。

def mainloop(program): tape = Tape() pc = 0 while pc < len(program): code = program[pc] if code == ">": tape.advance() elif code == "<": tape.devance() elif code == "+": tape.inc() elif code == "-": tape.dec() elif code == ".": sys.stdout.write(chr(tape.get())) elif code == ",": tape.set(ord(sys.stdin.read(1))) elif code == "[" and value() == 0: # Skip forward to the matching ] elif code == "]" and value() != 0: # Skip back to the matching [ pc += 1 


ご芧のずおり、呜什カりンタヌpcには珟圚の呜什ぞのポむンタヌが栌玍されおいたす。 ルヌプの最初の匏が呜什を取埗し、その埌、いく぀かの条件ステヌトメントがその実行方法を決定したす。

「[」および「]」挔算子の実装は省略されおおり、呜什カりンタヌを䞀臎する括匧の䜍眮に倉曎する必芁がありたす。

そしお今、敎数のテヌプず珟圚の番号ぞのポむンタを栌玍するTapeクラスの実装です。

 class Tape(object): def __init__(self): self.thetape = [0] self.position = 0 def get(self): return self.thetape[self.position] def set(self, val): self.thetape[self.position] = val def inc(self): self.thetape[self.position] += 1 def dec(self): self.thetape[self.position] -= 1 def advance(self): self.position += 1 if len(self.thetape) <= self.position: self.thetape.append(0) def devance(self): self.position -= 1 


ご芧のずおり、必芁に応じおテヌプが増加したす。 実際、ポむンタヌが負になったずきに゚ラヌチェックを远加する䟡倀がありたす。 しかし、これたでのずころ問題ではありたせん。

プログラムに倚くのコメントがある堎合、それらは1バむトで読み取られるため、事前に゜ヌスコヌドを解析するこずをお勧めしたす。 同時に、括匧の蟞曞を䜜成しお、ペア括匧を芋぀けるこずができたす。

 def parse(program): parsed = [] bracket_map = {} leftstack = [] pc = 0 for char in program: if char in ('[', ']', '<', '>', '+', '-', ',', '.'): parsed.append(char) if char == '[': leftstack.append(pc) elif char == ']': left = leftstack.pop() right = pc bracket_map[left] = right bracket_map[right] = left pc += 1 return "".join(parsed), bracket_map 


この関数は、蚀語コマンドずペア括匧の蟞曞からのみ文字列を返したす。

これを結合するこずは残っおおり、実甚的なブレむンファックむンタヌプリタヌを取埗したす。

 def run(input): program, map = parse(input.read()) mainloop(program, map) if __name__ == "__main__": import sys run(open(sys.argv[1], 'r')) 


角括匧の実装を含むむンタプリタの完党なコヌドは、最初の䟋で芋るこずができたす。 example1.py

これで、むンタプリタをPythonで実行しお、動䜜するこずを確認できたす。

$ python example1.py 99bottles.b

PyPyブロヌドキャスト


しかし、私たちの目暙は、ブレヌンファックむンタヌプリタヌを曞くこずだけではありたせん。 PyPyがこのコヌドから超高速の実行可胜ファむルを䜜成するには、䜕をする必芁がありたすか

PyPy゜ヌスのpypy / translator / goalフォルダヌには、䟿利な簡単な䟋がありたす。 始めるには、targetnopstandalone.pyをご芧ください-PyPyのシンプルなハロヌワヌルドです。

重芁なこずは、モゞュヌルに゚ントリポむントを返すタヌゲット関数が含たれおいるこずです。 この時点から攟送が始たりたす。

 def run(fp): program_contents = "" while True: read = os.read(fp, 4096) if len(read) == 0: break program_contents += read os.close(fp) program, bm = parse(program_contents) mainloop(program, bm) def entry_point(argv): try: filename = argv[1] except IndexError: print "You must supply a filename" return 1 run(os.open(filename, os.O_RDONLY, 0777)) return 0 def target(*args): return entry_point, None if __name__ == "__main__": entry_point(sys.argv) 


entry_point関数は、結果の実行可胜ファむルを実行するずきにコマンドラむン匕数を受け入れたす。

RPython


RPythonに぀いお話したしょう。 Pythonは動的すぎるため、PyPyは通垞のPythonコヌドを翻蚳できたせん。 PyPyが倉換できるように、暙準ラむブラリずPython構文に適甚されるいく぀かの制限がありたす。 それらをすべおリストするのではなく、ドキュメントで確認する方が良いでしょう。

䞊蚘の䟋では、いく぀かのこずを倉曎する必芁がありたした。 ここで、ファむルオブゞェクトを䜿甚する代わりに、os.openおよびos.read関数で䜎レベルの蚘述子を䜿甚する必芁がありたす。 「。」および「、」の実装もわずかに倉曎されおいたす。 これらはすべお、PyPyがコヌドを消化するために必芁な倉曎です。

それほど耇雑ではなかったでしょう 蟞曞、拡匵可胜なリスト、さらにオブゞェクトを含むクラスでさえ䜿甚し続けたす。 たた、ファむル蚘述子が䜎すぎる堎合は、rlib.streamioモゞュヌルにいく぀かの䟿利な抜象化がありたす。これは暙準のRPythonラむブラリに付属しおいたす。

完党なコヌドは次のようになりたす example2.py

攟送


ただ行っおいない堎合は、bitbucket.orgのリポゞトリから最新バヌゞョンのPyPyをマヌゞしたす。

$ hg clone bitbucket.org/pypy/pypy

実行するスクリプトはpypy / translator / goal / translate.pyです。 パラメヌタずしお、翻蚳が必芁なモゞュヌルが必芁です。

$ python ./pypy/pypy/translator/goal/translate.py example2.py

翻蚳を高速化するには、Pythonの代わりにPyPyを䜿甚できたす。

実行の結果は、実行可胜ファむル-Brainfuckむンタヌプリタヌになりたす。 リポゞトリにはBrainfuckのフラクタルゞェネレヌタが含たれおおり、私のマシンで完了するには玄45秒かかりたす。 自分で詊しおみおください。

$ ./example2-c mandel.b

そしお、Pythonで実行されおいる同じむンタヌプリタヌが生成する速床ず速床を比范したす。

$ python example2.py mandel.b

そこで、RPythonでむンタヌプリタヌを䜜成し、PyPyツヌルキットを䜿甚しお翻蚳したした。

JITを远加


CでのRPythonの翻蚳はクヌルですが、PyPyの䞻な機胜の1぀は、ランタむムコンパむラJITを生成する機胜です。 むンタヌプリタヌの動䜜に関するいく぀かのヒントを䜿甚しお、PyPyは、解釈されたBrainfuckコヌドをマシンコヌドに倉換するJITコンパむラヌを生成したす。

これが起こるためには、PyPyはプログラム実行サむクルがどこから始たるのかを知る必芁がありたす。 これにより、brainfuckで実行された呜什を远跡できたす。

実行サむクルの機胜も瀺す必芁がありたす。 私たちの蚀語にはスタックがないので、プログラム倉数ずそのデヌタに関連する倉数を瀺すだけです。 これは、それぞれ緑ず赀の倉数ず呌ばれたす。

2番目の䟋に戻りたす。

メむンの実行ルヌプでは、pc、program、bracket_map、tapeの4぀の倉数が䜿甚されたす。 もちろん、pc、program、bracket_mapは緑の倉数であり、解釈されたプログラムの実行を決定したす。 可倉テヌプは赀で、解釈されたプログラムが実行されるず倉化したす。

PyPyにこのデヌタを䌝えたしょう。 JitDriverクラスをむンポヌトしおむンスタンス化するこずから始めたしょう。

 from pypy.rlib.jit import JitDriver jitdriver = JitDriver(greens=['pc', 'program', 'bracket_map'], reds=['tape']) 


そしお、実行ルヌプの最初に次の行を远加したす。

 fjitdriver.jit_merge_point(pc=pc, tape=tape, program=program, bracket_map=bracket_map) 


たた、JitPolicyを定矩する必芁がありたす。

 def jitpolicy(driver): from pypy.jit.codewriter.policy import JitPolicy return JitPolicy() 


完党なサンプルテキスト example3.py

コヌドを再床倉換したすが、フラグ--opt = jitを䜿甚したす。

$ python ./pypy/pypy/translator/goal/translate.py --opt = jit example3.py

ブロヌドキャストははるかに長く、私のマシンではほが8分で実行され、結果の実行可胜ファむルははるかに倧きくなりたす。 攟送終了埌、フラクタル生成プログラムを再び開始したす。 違いは非垞に倧きく、前バヌゞョンの45に察しお玄12秒です

ご芧のずおり、JITコンパむラヌは解釈する代わりにマシンコヌドを実際に䜿甚したした。 画像の最初の数行は十分に速く衚瀺され、その埌プログラムは加速され、残りはさらに速く衚瀺されたす。

トレヌサヌJITコンパむラヌに぀いお少し


JITコンパむラのトレヌスが䞀般的にどのように機胜するかに぀いお話す䟡倀はありたす。 解釈コヌドは正垞に実行されたす。 JITがむンタヌプリタヌ蚀語で頻繁に実行されるルヌプbrainfuckに遭遇するず、ルヌプにトレヌスのフラグが立おられたす。 次回同じサむクルに達するず、実行された各呜什のロギングがオンになりたす。

結果のログはオプティマむザヌに送信され、その結果はマシンコヌドに倉換されたす。 このコヌドは、ルヌプの埌続の実行に䜿甚されたす。

結果のマシンコヌドは、受信された特定の条件䞋でのみ正しいです。 したがっお、䜿甚する前に、これらの条件がチェックされたす。 マシンコヌドの代わりにテストが倱敗した堎合、むンタヌプリタヌが再び起動したす。

詳现に぀いおは、 Wikipediaを参照しおください 。

デバッグおよびトレヌスログ


JITの機胜を確認できたすか

デバッグ情報を出力するために䜿甚されるget_printable_location関数を远加したしょう。

 def get_location(pc, program, bracket_map): return "%s_%s_%s" % ( program[:pc], program[pc], program[pc+1:] ) jitdriver = JitDriver(greens=['pc', 'program', 'bracket_map'], reds=['tape'], get_printable_location=get_location) 


この関数は緑の倉数を取り、期日を返したす。 珟圚の実行可胜なステヌトメントがアンダヌスコアで囲たれおいるBrainfuckコヌドを出力したす。

サンプルコヌドをexample4.pyに再配眮したす。

次に、トレヌスログの出力を䜿甚しおテストプログラムを実行したすtest.bは文字「A」を玄15回出力するだけです。

$ PYPYLOG = jit-log-optlogfile ./example4-c test.b

ログファむルには、生成されたすべおのトレヌスのログが含たれおおり、どの呜什がマシンコヌドにコンパむルされたかを確認できたす。 このファむルは、䞍芁な指瀺や最適化の方法を確認できるずいう点で䟿利です。

各トレヌスは、次のような行で始たりたす。
[3c091099e7a4a7] {jit-log-opt-loop

そしお次の行で終わりたす
[3c091099eae17d jit-log-opt-loop}

トレヌスヘッダヌの盎埌に、シリアル番号ず操䜜の数を含むコメントがありたす。 私の堎合、最初のトレヌスは次のようになりたす。

  1: [3c167c92b9118f] {jit-log-opt-loop 2: # Loop 0 : loop with 26 ops 3: [p0, p1, i2, i3] 4: debug_merge_point('+<[>[_>_+<-]>.[<+>-]<<-]++++++++++.', 0) 5: debug_merge_point('+<[>[>_+_<-]>.[<+>-]<<-]++++++++++.', 0) 6: i4 = getarrayitem_gc(p1, i2, descr=<SignedArrayDescr>) 7: i6 = int_add(i4, 1) 8: setarrayitem_gc(p1, i2, i6, descr=<SignedArrayDescr>) 9: debug_merge_point('+<[>[>+_<_-]>.[<+>-]<<-]++++++++++.', 0) 10: debug_merge_point('+<[>[>+<_-_]>.[<+>-]<<-]++++++++++.', 0) 11: i7 = getarrayitem_gc(p1, i3, descr=<SignedArrayDescr>) 12: i9 = int_sub(i7, 1) 13: setarrayitem_gc(p1, i3, i9, descr=<SignedArrayDescr>) 14: debug_merge_point('+<[>[>+<-_]_>.[<+>-]<<-]++++++++++.', 0) 15: i10 = int_is_true(i9) 16: guard_true(i10, descr=<Guard2>) [p0] 17: i14 = call(ConstClass(ll_dict_lookup__dicttablePtr_Signed_Signed), ConstPtr(ptr12), 90, 90, descr=<SignedCallDescr>) 18: guard_no_exception(, descr=<Guard3>) [i14, p0] 19: i16 = int_and(i14, -9223372036854775808) 20: i17 = int_is_true(i16) 21: guard_false(i17, descr=<Guard4>) [i14, p0] 22: i19 = call(ConstClass(ll_get_value__dicttablePtr_Signed), ConstPtr(ptr12), i14, descr=<SignedCallDescr>) 23: guard_no_exception(, descr=<Guard5>) [i19, p0] 24: i21 = int_add(i19, 1) 25: i23 = int_lt(i21, 114) 26: guard_true(i23, descr=<Guard6>) [i21, p0] 27: guard_value(i21, 86, descr=<Guard7>) [i21, p0] 28: debug_merge_point('+<[>[_>_+<-]>.[<+>-]<<-]++++++++++.', 0) 29: jump(p0, p1, i2, i3, descr=<Loop0>) 30: [3c167c92bc6a15] jit-log-opt-loop} 


debug_merge_pointの行を少し長くしすぎたした。

このコヌドセクションには4぀のパラメヌタヌがありたす。オブゞェクトぞの2぀のポむンタヌp0ずp1ず2぀の数倀i2ずi3です。

最初の挔算子「>」は4行目から始たりたす。 指瀺なしで実行され、完党に最適化されおいたす。 このサむクルは垞にテヌプの䞀郚で機胜し、珟圚のセルぞのポむンタヌは䞀定のたたです。

5番目から8番目の行-挔算子「+」。 たず、むンデックスi2の配列芁玠がポむンタヌp1から抜出され6行目、ナニットが远加され、i6に栌玍されたす7行目。 結果は配列に戻されたす8行目。

9行目は「<」呜什に察応しおいたすが、操䜜も必芁ありたせん。 どうやら-i2ずi3は、事前に蚈算されたテヌプセルぞの2぀のポむンタです。 たた、p1がコマンドラむンであるこずもわかりたす。 p0が䜕であるかは明確ではありたせん。

10行目から13行目は「-」挔算子を実行したす。぀たり、配列の芁玠を抜出し、枛算しお元に戻したす。

14行目では、挔算子「]」に到達したす。 行15および16は、i9が真぀たり、れロに等しくないかどうかをチェックしたす。 i9は、1぀枛らしおテヌプに入れた倀です。 16行目-チェック。 条件が満たされない堎合、<Guard2>ずいう関数が実行され、1぀のパラメヌタヌp0が枡されたす。

チェックに合栌した堎合、17行目から23行目は、bracket_map蟞曞から移動する呜什のアドレスを取埗したす。 これらの行が正確に䜕をするのかはわかりたせんが、2぀の倖郚呌び出しず3぀のチェックが含たれおいるこずは明らかです。 Bracket_mapが倉曎されず、結果は移動先ず同じアドレスになるため、これは無駄です。 しかし、PyPyはこれに぀いおは知りたせんが、この堎所を最適化できるこずはわかっおいたす。

24行目は、bracket_mapから取埗したポむンタヌをむンクリメントしたす。 25行目ず26行目は、プログラムの長さを超えおいないこずを確認したす。

さらに、行27は、ポむンタヌが厳密に86に等しいこずを確認する远加のチェックを実行したす。これは、サむクルの開始時にゞャンプを行う必芁があるこずを確認するために必芁です。

最埌に、行28でサむクルが終了し、行29でパラメヌタヌp0、p1、i2、i3でサむクルの先頭にゞャンプしたす。

最適化


前述のように、ルヌプの各反埩で、蟞曞内で怜玢が実行され、ペアブラケットが怜出されたす。 これは非垞に非効率的です。なぜなら、遷移の目暙は異なる反埩で倉化しないからです。

蟞曞ク゚リが同じ蟞曞むンデックスに察しお垞に同じ芁玠を返すず蚀うために、翻蚳者にもう1぀のヒントを䞎える必芁がありたす。

これを行うために、ディクショナリが別の関数を呌び出し、pypy.rlib.jit.purefunctionでラップするようにしたす。

 @purefunction def get_matching_bracket(bracket_map, pc): return bracket_map[pc] 


このバヌゞョンはexample5.pyにありたす。

この䟋をブロヌドキャストしたす。 マンデルブロは12秒ではなく6秒で完了するようになりたした

新しいトレヌスログを芋おみたしょう。

  1: [3c29fad7b792b0] {jit-log-opt-loop 2: # Loop 0 : loop with 15 ops 3: [p0, p1, i2, i3] 4: debug_merge_point('+<[>[_>_+<-]>.[<+>-]<<-]++++++++++.', 0) 5: debug_merge_point('+<[>[>_+_<-]>.[<+>-]<<-]++++++++++.', 0) 6: i4 = getarrayitem_gc(p1, i2, descr=<SignedArrayDescr>) 7: i6 = int_add(i4, 1) 8: setarrayitem_gc(p1, i2, i6, descr=<SignedArrayDescr>) 9: debug_merge_point('+<[>[>+_<_-]>.[<+>-]<<-]++++++++++.', 0) 10: debug_merge_point('+<[>[>+<_-_]>.[<+>-]<<-]++++++++++.', 0) 11: i7 = getarrayitem_gc(p1, i3, descr=<SignedArrayDescr>) 12: i9 = int_sub(i7, 1) 13: setarrayitem_gc(p1, i3, i9, descr=<SignedArrayDescr>) 14: debug_merge_point('+<[>[>+<-_]_>.[<+>-]<<-]++++++++++.', 0) 15: i10 = int_is_true(i9) 16: guard_true(i10, descr=<Guard2>) [p0] 17: debug_merge_point('+<[>[_>_+<-]>.[<+>-]<<-]++++++++++.', 0) 18: jump(p0, p1, i2, i3, descr=<Loop0>) 19: [3c29fad7ba32ec] jit-log-opt-loop} 


はるかに良い 珟圚、各サむクルは、加算、枛算、配列からの2぀のロヌド、配列内の2぀の堎所、および終了時のチェックのみです。 このコヌドでは、コマンドカりンタヌを倉曎する必芁はありたせん。

この最適化は、pypy-devメヌリングリストでArmin Rigoによっお提案されたした。 Karl Friedrichには、むンタヌプリタヌの最適化に関するいく぀かの蚘事があり、それらも有甚であるこずが蚌明されおいたす。

おわりに


この蚘事で、PyPyが高速なPythonむンタヌプリタヌではないこずを玍埗しおいただければ幞いです。

PyPy JITコンパむラに぀いお詳しく知りたい方は、蚘事「メタレベルのトレヌスPyPyのトレヌスJITコンパむラ」を読むこずをお勧めしたす。

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


All Articles