f# - 我可以获得 F# 引用的标识吗?

标签 f# quotations

F# 引用是一个很棒的功能,它允许我们将 F# 表达式视为正常的 F# 值。在我的上下文中,我使用 F# 引用对 Gpu 内核进行编码,并将其编译为 Gpu bitcode 模块。

有一个问题。我不想每次都编译 Gpu 内核,我想缓存编译后的 Gpu bitcode 模块。因此我需要一个键,或者来自 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)

有一种解决方案,使用引号 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实例作为键,最好获取静态字段 token 并将其用作键。但我不知道如何获取这些信息;
  • 我们看到有很多 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 <@ (-) @>))
    

    最佳答案

    每次通过循环创建一个新对象并不一定意味着该对象不能用作键,只要对象每次比较相等即可。

    您将遇到的真正问题是,“相同”的引用对您来说意味着与 F# 编译器不同的东西,尤其是在引用中的变量方面。例如,您可以验证

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

    计算结果为 true , 和
    <@ fun x -> x @> = <@ fun y -> y @>
    

    计算结果为 false (希望这是有道理的,因为 lambda 相当于重命名,但不完全相同)。也许更令人惊讶的是,你会看到
    <@ 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)
    

    关于f# - 我可以获得 F# 引用的标识吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/34611616/

    相关文章:

    f# - F# 命名约定是否禁止在不同光盘中使用相同的名称。工会类型?

    javascript - 在 WebSharper 中的整数类型之间转换

    azure - F# Azure 辅助角色和目标运行时

    f# - F# 中对象/记录的无点样式

    build - 我对我应该引用哪个 fsharp.core 以及如何去做感到困惑

    sql - 如何用单引号插入文本sql server 2005

    types - 什么时候更喜欢F#中的无类型引用而不是有类型的引用?

    php - 如何从文本(PHP)中提取引文?

    reflection - 在 F# 引号内嵌入变量

    project-management - 如何为设计 session 制定固定报价?