Hacktoberfest 2026:メンテナが10月に向けて印を付けた、オープンで初心者向けの issue。 Hacktoberfest の issue を見る

Skip the `FSharpType.IsUnion` check when a quotation node for an instance member is built

オープン
#20,765 コメント 0 件 リアクション 0 件 担当者 0 名 GitHub で見る

メンテナーはふだん 3 日以内に返信

まだ誰も着手していません。

評価

難易度
3/5
見積もり時間
1〜2日
初心者へのやさしさ
57/100
issue の種類
機能追加
明瞭さ
おおむね明確
活発さ
活発
技術スタック
fsharp
領域
performance

調査の方向性

Start by reading checkObj and the instance member builders named in the issue, then inspect how RebuildShapeCombination reaches them. The proposed change is to check whether the receiver type is assignable to the declaring type before running the union type lookup. Verify that existing accepted and rejected receiver cases remain the same and compare instance member construction benchmarks with the static case.

索引モデルが issue の本文から書いたものです。

説明

Feature Request Needs-Triage

Is your feature request related to a problem? Please describe.

Building a quotation node for an instance property, field or method takes 5 µs, a hundred times what the node for a static member takes. Every transformation through ExprShape pays that for each such node, because RebuildShapeCombination runs the same check as Expr.PropertyGet and Expr.Call.

type Person = { Name : string; Age : int; City : string }
type Clock () =
    static member Zone = "UTC"

[<MemoryDiagnoser>]
type InstanceMember () =
    let p = Var ("p", typeof<Person>)
    let q = Var ("q", typeof<Person>)
    let person = Expr.Var p
    let age = typeof<Person>.GetProperty "Age"
    let zone = typeof<Clock>.GetProperty "Zone"
    let predicate =
        <@@ (%%person : Person).Age >= 18 && (%%person : Person).City = "Kyiv" && (%%person : Person).Name <> "" @@>

    let rec renameByShape (expr : Expr) : Expr =
        match expr with
        | ShapeVar v -> if v = p then Expr.Var q else expr
        | ShapeLambda (v, body) -> Expr.Lambda (v, renameByShape body)
        | ShapeCombination (shape, parts) -> RebuildShapeCombination (shape, List.map renameByShape parts)

    [<Benchmark>]
    member _.StaticProperty () = Expr.PropertyGet zone
    [<Benchmark>]
    member _.InstanceProperty () = Expr.PropertyGet (person, age)
    [<Benchmark>]
    member _.IsUnion () = FSharpType.IsUnion typeof<Person>
    [<Benchmark>]
    member _.RenameByShape () = renameByShape predicate
    [<Benchmark>]
    member _.RenameBySubstitute () = predicate.Substitute (fun v -> if v = p then Some (Expr.Var q) else None)
Method Mean Allocated
StaticProperty 0.05 µs 104 B
InstanceProperty 5.2 µs 1216 B
IsUnion 5.1 µs 1080 B
RenameByShape 20.1 µs 7824 B
RenameBySubstitute 0.8 µs 2264 B

BenchmarkDotNet 0.15.8, .NET 10.0.12, FSharp.Core 10.1.401, Windows 11 x64. The machine was not idle: standard deviations are up to 10 percent, the allocations are exact. A property of a class, an instance method call and RebuildShapeCombination of one such node measure like InstanceProperty.

Where it goes, as far as I read the code at a274432:

  • mkInstancePropGet, mkInstancePropSet, mkInstanceFieldGet, mkInstanceFieldSet, mkInstanceMethodCall and mkInstanceMethodCallW call checkObj, and RebuildShapeCombination calls them.

  • checkObj first asks FSharpType.IsUnion about the declaring type, for the case that the member belongs to the nested class of a union case:

    let rec loop (ty: Type) =
        if FSharpType.IsUnion ty && FSharpType.IsUnion ty.BaseType then loop ty.BaseType else ty
    
    let declType = loop membInfo.DeclaringType
    
    if not (assignableFrom declType (typeOf obj)) then
        invalidArg "obj" (SR.GetString(SR.QincorrectInstanceType))
    
  • FSharpType.IsUnion reads CompilationMappingAttribute through GetCustomAttributes and keeps nothing; for a record type that is four reads in one call (isExceptionRepr, isConstructorRepr for the type and its base type, isUnionType).

  • Expr.Substitute builds nodes without the check, hence the last row.

Describe the solution you'd like

Test the common case first: when the receiver's type is assignable to the declaring type, the node is valid and nothing else has to be asked.

let checkObj (membInfo: MemberInfo) (obj: Expr) =
    let objType = typeOf obj

    if not (assignableFrom membInfo.DeclaringType objType) then
        // as today: find the union type, test again, raise

It accepts and rejects what the current code does: loop gives the declaring type or a base type of it, and a receiver assignable to a type is assignable to its base types. I have not built FSharp.Core with it; from the table, an instance member node should then cost about what a static one costs. I can send a pull request.

Describe alternatives you've considered

  • Keeping the answer of FSharpType.IsUnion per type. It helps every caller of FSharp.Reflection, but adds a cache for the life of the process where the change above removes the question.
  • In my code: Expr.Substitute where a rewrite only replaces variables. A rewrite that replaces any other node still rebuilds every node above it.

Additional context

Found while measuring a quotation normalizer (pipes, let and [<ReflectedDefinition>] calls rewritten away) for fsprojects/FSharp.Azure.Cosmos: inlining a function with five member accesses took about 57 µs through ExprShape and 22 µs once the parameters were replaced with Expr.Substitute.

主要言語
F#
スター
4.3k
フォーク
880
平均マージ
4日 14分
マージ済み PR(30日)
99

環境構築

Codespaces で開く

このプロジェクトの開発コンテナを、あなたの GitHub アカウントでブラウザ上に起動します。

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

dotnet/fsharp のほかの issue

dotnet/fsharp の issue をすべて見る

似ている issue

Performance の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。