Как кэшировать хэш-коды для AST?

Я работаю над языком на F # и при тестировании обнаружил, что среда выполнения тратит более 90% своего времени на сравнение на равенство. Из-за этого язык настолько медленный, что его невозможно использовать. Во время инструментирования функция GetHashCode занимает довольно высокое место в списке как источник накладных расходов. Происходит то, что во время вызовов методов я использую тела методов (Expr) вместе с аргументами вызова в качестве ключей в словаре, и это запускает повторные обходы сегментов AST.

Для повышения производительности я хотел бы добавить узлы запоминания в AST.

type Expr =
| Add of Expr * Expr
| Lit of int
| HashNode of int * Expr

В вышеприведенном упрощенном примере я хотел бы, чтобы HashNode представляло хэш своего выражения, чтобы GetHashCode не приходилось углубляться в AST, чтобы вычислить его.

При этом я не уверен, как мне переопределить метод GetHashCode. В идеале я хотел бы повторно использовать встроенный метод хеширования и заставить его каким-то образом игнорировать только HashNode, но я не уверен, как это сделать.

Скорее всего, мне придется создать свою собственную хэш-функцию, но, к сожалению, я ничего не знаю о хеш-функциях, поэтому сейчас я немного запутался.

Альтернативная идея, которая у меня есть, состоит в том, чтобы заменить узлы уникальными идентификаторами, сохранив при этом эту хэш-функцию, но это внесет дополнительные сложности в код, которых я бы предпочел избегать, если мне это не нужно.


person Marko Grdinic    schedule 06.08.2017    source источник
comment
Зачем вам нужно сравнивать equality? Встроенная функция F# equal работает медленно, но в любом случае сравнение дерева будет дорогостоящим. Если вам просто нужно сравнить идентификатор объекта, а не равенство значений, вы можете использовать атрибут CustomEquality.   -  person Just another metaprogrammer    schedule 06.08.2017
comment
См. мой ответ на dragonnixx в этой теме. То, что я делаю, называется поливариантной специализацией, и мне нужно, чтобы она обрабатывала рекурсию в моем языке. Я думаю, что у меня есть идея, как это сделать сейчас.   -  person Marko Grdinic    schedule 06.08.2017
comment
Этот вопрос кажется немного повсеместным. Что конкретно вы спрашиваете?   -  person Dax Fohl    schedule 07.08.2017


Ответы (1)


Недавно мне понадобилась похожая вещь в TheGamma (GitHub), где я строю граф зависимостей ( вроде AST), который очень часто воссоздается (когда вы изменяете код в редакторе и он повторно анализируется), но у меня есть живые предварительные просмотры, которые могут занять некоторое время для расчета, поэтому я хотел повторно использовать как можно больше предыдущего графика. возможно.

Я делаю это так: я прикрепляю «символ» к каждому узлу. Два узла с одним и тем же символом равны, что, я думаю, вы могли бы использовать для эффективной проверки равенства:

type Expr =
  | Add of ExprNode * ExprNode
  | Lit of int

and ExprNode(expr:Expr, symbol:int) = 
  member x.Expression = expr
  member x.Symbol = symbol
  override x.GetHashCode() = symbol
  override x.Equals(y) = 
    match y with 
    | :? ExprNode as y -> y.Symbol = x.Symbol
    | _ -> false

Я держу кэш узлов - ключом является некоторый код вида узла (0 для Add, 1 для Lit и т. д.) и символы всех вложенных узлов. Для литералов я также добавляю сам номер, что будет означать, что создание одного и того же литерала дважды даст вам один и тот же узел. Итак, создание узла выглядит так:

let node expr ctx =  
  // Get the key from the kind of the expression
  // and symbols of all nested node in this expression
  let key = 
    match expr with 
    | Lit n -> [0; n]
    | Add(e1, e2) -> [1; e1.Symbol; e2.Symbol]
  // Return either a node from cache or create a new one
  match ListDictionary.tryFind key ctx with
  | Some res -> res
  | None ->
      let res = ExprNode(expr, nextId())
      ListDictionary.set key res ctx
      res

Модуль ListDictionary — это изменяемый словарь, где ключ — это список целых чисел, а nextId — обычная функция для генерации следующего идентификатора:

type ListDictionaryNode<'K, 'T> = 
  { mutable Result : 'T option
    Nested : Dictionary<'K, ListDictionaryNode<'K, 'T>> }

type ListDictionary<'K, 'V> = Dictionary<'K, ListDictionaryNode<'K, 'V>>

[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
module ListDictionary = 
  let tryFind ks dict = 
    let rec loop ks node =
      match ks, node with
      | [], { Result = Some r } -> Some r
      | k::ks, { Nested = d } when d.ContainsKey k -> loop ks (d.[k])
      | _ -> None
    loop ks { Nested = dict; Result = None }

  let set ks v dict =
    let rec loop ks (dict:ListDictionary<_, _>) = 
      match ks with
      | [] -> failwith "Empty key not supported"
      | k::ks ->
          if not (dict.ContainsKey k) then 
            dict.[k] <- { Nested = Dictionary<_, _>(); Result = None }
          if List.isEmpty ks then dict.[k].Result <- Some v
          else loop ks (dict.[k].Nested)
    loop ks dict


let nextId = 
  let mutable id = 0
  fun () -> id <- id + 1; id

Итак, я думаю, я говорю, что вам нужно будет реализовать свой собственный механизм кэширования, но это сработало для меня довольно хорошо и может подсказать, как это сделать в вашем случае!

person Tomas Petricek    schedule 06.08.2017
comment
Это довольно хороший ответ. Учитывая, что в ваших Expr уже есть ExprNode, а это означает, что хеш-вычисления будут иметь глубину не более одного уровня, необходимо ли сделать еще один шаг и использовать древовидное представление с вложенными словарями? Будет ли это быстрее, чем использовать Expr в качестве ключа напрямую? - person Marko Grdinic; 07.08.2017
comment
@MarkoGrdinic Вы правы, я думаю, что использование Expr в качестве ключа напрямую должно помочь - я думаю, что в основном я этого не делал, потому что все работает на JavaScript (через Fable), и я не хотел быть слишком предприимчивым :-) - person Tomas Petricek; 07.08.2017