Как узнать, выполняет ли Prolog оптимизацию хвостового вызова

Используя разрабатываемую версию SWI Prolog (Win x64), я написал предикат DCG для детерминированного лексера (размещенного на github ) (таким образом, все внешние предикаты не оставляют точек выбора):

read_token(parser(Grammar, Tables),
       lexer(dfa-DFAIndex, last_accept-LastAccept, chars-Chars0),
       Token) -->
(   [Input],
    {
     dfa:current(Tables, DFAIndex, DFA),
     char_and_code(Input, Char, Code),
     dfa:find_edge(Tables, DFA, Code, TargetIndex)
    }
->  { table:item(dfa_table, Tables, TargetIndex, TargetDFA),
      dfa:accept(TargetDFA, Accept),
      atom_concat(Chars0, Char, Chars),
      NewState = lexer(dfa-TargetIndex,
                       last_accept-Accept,
                       chars-Chars)
    },
    read_token(parser(Grammar, Tables), NewState, Token)
;   {
     (   LastAccept \= none
     ->  Token = LastAccept-Chars0
     ;   (   ground(Input)
         ->  once(symbol:by_type_name(Tables, error, Index, _)),
             try_restore_input(Input, FailedInput, InputR),
             Input = [FailedInput | InputR],
             format(atom(Error), '~w', [FailedInput]),
             Token = Index-Error
         ;   once(symbol:by_type_name(Tables, eof, Index, _)),
             Token = Index-''
         )
     )
    }
).

Теперь, когда я много использую (;) и ->, мне было интересно, что SWI-Prolog может оптимизировать рекурсивный read_token(parser(Grammar, Tables), NewState, Token) с помощью Tail-Call-Optimization, или если мне нужно вручную разбить предикат на несколько предложений. Я просто не знаю, как узнать, что делает интерпретатор, особенно зная, что TCO отключена при запуске отладчика.


person Sebastian    schedule 15.03.2013    source источник
comment
Не переполняется ли стек при запуске интерпретатора без отладки?   -  person hardmath    schedule 15.03.2013
comment
Глядя на ваш фрагмент кода, кажется, что необходимое условие для TCO выполнено (точка выбора не открывается, когда read_token/3 вызывает себя). Я понимаю, что вы спрашиваете, действительно ли SWI-Prolog использует преимущества совокупной стоимости владения в данном конкретном случае.   -  person hardmath    schedule 15.03.2013
comment
Нет, поскольку я использую x64 bit, размер стека довольно велик. Просто прошу совета по технике программирования. У меня нет файлов, достаточно больших, чтобы лексер мог вызвать переполнение, я полагаю (поскольку я могу отлично отладить метод, когда не применяется совокупная стоимость владения)   -  person Sebastian    schedule 15.03.2013
comment
Я мог бы ответить параметрами командной строки, которые уменьшают размер стека. Это был бы мой грубый подход к тестированию на общую стоимость владения.   -  person hardmath    schedule 15.03.2013
comment
Что вас интересует, так это порог размера локального стека, при котором ваш код не работает. См. Обсуждение внизу страницы. Как увеличить стопки?.   -  person hardmath    schedule 15.03.2013
comment
Хорошо. Меня больше интересует, действительно ли мой способ программирования активно предотвращает совокупную стоимость владения. Если так, я бы изменил все свои предикаты, если нет, я останусь таким.   -  person Sebastian    schedule 15.03.2013


Ответы (1)


Чтобы ответить на ваш вопрос, я сначала искал "тривиальные" цели, которые могли бы помешать оптимизации последнего вызова. Если нашли:

     ;   (   ground(Input)
         ->  once(symbol:by_type_name(Tables, error, Index, _)),
             try_restore_input(Input, FailedInput, InputR),
             Input = [FailedInput | InputR],
             format(atom(Error), '~w', [FailedInput]),
             Token = Index-Error
         ;   once(symbol:by_type_name(Tables, eof, Index, _)),
             Token = Index-''
         )

В этих двух случаях LCO предотвращается только этими целями.

Теперь я выполнил ваше правило и посмотрел на расширение с listing:

?- listing(read_token).
read_token(parser(O, B), lexer(dfa-C, last_accept-T, chars-J), Q, A, S) :-
        (   A=[D|G],
            dfa:current(B, C, E),
            char_and_code(D, K, F),
            dfa:find_edge(B, E, F, H),
            N=G
        ->  table:item(dfa_table, B, H, I),
            dfa:accept(I, L),
            atom_concat(J, K, M),
            P=lexer(dfa-H, last_accept-L, chars-M),
            R=N,
            read_token(parser(O, B),
                       P,
                       Q,
                       R,
                       S)        % 1: looks nice!
        ;   (   T\=none
            ->  Q=T-J
            ;   ground(D)
            ->  once(symbol:by_type_name(B, error, W, _)),
                try_restore_input(D, U, V),
                D=[U|V],
                format(atom(X), '~w', [U]),
                Q=W-X    % 2: prevents LCO
            ;   once(symbol:by_type_name(B, eof, W, _)),
                Q=W-''   % 3: prevents LCO
            ),
            S=A    % 4: prevents LCO
        ).

ad 1) Это рекурсивный случай, который вы, скорее всего, ищете. Здесь вроде все хорошо.

объявление 2,3) Уже обсуждалось выше, возможно вы хотите обменять голы

объявление 4) Это результат точного и последовательного подхода к {}//1 в DCG. Практическое правило: разработчики предпочитают быть стойкими, чем стремиться к LCO. См.: Расширение DCG: игнорируется ли стойкость?

Также обратите внимание, что это намного больше, чем простое повторное использование кадра вызова. Есть много взаимодействия со сборкой мусора. Чтобы преодолеть все эти проблемы в SWI, была необходима дополнительная фаза сборки мусора.

Для получения дополнительной информации см. Крошечные тесты в Точная сборка мусора в Пролог

Итак, чтобы наконец ответить на ваш вопрос: ваше правило может быть оптимизировано; при условии, что до рекурсивной цели не осталось точки выбора.


К этому также можно отнести подход на самом низком уровне. Я никогда не использую это для разработки кода: vm_list. Список в конечном итоге показывает вам, может ли SWI рассматривать LCO (при условии, что нет точки выбора).

i_call и i_callm никогда не будут выполнять LCO. Подойдет только i_depart. В 7_

?- vm_list(read_token).
========================================================================
read_token/5
========================================================================
   0 s_virgin
   1 i_exit
----------------------------------------
clause 1 ((0x1cc4710)):
----------------------------------------
   0 h_functor(parser/2)
   2 h_firstvar(5)
   4 h_firstvar(6)
   6 h_pop
   7 h_functor(lexer/3)
   9 h_functor((-)/2)
  11 h_const(dfa)
  13 h_firstvar(7)
  15 h_pop
  16 h_functor((-)/2)
  18 h_const(last_accept)
  20 h_firstvar(8)
  22 h_pop
  23 h_rfunctor((-)/2)
  25 h_const(chars)
  27 h_firstvar(9)
  29 h_pop
  30 i_enter
  31 c_ifthenelse(26,118)
  34 b_unify_var(3)
  36 h_list_ff(10,11)
  39 b_unify_exit
  40 b_var(6)
  42 b_var(7)
  44 b_firstvar(12)
  46 i_callm(dfa,dfa:current/3)
  49 b_var(10)
  51 b_firstvar(13)
  53 b_firstvar(14)
  55 i_call(char_and_code/3)
  57 b_var(6)
  59 b_var(12)
  61 b_var(14)
  63 b_firstvar(15)
  65 i_callm(dfa,dfa:find_edge/4)
  68 b_unify_fv(16,11)
  71 c_cut(26)
  73 b_const(dfa_table)
  75 b_var(6)
  77 b_var(15)
  79 b_firstvar(17)
  81 i_callm(table,table:item/4)
  84 b_var(17)
  86 b_firstvar(18)
  88 i_callm(dfa,dfa:accept/2)
  91 b_var(9)
  93 b_var(13)
  95 b_firstvar(19)
  97 i_call(atom_concat/3)
  99 b_unify_firstvar(20)
 101 b_functor(lexer/3)
 103 b_functor((-)/2)
 105 b_const(dfa)
 107 b_argvar(15)
 109 b_pop
 110 b_functor((-)/2)
 112 b_const(last_accept)
 114 b_argvar(18)
 116 b_pop
 117 b_rfunctor((-)/2)
 119 b_const(chars)
 121 b_argvar(19)
 123 b_pop
 124 b_unify_exit
 125 b_unify_fv(21,16)
 128 b_functor(parser/2)
 130 b_argvar(5)
 132 b_argvar(6)
 134 b_pop
 135 b_var(20)
 137 b_var2
 138 b_var(21)
 140 b_var(4)
 142 i_depart(read_token/5)
 144 c_var_n(22,2)
 147 c_var_n(24,2)
 150 c_jmp(152)
 152 c_ifthenelse(27,28)
 155 b_var(8)
 157 b_const(none)
 159 i_call((\=)/2)
 161 c_cut(27)
 163 b_unify_var(2)
 165 h_functor((-)/2)
 167 h_var(8)
 169 h_var(9)
 171 h_pop
 172 b_unify_exit
 173 c_var(10)
 175 c_var_n(22,2)
 178 c_var_n(24,2)
 181 c_jmp(101)
 183 c_ifthenelse(28,65)
 186 b_firstvar(10)
 188 i_call(ground/1)
 190 c_cut(28)
 192 b_functor((:)/2)
 194 b_const(symbol)
 196 b_rfunctor(by_type_name/4)
 198 b_argvar(6)
 200 b_const(error)
 202 b_argfirstvar(22)
 204 b_void
 205 b_pop
 206 i_call(once/1)
 208 b_var(10)
 210 b_firstvar(23)
 212 b_firstvar(24)
 214 i_call(try_restore_input/3)
 216 b_unify_var(10)
 218 h_list
 219 h_var(23)
 221 h_var(24)
 223 h_pop
 224 b_unify_exit
 225 b_functor(atom/1)
 227 b_argfirstvar(25)
 229 b_pop
 230 b_const('~w')
 232 b_list
 233 b_argvar(23)
 235 b_nil
 236 b_pop
 237 i_call(format/3)
 239 b_unify_var(2)
 241 h_functor((-)/2)
 243 h_var(22)
 245 h_var(25)
 247 h_pop
 248 b_unify_exit
 249 c_jmp(33)
 251 b_functor((:)/2)
 253 b_const(symbol)
 255 b_rfunctor(by_type_name/4)
 257 b_argvar(6)
 259 b_const(eof)
 261 b_argfirstvar(22)
 263 b_void
 264 b_pop
 265 i_call(once/1)
 267 b_unify_var(2)
 269 h_functor((-)/2)
 271 h_var(22)
 273 h_const('')
 275 h_pop
 276 b_unify_exit
 277 c_var(10)
 279 c_var_n(23,2)
 282 c_var(25)
 284 b_unify_vv(4,3)
 287 c_var_n(11,2)
 290 c_var_n(13,2)
 293 c_var_n(15,2)
 296 c_var_n(17,2)
 299 c_var_n(19,2)
 302 c_var(21)
 304 i_exit
person false    schedule 15.03.2013
comment
Точка выбора остается независимо от того, всегда ли Token (S в вашем скомпилированном выводе) не привязан, верно? - person Sebastian; 15.03.2013
comment
Сам код не дает лишнего выбора. Но, возможно, table: item или dfa: accept оставит открытым пункт выбора. Все это зависит. Вы можете легко это увидеть, вызвав statistics сразу в конце: Сколько осталось в локальном стеке? - person false; 15.03.2013
comment
ой! Я забыл упомянуть, что все использованные предикаты не оставляют никаких точек выбора - person Sebastian; 15.03.2013
comment
Кстати, мне нравится ваш совет о листинге, я всегда смотрю на код DCG как на исходный, иногда я забываю о сахаре синтаксиса term_Expansion. - person Sebastian; 15.03.2013
comment
Вы сделали несколько хороших замечаний об использовании listing и statistics, но я не понимаю вывод относительно точек выбора% 2,% 3,% 4. Они не открываются, когда read_token вызывает себя. Вызов read_token находится в одной ветви конструкции if-then-else. Prolog фиксируется в одной ветке и не возвращается к другой, поэтому оптимизация последнего вызова AFAIK будет работать. Возможно, нам нужно увидеть статистику локального стека, чтобы быть уверенным. - person hardmath; 16.03.2013
comment
В% 2,% 3,% 4 дело не в точках выбора. Но эти конструкции препятствуют тому, чтобы последний вызов предиката был i_depart, который является единственным местом, где может произойти LCO - статически. Точки выбора - это динамический вопрос. - person false; 16.03.2013
comment
@hardmath: рекурсивный вызов read_token - это i_depart, что означает, что его можно оптимизировать. - person false; 16.03.2013