Можно ли получить идентификатор цитаты F#?

Цитата F# — замечательная функция, она позволяет нам рассматривать выражение F# как обычное значение F#. В моем контексте я использую цитату F # для кода ядра Gpu и компилирую его в модуль битового кода Gpu.

Есть одна проблема. Я не хочу каждый раз компилировать ядро ​​​​Gpu, я хотел бы кэшировать скомпилированный модуль битового кода Gpu. Таким образом, мне нужен ключ или идентификатор из значения цитаты F #. Я хотел бы иметь систему кэширования, например:

let compile : Expr -> GpuModule

let cache = ConcurrentDictionary<Key, GpuModule>()

let jitCompile (expr:Expr) =
    let key = getQuotationKey(expr)
    cache.GetOrAdd(key, fun key -> compile expr)

Есть одно решение — использовать экземпляр выражения цитаты в качестве ключа. Но посмотрите на этот кусок кода:

open Microsoft.FSharp.Quotations

let foo (expr:Expr) =
    printfn "%O" expr

[<EntryPoint>]
let main argv = 

    for i = 1 to 10 do
        foo <@ fun x y -> x + y @>

    0

Если я проверю скомпилированный код IL, я получу следующие инструкции IL:

IL_0000: nop
IL_0001: ldc.i4.1
IL_0002: stloc.0
IL_0003: br IL_00a2
// loop start (head: IL_00a2)
    IL_0008: ldtoken '<StartupCode$ConsoleApplication2>.$Program'
    IL_000d: call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle)
    IL_0012: ldc.i4.5
    IL_0013: newarr [mscorlib]System.Type
    IL_0018: dup
    IL_0019: ldc.i4.0
    IL_001a: ldtoken [mscorlib]System.Int32
    IL_001f: call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle)
    IL_0024: stelem.any [mscorlib]System.Type
    IL_0029: dup
    IL_002a: ldc.i4.1
    IL_002b: ldtoken [FSharp.Core]Microsoft.FSharp.Core.Operators
    IL_0030: call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle)
    IL_0035: stelem.any [mscorlib]System.Type
    IL_003a: dup
    IL_003b: ldc.i4.2
    IL_003c: ldtoken [mscorlib]System.Tuple`2
    IL_0041: call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle)
    IL_0046: stelem.any [mscorlib]System.Type
    IL_004b: dup
    IL_004c: ldc.i4.3
    IL_004d: ldtoken [mscorlib]System.String
    IL_0052: call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle)
    IL_0057: stelem.any [mscorlib]System.Type
    IL_005c: dup
    IL_005d: ldc.i4.4
    IL_005e: ldtoken [mscorlib]System.Tuple`5
    IL_0063: call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle)
    IL_0068: stelem.any [mscorlib]System.Type
    IL_006d: ldc.i4.0
    IL_006e: newarr [mscorlib]System.Type
    IL_0073: ldc.i4.0
    IL_0074: newarr [FSharp.Core]Microsoft.FSharp.Quotations.FSharpExpr
    IL_0079: ldc.i4 372
    IL_007e: newarr [mscorlib]System.Byte
    IL_0083: dup
    IL_0084: ldtoken field valuetype '<PrivateImplementationDetails$ConsoleApplication2>'/T1805_372Bytes@ Program::field1806@
    IL_0089: call void [mscorlib]System.Runtime.CompilerServices.RuntimeHelpers::InitializeArray(class [mscorlib]System.Array, valuetype [mscorlib]System.RuntimeFieldHandle)
    IL_008e: call class [FSharp.Core]Microsoft.FSharp.Quotations.FSharpExpr [FSharp.Core]Microsoft.FSharp.Quotations.FSharpExpr::Deserialize40(class [mscorlib]System.Type, class [mscorlib]System.Type[], class [mscorlib]System.Type[], class [FSharp.Core]Microsoft.FSharp.Quotations.FSharpExpr[], uint8[])
    IL_0093: call class [FSharp.Core]Microsoft.FSharp.Quotations.FSharpExpr`1<!!0> [FSharp.Core]Microsoft.FSharp.Quotations.FSharpExpr::Cast<class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<int32, class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<int32, int32>>>(class [FSharp.Core]Microsoft.FSharp.Quotations.FSharpExpr)
    IL_0098: call void Program::foo(class [FSharp.Core]Microsoft.FSharp.Quotations.FSharpExpr)
    IL_009d: nop
    IL_009e: ldloc.0
    IL_009f: ldc.i4.1
    IL_00a0: add
    IL_00a1: stloc.0

    IL_00a2: ldloc.0
    IL_00a3: ldc.i4.s 11
    IL_00a5: blt IL_0008
// end loop

IL_00aa: ldc.i4.0
IL_00ab: ret

Это большой код, но в основном он выполняет следующие действия в цикле:

  • Загрузить байтовый массив для котировки из некоторого статического поля
  • Информация о типе установки
  • Вызовите FSharp.Quotations.FSharpExpr::Deserialize40 для повторного создания объекта предложения;

Итак, исходя из этого наблюдения, мои вопросы:

  1. Хотя цитаты хранятся в одном статическом поле, но каждый раз, когда мы пишем <@ ... @>, они будут создавать новый экземпляр Expr, даже если статическое поле такое же. Поэтому я не могу использовать экземпляр Expr в качестве ключа, было бы хорошо получить токен статического поля и использовать его в качестве ключа. Но я не знаю, как получить эту информацию;
  2. Мы видели, что существует множество инструкций IL для повторного создания экземпляра цитаты, даже если это одна и та же цитата. Это может иметь некоторые проблемы с производительностью, можно ли здесь оптимизировать компилятор F#?

С уважением, Сян.

@kvb дал прекрасный ответ. Похоже, нам просто нужно исправить сравнение Var в кавычках (когда var имеет аналог и имеет тот же тип). Следуйте его ответу. Я сделал следующие тесты, и это работает:

let comparer =
    let rec compareQuots vs = function
        | ShapeLambda(v,e), ShapeLambda(v',e') ->
            compareQuots (vs |> Map.add v v') (e,e')
        | ShapeCombination(o,es), ShapeCombination(o',es') ->
            o = o' && (es.Length = es'.Length) && List.forall2 (fun q1 q2 -> compareQuots vs (q1, q2)) es es'
        | ShapeVar v, ShapeVar v' when Map.tryFind v vs = Some v' && v.Type = v'.Type ->
            true
        | _ -> false

    let rec hashQuot n vs = function
        | ShapeLambda(v,e) ->
            hashQuot (n+1) (vs |> Map.add v n) e
        | ShapeCombination(o,es) ->
            es |> List.fold (fun h e -> 31 * h + hashQuot n vs e) (o.GetHashCode())
        | ExprShape.ShapeVar v ->
            Map.find v vs

    { new System.Collections.Generic.IEqualityComparer<_> with 
        member __.Equals(q1,q2) = compareQuots Map.empty (q1,q2)
        member __.GetHashCode q = hashQuot 0 Map.empty q }

type Module = int

let mutable counter = 0

let compile (expr:Expr) =
    counter <- counter + 1
    printfn "Compiling #.%d module..." counter
    counter

let cache = ConcurrentDictionary<Expr, Module>(comparer)

let jitCompile (expr:Expr) =
    cache.GetOrAdd(expr, compile)

[<Test>]
let testJITCompile() =
    Assert.AreEqual(1, jitCompile <@ fun x y -> x + y @>)
    Assert.AreEqual(1, jitCompile <@ fun x y -> x + y @>)
    Assert.AreEqual(1, jitCompile <@ fun a b -> a + b @>)
    Assert.AreEqual(2, jitCompile <@ fun a b -> a + b + 1 @>)

    let combineExpr (expr:Expr<int -> int -> int>) =
        <@ fun (a:int) (b:int) -> ((%expr) a b) + 1 @> 

    // although (combineExpr <@ (+) @>) = <@ fun a b -> a + b + 1 @>
    // but they are treated as different expr.
    Assert.AreEqual(3, jitCompile (combineExpr <@ (+) @>))
    Assert.AreEqual(3, jitCompile (combineExpr <@ (+) @>))
    Assert.AreEqual(4, jitCompile (combineExpr <@ (-) @>))

person Xiang Zhang    schedule 05.01.2016    source источник


Ответы (1)


Создание нового объекта каждый раз в цикле не обязательно означает, что объект нельзя использовать в качестве ключа, если объекты каждый раз сравниваются одинаково.

Реальная проблема, с которой вы столкнетесь, заключается в том, что «та же самая» цитата означает для вас нечто иное, чем для компилятора F#, особенно когда речь идет о переменных в кавычках. Например, вы можете убедиться, что

<@ [1 + 1] @> = <@ [1 + 1] @>

оценивается как true, и

<@ fun x -> x @> = <@ fun y -> y @>

оценивается как false (что, надеюсь, имеет смысл, поскольку лямбды эквивалентны вплоть до переименования, но не идентичны). Возможно, что еще более удивительно, вы увидите, что

<@ fun x -> x @> = <@ fun x -> x @>

также оценивается как false. Это связано с тем, что переменные в каждой цитате рассматриваются как разные переменные, которые просто имеют одно и то же имя. Вы увидите такое же поведение в своем цикле — переменная x каждой итерации считается разной.

Однако не все потеряно; все, что вам нужно сделать, это использовать пользовательский файл IEqualityComparer<Quotations.Expr>. Я думаю, что что-то вроде этого должно работать, чтобы идентифицировать любые цитаты, которые идентичны переименованию переменной по модулю:

let comparer = 
    let rec compareQuots vs = function
    | Quotations.ExprShape.ShapeLambda(v,e), Quotations.ExprShape.ShapeLambda(v',e') ->
        compareQuots (vs |> Map.add v v') (e,e')
    | Quotations.ExprShape.ShapeCombination(o,es), Quotations.ExprShape.ShapeCombination(o',es') ->
        o = o' && (es.Length = es'.Length) && List.forall2 (fun q1 q2 -> compareQuots vs (q1, q2)) es es'
    | Quotations.ExprShape.ShapeVar v, Quotations.ExprShape.ShapeVar v' when Map.tryFind v vs = Some v' && v.Type = v'.Type -> 
        true
    | _ -> false

    let rec hashQuot n vs = function
    | Quotations.ExprShape.ShapeLambda(v,e) -> 
        hashQuot (n+1) (vs |> Map.add v n) e
    | Quotations.ExprShape.ShapeCombination(o,es) -> 
        es |> List.fold (fun h e -> 31 * h + hashQuot n vs e) (o.GetHashCode())
    | Quotations.ExprShape.ShapeVar v -> 
        Map.find v vs

    { new System.Collections.Generic.IEqualityComparer<_> with 
        member __.Equals(q1,q2) = compareQuots Map.empty (q1,q2)
        member __.GetHashCode q = hashQuot 0 Map.empty q }

let cache = ConcurrentDictionary<Expr, Module>(comparer)
person kvb    schedule 05.01.2016
comment
Это очень умно :) Сейчас тестирую. - person Xiang Zhang; 06.01.2016
comment
это прекрасное решение, похоже, нам нужно просто исправить сравнение типа Var. Я сделал несколько тестов и обновил их до вопроса. Тесты пройдены, спасибо! - person Xiang Zhang; 06.01.2016
comment
Теперь единственная проблема заключается в том, что для построения объектов кавычек используется много кода IL, даже если они представляют одну и ту же функцию. Это будет иметь небольшие проблемы с производительностью (особенно когда мы хотим сделать ускорение графического процессора). Но я предполагаю, что это должна быть некоторая оптимизация в компиляторе F #. - person Xiang Zhang; 06.01.2016
comment
Это отвечает только на часть 1. но не затрагивает вопрос 2, что компилятор F # генерирует множество инструкций IL, чтобы просто воссоздать экземпляр цитаты. Это серьезная проблема производительности, которая должна быть улучшена. Есть идеи как? - person Daniel; 06.09.2016
comment
@Daniel - я не могу вспомнить многих ситуаций, когда создание большого количества цитат в узком цикле имеет смысл, поэтому, даже если бы это можно было значительно улучшить, я сомневаюсь, что это будет иметь заметное значение для большинства пользователей. Таким образом, я предполагаю, что есть много более эффективных способов улучшить компилятор (но я рад убедиться в обратном, если у вас есть убедительный вариант использования). - person kvb; 07.09.2016
comment
@Daniel - (и, как я указываю в своем ответе, это не просто повторное создание экземпляра цитаты, поскольку семантика языка требует, чтобы экземпляры были структурно неравными). - person kvb; 07.09.2016
comment
@kvb На самом деле цель этого использования в том, что у нас есть функция, определенная в F #, затем мы хотим запустить ее в графическом процессоре. В C# мы можем использовать делегат, чтобы указать, какую функцию вы хотите (а также тип аргумента выводится, поэтому типобезопасен), мы хотим иметь эту функцию, как в C#, цитата — это способ помочь нам найти метод, также с безопасный тип. - person Xiang Zhang; 08.09.2016