Цитата 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
для повторного создания объекта предложения;
Итак, исходя из этого наблюдения, мои вопросы:
- Хотя цитаты хранятся в одном статическом поле, но каждый раз, когда мы пишем
<@ ... @>
, они будут создавать новый экземплярExpr
, даже если статическое поле такое же. Поэтому я не могу использовать экземплярExpr
в качестве ключа, было бы хорошо получить токен статического поля и использовать его в качестве ключа. Но я не знаю, как получить эту информацию; - Мы видели, что существует множество инструкций 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 <@ (-) @>))