Unpickle a quotation literal once, not on every evaluation
维护者通常 3 天内回复
还没有人认领这个 Issue。
评估
- 难度
- 5/5
- 预计耗时
- 一周以上
- 新手友好度
- 35/100
- Issue 类型
- 功能
- 描述清晰度
- 基本清楚
- 活跃度
- 活跃
- 技术栈
- fsharp
- 领域
- compilers
调研方向
Start by reading IlxGen.GenQuotation, Expr.Deserialize40, unpickleExpr, and fillHolesInRawExpr; the issue identifies these entry points and describes the repeated work. The proposed cache key and static field approach are examples, and the issue notes unresolved questions about generic instantiations and bound variable identity. Done means quotation literals reuse an unpickled tree while still filling captured values correctly, with benchmarks showing the cost change.
由索引模型根据 Issue 内容生成。
描述
Is your feature request related to a problem? Please describe.
Every evaluation of a quotation literal costs tens of microseconds and tens of kilobytes, however often the same literal was evaluated before. A library that takes quotations as its input (a query translator, in my case) cannot avoid that with a cache of its own: it receives the tree only after it is built.
open BenchmarkDotNet.Attributes
open BenchmarkDotNet.Running
open FSharp.Quotations
type Person = { Name : string; Age : int; City : string; Nick : string option; Tags : string list }
[<MemoryDiagnoser>]
type QuotationLiteral () =
let minAge = 18
let city = "Kyiv"
// Evaluated once: reading it afterwards costs nothing
let held : Expr<Person -> bool> = <@ fun p -> p.Age >= 18 @>
[<Benchmark(Description = "One condition, no captured value, held in a field")>]
member _.Held () : Expr<Person -> bool> = held
[<Benchmark(Description = "One condition, no captured value")>]
member _.Closed () : Expr<Person -> bool> = <@ fun p -> p.Age >= 18 @>
[<Benchmark(Description = "One condition, one captured value")>]
member _.OneCondition () : Expr<Person -> bool> = <@ fun p -> p.Age >= minAge @>
[<Benchmark(Description = "Four conditions, two captured values")>]
member _.FourConditions () : Expr<Person -> bool> =
<@ fun p -> p.Age >= minAge && p.City = city && p.Nick.IsSome && List.contains "vip" p.Tags @>
[<EntryPoint>]
let main arguments =
BenchmarkSwitcher.FromAssembly(typeof<QuotationLiteral>.Assembly).Run arguments |> ignore
0
| Method | Mean | Allocated |
|---|---|---|
| One condition, no captured value, held in a field | 0.5 ns | 0 |
| One condition, no captured value | 14.7 µs | 19.9 KB |
| One condition, one captured value | 22.0 µs | 20.5 KB |
| Four conditions, two captured values | 116 µs | 51.1 KB |
BenchmarkDotNet 0.15.8, .NET 10.0.12, FSharp.Core 10.1.401 (SDK 10.0.401), Windows 11 x64, Release. The machine was not idle, so the times are approximate (the last one has a standard deviation of 37 µs); the allocations are exact.
Where it goes, as far as I read the code at a274432:
IlxGen.GenQuotationpickles the tree at compile time and emits, in place of the literal, a call ofExpr.Deserialize40 (type, referencedTypes, spliceTypes, spliceExprs, bytes). The bytes are a new array on every evaluation (newarrandRuntimeHelpers.InitializeArray).Deserialize40runsunpickleExprover the bytes on every call and thenfillHolesInRawExprwith the spliced expressions, which are the captured values.- Unpickling resolves every type, method and property through reflection again;
bindMethodHelper, for one, callsType.GetMethod (name, flags, null, argTypes, null). - Nothing is kept between two evaluations. Only the bodies of
[<ReflectedDefinition>]members have a table (reflectedDefinitionTable).
Describe the solution you'd like
Unpickle a literal once and keep the tree with its holes. A later evaluation only fills the holes, and a literal without splices can give back the same instance.
The tree with holes depends on the pickled bytes, the referenced type definitions and the type splices, not on the spliced expressions. FSharp.Core has no key to cache by today, because the byte array is a new one each time, so the compiler would have to provide one: for example a static field per literal that holds the unpickled tree, passed to a new entry point next to Deserialize40. A literal with type splices, one inside generic code, needs one tree per instantiation.
I have not prototyped this, so I cannot say how much of the cost it removes. One thing to check is the identity of bound variables: two evaluations of one literal would share their Var objects, as two uses of one quotation value do today.
Describe alternatives you've considered
- Holding the quotation in a value that is evaluated once and passing what changes as arguments of the quoted lambda (the first row of the table). That works where the author of the quotation can restructure the code, not for the caller of a library who writes the quotation at the call.
- A cache inside the library that receives the quotation. It comes too late: the cost is paid before the library is called.
Additional context
Found while measuring the cost model of a quotation-based query translator for Azure Cosmos DB in fsprojects/FSharp.Azure.Cosmos. There a query round trip to the local emulator takes about 2 ms and everything the library does per cached query about 1 µs, so the literal is by far the largest cost on the client.
- 主要语言
- F#
- 星标
- 4.3k
- 派生
- 881
- 平均合并
- 3 天 23 小时
- 30 天内合并 PR
- 110
环境准备
在浏览器里用你自己的 GitHub 账号启动这个项目的开发容器。
- 没有 Dockerfile 或 Docker Compose 文件
- 有 Pull Request 模板
- 阅读贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
dotnet/fsharp 的其他 Issue
-
Needs-Triage
难度 1/5 1 小时以内 新手友好度 78/100
维护者通常 3 天内回复
-
Semantic classification cache for opened documents is never populated (written to the unopened-documents cache)可能已有人在做 @xperiandri 于 41 天前认领。 未关闭Needs-Triage
难度 1/5 1 小时以内 新手友好度 90/100
维护者通常 3 天内回复
-
Needs-Triage
难度 2/5 1-3 小时 新手友好度 78/100
维护者通常 3 天内回复
-
Bug Needs-Triage
难度 2/5 1-3 小时 新手友好度 78/100
维护者通常 3 天内回复
-
Feature Request
难度 3/5 1-2 天 新手友好度 57/100
dotnet/fsharp#20765 · 1 条评论 · 1 个 reaction ·
维护者通常 3 天内回复
相似的 Issue
-
A `--symbolic` entry binding `𝑛3` and `!n3` passes the load check, since `once` compares spellings, and fails mid-run with a `user error`可能已有人在做 @sherenkov 今天认领。 未关闭
难度 2/5 1-3 小时 新手友好度 66/100
objectionary/phino#1929 · 1 条评论 ·
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 68/100
FuRongJun-1999/dsh-memory#402 ·
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 66/100
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 70/100
-
bug frontend
难度 2/5 1-3 小时 新手友好度 68/100
PennyLaneAI/catalyst#3337 ·
维护者通常 1 天内回复