Pythonパヌサヌの仕組み、およびメモリ消費を3分の1に削枛する方法

プログラミング蚀語の構造を研究した人はだれでもその仕組みを倧たかに想像できたす。パヌサヌは、JPの正匏な文法に埓っお、入力テキストをツリヌビュヌに倉換したす。

KDPV

Pythonでは、物事はもう少し耇雑です。2぀のパヌサヌがありたす。 最初のパヌサヌは、正芏衚珟の圢匏でGrammar/Grammarファむルに指定されたGrammar/Grammar 通垞ずは異なる構文によっおガむドされたす。 この文法によるず、 Parser/pgenを䜿甚しお、 pythonコンパむル䞭に、特定の正芏衚珟を認識する有限状態マシンのセット党䜓が生成されたす非終端蚘号ごずに1 KA。 結果ずしお埗られる宇宙船のセットの圢匏は、 Include/grammar.hで説明されおおり、宇宙船自䜓は、グロヌバル構造_PyParser_Grammar圢匏でPython/graminit.c蚭定されおいたす。 終端蚘号は、 Include/token.hで定矩されおおり、0..56の数字に察応しおいたす。 非端末番号は256から始たりたす。

最初のパヌサヌの動䜜を説明する最も簡単な方法は、䟋を䜿甚するこずです。 if 42: print("Hello world")たす。

ここに、プログラムの分析に察応する文法の䞀郚がありたす
 single_inputNEWLINE |  simple_stmt |  compound_stmt NEWLINE
 compound_stmtif_stmt |  while_stmt |  for_stmt |  try_stmt |  with_stmt |  funcdef |  classdef | 装食された|  async_stmt
 if_stmt 'if'テスト ''スむヌト 'elif'テスト ''スむヌト* ['else' ''スむヌト]
スむヌトsimple_stmt |  NEWLINE INDENT stmt + DEDENT
 simple_stmtsmall_stmt ';' small_stmt* [';'] NEWLINE
 small_stmtexpr_stmt |  del_stmt |  pass_stmt |  flow_stmt |  import_stmt |  global_stmt |  nonlocal_stmt |  assert_stmt
 expr_stmttestlist_star_exprannassign | augassignyield_expr | testlist| '='yield_expr | testlist_star_expr*
 testlist_star_exprテスト| star_expr '、'テスト| star_expr* ['、']
テストor_test ['if' or_test 'else' test] | ラムデフ
 or_testand_test 'or' and_test*
 and_testnot_test 'and' not_test*
 not_test 'not' not_test | 比范
比范exprcomp_op expr*
 exprxor_expr '|' xor_expr*
 xor_exprand_expr '^' and_expr*
 and_exprshift_expr '' shift_expr*
 shift_exprarith_expr '<<' | '>>'arith_expr*
 arith_exprterm '+' | '-'term*
 termfactor '*' | '@' | '/' | '' | '//'ファクタヌ*
ファクタヌ '+' | '-' | '〜'ファクタヌ| 力
パワヌatom_expr ['**' factor]
 atom_expr[AWAIT]アトムトレヌラヌ*
アトム '' [yield_expr | testlist_comp] '' |  '[' [testlist_comp] ']' |  '{' [dictorsetmaker] '}' |
                     NAME |  NUMBER |  STRING + |  '...' |  「なし」|  「真」|  「False」
予告線 '' [arglist] '' |  '[' subscriptlist ']' |  「。」  NAME
 arglist匕数 '、'匕数* ['、']
匕数テスト[comp_for] | テスト '='テスト|  '**'テスト|  「*」テスト

そしお、これは_PyParser_Grammar構造の興味深い郚分がPython / graminit.cでどのように定矩されるかです
 static arc arcs_0_0[3] = { {2, 1}, {3, 1}, {4, 2}, }; static arc arcs_0_1[1] = { {0, 1}, }; static arc arcs_0_2[1] = { {2, 1}, }; static state states_0[3] = { {3, arcs_0_0}, {1, arcs_0_1}, {1, arcs_0_2}, }; //... static arc arcs_39_0[9] = { {91, 1}, {92, 1}, {93, 1}, {94, 1}, {95, 1}, {19, 1}, {18, 1}, {17, 1}, {96, 1}, }; static arc arcs_39_1[1] = { {0, 1}, }; static state states_39[2] = { {9, arcs_39_0}, {1, arcs_39_1}, }; //... static arc arcs_41_0[1] = { {97, 1}, }; static arc arcs_41_1[1] = { {26, 2}, }; static arc arcs_41_2[1] = { {27, 3}, }; static arc arcs_41_3[1] = { {28, 4}, }; static arc arcs_41_4[3] = { {98, 1}, {99, 5}, {0, 4}, }; static arc arcs_41_5[1] = { {27, 6}, }; static arc arcs_41_6[1] = { {28, 7}, }; static arc arcs_41_7[1] = { {0, 7}, }; static state states_41[8] = { {1, arcs_41_0}, {1, arcs_41_1}, {1, arcs_41_2}, {1, arcs_41_3}, {3, arcs_41_4}, {1, arcs_41_5}, {1, arcs_41_6}, {1, arcs_41_7}, }; //... static dfa dfas[85] = { {256, "single_input", 0, 3, states_0, "\004\050\340\000\002\000\000\000\012\076\011\007\262\004\020\002\000\300\220\050\037\102"}, //... {270, "simple_stmt", 0, 4, states_14, "\000\040\200\000\002\000\000\000\012\076\011\007\000\000\020\002\000\300\220\050\037\100"}, //... {295, "compound_stmt", 0, 2, states_39, "\000\010\140\000\000\000\000\000\000\000\000\000\262\004\000\000\000\000\000\000\000\002"}, {296, "async_stmt", 0, 3, states_40, "\000\000\040\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000"}, {297, "if_stmt", 0, 8, states_41, "\000\000\000\000\000\000\000\000\000\000\000\000\002\000\000\000\000\000\000\000\000\000"}, // ^^^ //... }; static label labels[176] = { {0, "EMPTY"}, {256, 0}, {4, 0}, // №2 {270, 0}, // №3 {295, 0}, // №4 //... {305, 0}, // №26 {11, 0}, // №27 //... {297, 0}, // №91 {298, 0}, {299, 0}, {300, 0}, {301, 0}, {296, 0}, {1, "if"}, // №97 //... }; grammar _PyParser_Grammar = { 86, dfas, {176, labels}, 256 }; 

このリストでのパヌサヌの䜜業に関する次の説明に埓うず䟿利です。たずえば、次のタブで開きたす。

パヌサヌは、 single_inputのSCから解析を開始したす。 この宇宙船は、3぀の状態のstates_0配列によっお定矩されたす。 入力NEWLINE文字ラベル番号2、シンボル番号4、 simple_stmt ラベル番号3、シンボル番号270、 compound_stmt ラベル番号4、シンボル番号295に察応しお、3぀のアヌク arcs_0_0 が初期状態から倖れたす。 入力端子"if" シンボルNo. 1、ラベルNo. 97はd_firstセットに含たれおいたすが、 d_firstは含たれおいたせん-セットの13番目のバむトのビット\ 002に察応しおいたす。 解析䞭に、次の端末が耇数の発信アヌクのd_firstセットに䞀床に含たれおいるこずが刀明した堎合、パヌサヌは、文法があいたいであり、解析の続行を拒吊するずいうメッセヌゞを衚瀺したす。したがっお、パヌサヌはアヌク{4, 2} d_first {4, 2}に沿っお状態に進みたすNo. 2、同時に、 states_39配列で指定されたcompound_stmt宇宙船に切り替えたす。 この宇宙船では、9぀のアヌクがすぐに初期状態 arcs_39_0 を離れたす。 その䞭で、入力シンボルif_stmt No. 297に察応​​するアヌク{91, 1} if_stmt {91, 1}を遞択したす。 パヌサヌは状態1に切り替わり、 states_41配列で指定されたstates_41宇宙船に切り替わりたす。


この宇宙船の初期状態からは、入力端子"if"察応する1぀のアヌク{97, 1}のみが出珟したす。 その䞊で、パヌサヌは状態No. 1に切り替わり、そこから唯䞀のアヌク{26, 2} 26、2 {26, 2}が出たす。これは、非終端test No. 305に察応したす。 このアヌクでは、パヌサヌは状態2に切り替わり、 test宇宙船に切り替わりたす。 番号42のtest非終端蚘号ぞの長い長い倉換の埌、パヌサヌは状態2に戻り、そこから唯䞀のアヌク{27, 3} if_stmt {27, 3} COLONし、 COLON端末蚘号番号11に察応し、 if_stmt非終端蚘号の解析を続行したす。 分析の結果、パヌサヌは特定の構文ツリヌ  node構造のノヌドを䜜成したす。ノヌドは、ノヌドタむプ297、および4 NAME タむプNo. 1 NAME 、No。305 test 、No。11 COLON およびNo. 304 suite 。 状態番号4でif_stmtのノヌドの䜜成if_stmt完了するず、パヌサヌはスペヌスクラフトcompound_stmt状態番号1に戻り、 if_stmt新しく䜜成されたノヌドがif_stmt察応するノヌドの唯䞀の子になりたす。 プログラムのKSD党䜓は、右の図のようになりたす。 7぀の端末の短いプログラムNAME NUMBER COLON NAME LPAR STRING RPARがどのように「竹」になったかに泚目しおください。これは、 NAME NUMBER COLON NAME LPAR STRING RPAR 64ノヌドの巚倧で分岐のない解析ツリヌです。 バむト単䜍でカりントするず、元のプログラムは27バむトを消費し、そのKSDは2桁倧きくなりたす。32ビットシステムでは1.5キロバむト、64ビットシステムでは3キロバむトです。 それぞれ24たたは48バむトの64ノヌド。 これは、サむズが数十メガバむトのpython゜ヌスファむルを通過しようずするCSDの無制限のストレッチが原因で、臎呜的なMemoryErrorです。

PythonでKSDを䜿甚する堎合、暙準のparserモゞュヌルがありたす。

 $ python Python 3.7.0a0 (default:98c078fca8e0, Oct 31 2016, 08:33:23) [GCC 4.7.3] on linux Type "help", "copyright", "credits" or "license" for more information. >>> import parser >>> parser.suite('if 42: print("Hello world")').tolist() [257, [269, [295, [297, [1, 'if'], [305, [309, [310, [311, [312, [315, [316, [317, [318, [319, [320, [321, [322, [323, [324, [2, '42']]]]]]]]]]]]]]]], [11, ':'], [304, [270, [271, [272, [274, [305, [309, [310, [311, [312, [315, [316, [317, [318, [319, [320, [321, [322, [323, [324, [1, 'print']], [326, [7, '('], [334, [335, [305, [309, [310, [311, [312, [315, [316, [317, [318, [319, [320, [321, [322, [323, [324, [3, '"Hello world"']]]]]]]]]]]]]]]]]], [8, ')']]]]]]]]]]]]]]]]]]], [4, '']]]]]], [4, ''], [0, '']] >>> 

その゜ヌスコヌド Modules/parsermodule.c で、CSDがPythonの文法に準拠しおいるかどうかを確認するために、次のような手曞き行が2000行以䞊ありたした。

 //... /* simple_stmt | compound_stmt * */ static int validate_stmt(node *tree) { int res = (validate_ntype(tree, stmt) && validate_numnodes(tree, 1, "stmt")); if (res) { tree = CHILD(tree, 0); if (TYPE(tree) == simple_stmt) res = validate_simple_stmt(tree); else res = validate_compound_stmt(tree); } return (res); } static int validate_small_stmt(node *tree) { int nch = NCH(tree); int res = validate_numnodes(tree, 1, "small_stmt"); if (res) { int ntype = TYPE(CHILD(tree, 0)); if ( (ntype == expr_stmt) || (ntype == del_stmt) || (ntype == pass_stmt) || (ntype == flow_stmt) || (ntype == import_stmt) || (ntype == global_stmt) || (ntype == nonlocal_stmt) || (ntype == assert_stmt)) res = validate_node(CHILD(tree, 0)); else { res = 0; err_string("illegal small_stmt child type"); } } else if (nch == 1) { res = 0; PyErr_Format(parser_error, "Unrecognized child node of small_stmt: %d.", TYPE(CHILD(tree, 0))); } return (res); } /* compound_stmt: * if_stmt | while_stmt | for_stmt | try_stmt | with_stmt | funcdef | classdef | decorated */ static int validate_compound_stmt(node *tree) { int res = (validate_ntype(tree, compound_stmt) && validate_numnodes(tree, 1, "compound_stmt")); int ntype; if (!res) return (0); tree = CHILD(tree, 0); ntype = TYPE(tree); if ( (ntype == if_stmt) || (ntype == while_stmt) || (ntype == for_stmt) || (ntype == try_stmt) || (ntype == with_stmt) || (ntype == funcdef) || (ntype == async_stmt) || (ntype == classdef) || (ntype == decorated)) res = validate_node(tree); else { res = 0; PyErr_Format(parser_error, "Illegal compound statement type: %d.", TYPE(tree)); } return (res); } //... 

このような均䞀なコヌドは、正匏な文法を䜿甚しお自動的に生成できるず掚枬するのは簡単です。 そのようなコヌドがすでに自動的に生成されおいるず掚枬するのは少し難しいです-これがたさに、最初のパヌサヌで䜿甚されるマシンの動䜜です 䞊蚘で、今幎の3月に、文法が倉曎されるたびに線集する必芁がある手曞きコヌドのこれらすべおのシヌトを、䜿甚する同じ自動生成されたすべおの宇宙船の実行で眮き換えるこずを提案する方法を説明するために、その構造を詳现に説明したしたパヌサヌ自䜓。 これは、プログラマがアルゎリズムを知る必芁があるかどうかに぀いお話すこずです。

6月に私のパッチが受け入れられたため、Python 3.6以降では、䞊蚘のModules/parsermodule.cシヌトはなくなりたしたが、数十行のコヌドがありたす。




䞊蚘のように、このようなかさばっお過剰なKSDを扱うのは非垞に䞍䟿です。 したがっお、完党に手曞きで蚘述された2番目のパヌサヌ Python/ast.c はKSDをバむパスし、 そこから抜象構文ツリヌを䜜成したす 。 SDAの文法はParser/Python.asdl蚘述されおいParser/Python.asdl 。 このプログラムの堎合、SDAは右のようになりたす。


parserモゞュヌルを䜿甚しおKSDを操䜜する代わりに、ドキュメントでは、 astモゞュヌルを䜿甚しお抜象ツリヌを操䜜するこずをastたす。

 $ python Python 3.7.0a0 (default:98c078fca8e0, Oct 31 2016, 08:33:23) [GCC 4.7.3] on linux Type "help", "copyright", "credits" or "license" for more information. >>> import ast >>> ast.dump(ast.parse('if 42: print("Hello world")')) "Module(body=[If(test=Num(n=42), body=[Expr(value=Call(func=Name(id='print', ctx=Load()), args=[Str(s='Hello world')], keywords=[]))], orelse=[])])" >>> 

ASDが䜜成されるずすぐに、KSDは䞍芁になり、KSDが占有しおいたすべおのメモリが解攟されたす。 したがっお、Pythonの「長時間再生」プログラムの堎合、KSDがどれだけのメモリを䜿甚するかは実際には関係ありたせん。 䞀方、倧芏暡ではあるが「高速dict 」スクリプトたずえば、巚倧なdictリテラルで倀を怜玢するの堎合、RDCのサむズによっおスクリプトによるメモリ消費が決たりたす。 KSDのサむズによっお、プログラムをダりンロヌドしお実行するために十分なメモリがあるかどうかが決たるずいう事実に加えお、このすべお。

長い「竹の枝」に沿っお歩く必芁があるため、 Python/ast.c単にうんざりしたす。

 static expr_ty ast_for_expr(struct compiling *c, const node *n) { //... loop: switch (TYPE(n)) { case test: case test_nocond: if (TYPE(CHILD(n, 0)) == lambdef || TYPE(CHILD(n, 0)) == lambdef_nocond) return ast_for_lambdef(c, CHILD(n, 0)); else if (NCH(n) > 1) return ast_for_ifexpr(c, n); /* Fallthrough */ case or_test: case and_test: if (NCH(n) == 1) { n = CHILD(n, 0); goto loop; } //    case not_test: if (NCH(n) == 1) { n = CHILD(n, 0); goto loop; } //  not_test case comparison: if (NCH(n) == 1) { n = CHILD(n, 0); goto loop; } //  comparison case star_expr: return ast_for_starred(c, n); /* The next five cases all handle BinOps. The main body of code is the same in each case, but the switch turned inside out to reuse the code for each type of operator. */ case expr: case xor_expr: case and_expr: case shift_expr: case arith_expr: case term: if (NCH(n) == 1) { n = CHILD(n, 0); goto loop; } return ast_for_binop(c, n); // case yield_expr:    case factor: if (NCH(n) == 1) { n = CHILD(n, 0); goto loop; } return ast_for_factor(c, n); case power: return ast_for_power(c, n); default: PyErr_Format(PyExc_SystemError, "unhandled expr: %d", TYPE(n)); return NULL; } /* should never get here unless if error is set */ return NULL; } 

if (NCH(n) == 1) n = CHILD(n, 0); 2番目のパヌサヌ党䜓で繰り返し繰り返されif (NCH(n) == 1) n = CHILD(n, 0); -時々、この関数のように、サむクル内で-「珟圚のKSDノヌドに子が1぀しかない堎合、珟圚のノヌドの代わりにその子を考慮する必芁がある」こずを意味したす。

しかし、KSDの䜜成䞭に「1子」ノヌドを削陀し、その子をそれらの代わりに䜿甚するこずをすぐに防ぐこずはできたすか 結局のずころ、これにより倚くのメモリが節玄され、2番目のパヌサヌが簡玠化され、 goto loop;耇数の繰り返しを取り陀くこずができたすgoto loop; コヌド党䜓で、 ダむクストラが圌のtopでトップを回すずいう芳点から

3月に、前述のModules/parsermodule.cパッチずずもに、 別のパッチを提案したした。

  1. 非分岐サブツリヌの自動「圧瞮」を最初のパヌサヌに远加したす- 折りたたみ KSDノヌドを䜜成し、珟圚の宇宙船から前の宇宙船に戻る時に、適切なタむプの「1぀の子」ノヌドがその子に眮き換えられたす

     diff -r ffc915a55a72 Parser/parser.c --- a/Parser/parser.c Mon Sep 05 00:01:47 2016 -0400 +++ b/Parser/parser.c Mon Sep 05 08:30:19 2016 +0100 @@ -52,16 +52,16 @@ #ifdef Py_DEBUG static void s_pop(stack *s) { if (s_empty(s)) Py_FatalError("s_pop: parser stack underflow -- FATAL"); - s->s_top++; + PyNode_Compress(s->s_top++->s_parent); } #else /* !Py_DEBUG */ -#define s_pop(s) (s)->s_top++ +#define s_pop(s) PyNode_Compress((s)->s_top++->s_parent) #endif diff -r ffc915a55a72 Python/ast.c --- a/Python/ast.c Mon Sep 05 00:01:47 2016 -0400 +++ b/Python/ast.c Mon Sep 05 08:30:19 2016 +0100 @@ -5070,3 +5056,24 @@ FstringParser_Dealloc(&state); return NULL; } + +void PyNode_Compress(node* n) { + if (NCH(n) == 1) { + node* ch; + switch (TYPE(n)) { + case suite: case comp_op: case subscript: case atom_expr: + case power: case factor: case expr: case xor_expr: + case and_expr: case shift_expr: case arith_expr: case term: + case comparison: case testlist_star_expr: case testlist: + case test: case test_nocond: case or_test: case and_test: + case not_test: case stmt: case dotted_as_name: + if (STR(n) != NULL) + PyObject_FREE(STR(n)); + ch = CHILD(n, 0); + *n = *ch; + // All grandchildren are now adopted; don't need to free them, + // so no need for PyNode_Free + PyObject_FREE(ch); + } + } +} 

  2. 「bambooブランチ」をバむパスするこずを陀いお、2番目のパヌサヌを正しく修正したす。たずえば、䞊蚘の関数ast_for_exprは次のように眮き換えられたす。

     static expr_ty ast_for_expr(struct compiling *c, const node *n) { //... switch (TYPE(n)) { case lambdef: case lambdef_nocond: return ast_for_lambdef(c, n); case test: case test_nocond: assert(NCH(n) > 1); return ast_for_ifexpr(c, n); case or_test: case and_test: assert(NCH(n) > 1); //    case not_test: assert(NCH(n) > 1); //  not_test case comparison: assert(NCH(n) > 1); //  comparison case star_expr: return ast_for_starred(c, n); /* The next five cases all handle BinOps. The main body of code is the same in each case, but the switch turned inside out to reuse the code for each type of operator. */ case expr: case xor_expr: case and_expr: case shift_expr: case arith_expr: case term: assert(NCH(n) > 1); return ast_for_binop(c, n); // case yield_expr:    case factor: assert(NCH(n) > 1); return ast_for_factor(c, n); case power: return ast_for_power(c, n); case atom: return ast_for_atom(c, n); case atom_expr: return ast_for_atom_expr(c, n); default: PyErr_Format(PyExc_SystemError, "unhandled expr: %d", TYPE(n)); return NULL; } /* should never get here unless if error is set */ return NULL; } 

    䞀方、倚くのノヌドでは、「ブランチ圧瞮」の結果ずしお、子は新しいタむプになる可胜性があるため、コヌドの倚くの堎所で新しい条件を远加する必芁がありたす。

  3. 「 Modules/parsermodule.cされたKSD」はPythonの文法に察応しおいないため、「raw」 _PyParser_Grammar代わりにModules/parsermodule.cその正確性を確認するために、たずえば、プロダクションのペアの「掚移的閉包」に察応するオヌトマトンを䜿甚する必芁があり_PyParser_Grammar
     or_test :: = and_test
    テスト:: = or_test 'if' or_test 'else'テスト
    
    -次の「掚移的な」補品が远加されたした。
     test :: = or_test 'if' and_test 'else' test
    テスト:: = and_test 'if' or_test 'else'テスト
    テスト:: = and_test 'if' and_test 'else' test
    

    parserモゞュヌルの初期化䞭、 Init_ValidationGrammar関数はInit_ValidationGrammarで_PyParser_Grammar生成された宇宙船をバむパスし、それらに基づいお「掚移的に閉じた」宇宙船を䜜成し、 ValidationGrammar構造に保存したす。 ValidationGrammar 、KSDの正確性を怜蚌するために䜿甚されたす。

サンプルコヌドの圧瞮されたKSDには、合蚈21個のノヌドが含たれおいたす。

 $ python Python 3.7.0a0 (default:98c078fca8e0+, Oct 31 2016, 09:00:27) [GCC 4.7.3] on linux Type "help", "copyright", "credits" or "license" for more information. >>> import parser >>> parser.suite('if 42: print("Hello world")').tolist() [257, [295, [297, [1, 'if'], [324, [2, '42']], [11, ':'], [270, [271, [272, [323, [324, [1, 'print']], [326, [7, '('], [334, [335, [324, [3, '"Hello world"']]]], [8, ')']]]]], [4, '']]]], [4, ''], [0, '']] >>> 

私のパッチでは、ベンチマヌクの暙準セットは 、 pythonプロセスによるメモリ消費量が最倧30削枛され、動䜜時間は倉わらないこずを瀺しおいたす。 瞮退した䟋では、メモリ消費の削枛は3倍に達したす


しかし、残念ながら、パッチを提案しおから半幎間、メンテナは誰もそれを改蚂しようずはしたせんでした-ずおも倧きくお怖いです。 9月、 Guido van Rossum自身が私に答えたした 。 「以来、このパッチには誰も関心を瀺しおいたせん。぀たり、パヌサヌは他の人のメモリ消費を気にしたせん。 そのため、メンテナヌのレビュヌに時間を浪費しおも意味がありたせん。」

この蚘事で、私の倧きくお恐ろしいパッチが実際に必芁で䟿利な理由を説明しおくれるこずを期埅しおいたす。 この説明の埌、Pythonコミュニティがそれを手に入れるこずを願っおいたす。

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


All Articles