Byrefs

F# には、低レベルのプログラミングの領域を扱う 2 つの主要な機能領域があります。

  • byref/inref/outref 型。これはマネージド ポインターです。 これらには、実行時に無効なプログラムをコンパイルできないように、使用方法に制限があります。
  • byref に似た構造体。これは、byref<'T> と似たセマンティクスおよび同じコンパイル時の制限を持つ構造体です。 Span<T> が一例です。

構文

// Byref types as parameters
let f (x: byref<'T>) = ()
let g (x: inref<'T>) = ()
let h (x: outref<'T>) = ()

// Calling a function with a byref parameter
let mutable x = 3
f &x

// Declaring a byref-like struct
open System.Runtime.CompilerServices

[<Struct; IsByRefLike>]
type S(count1: int, count2: int) =
    member x.Count1 = count1
    member x.Count2 = count2

byref、inref、および outref

byref には 3 つの形式があります。

  • inref<'T> は、基になる値を読み取るためのマネージド ポインターです。
  • outref<'T> は、基になる値に書き込むためのマネージド ポインターです。
  • byref<'T> は、基になる値の読み取りと書き込みを行うためのマネージド ポインターです。

inref<'T> が想定される場合は、byref<'T> を渡すことができます。 同様に、outref<'T> が想定される場合は、byref<'T> を渡すことができます。

byref の使用

inref<'T> を使用するには、& を使用してポインター値を取得する必要があります。

open System

let f (dt: inref<DateTime>) =
    printfn $"Now: %O{dt}"

let usage =
    let dt = DateTime.Now
    f &dt // Pass a pointer to 'dt'

outref<'T> または byref<'T> を使用してポインターに書き込むには、ポインターを取得する値も mutable にする必要があります。

open System

let f (dt: byref<DateTime>) =
    printfn $"Now: %O{dt}"
    dt <- DateTime.Now

// Make 'dt' mutable
let mutable dt = DateTime.Now

// Now you can pass the pointer to 'dt'
f &dt

ポインターを読み取る代わりに書き込みだけを行う場合は、byref<'T> の代わりに outref<'T> を使用することを検討してください。

inref セマンティクス

次のコードがあるとします。

let f (x: inref<SomeStruct>) = x.SomeField

意味的には、これは次のことを意味します。

  • x ポインターの所有者は、それを使用して値を読み取ることのみできます。
  • SomeStruct 内の入れ子になった struct フィールドに対して取得されたポインターには、inref<_> 型が指定されます。

次のことも当てはまります。

  • 他のスレッドまたは別名に x への書き込みアクセス権がないということではありません。
  • xinref であるという理由で SomeStruct が不変であることにはなりません。

ただし、不変の F# 値型の場合、this ポインターは inref として推論されます。

これらのすべての規則が組み合わされると、inref ポインターの所有者が、ポイントされているメモリの直接の内容を変更できないことを意味します。

outref セマンティクス

outref<'T> の目的は、ポインターの書き込みだけが必要であることを示すことです。 意外にも、outref<'T> では、その名前にもかかわらず基になる値の読み取りが許可されます。 これは、互換性のみを目的としています。

意味的には、outref<'T>byref<'T> と何ら変わりません。ただし、[<Out>] パラメーターを含むメソッドを呼び出す場合と同様に、outref<'T> パラメーターを含むメソッドがタプル戻り値の型に暗黙的に構築されているという 1 点においてのみ異なります。

type C =
    static member M1(x, y: _ outref) =
        y <- x
        true

match C.M1 1 with
| true, 1 -> printfn "Expected" // Fine with outref, error with byref
| _ -> printfn "Never matched"

C# との相互運用

C# では、ref 戻り値に加えて、in refout ref のキーワードがサポートされています。 F# によって C# の出力がどのように解釈されるかを次の表に示します。

C# コンストラクト F# の推論
ref 戻り値 outref<'T>
ref readonly 戻り値 inref<'T>
in ref パラメーター inref<'T>
out ref パラメーター outref<'T>

F# の出力を次の表に示します。

F# コンストラクト 出力されるコンストラクト
inref<'T> 引数 引数の [In] 属性
inref<'T> 戻り値 値の modreq 属性
抽象スロットまたは実装内の inref<'T> 引数または戻り値の modreq
outref<'T> 引数 引数の [Out] 属性

型の推定とオーバーロードの規則

inref<'T> 型は、次の場合に F# コンパイラによって推定されます。

  1. IsReadOnly 属性を持つ .NET パラメーターまたは戻り値の型。
  2. 変更可能なフィールドを持たない構造体型の this ポインター。
  3. 別の inref<_> ポインターから派生したメモリ位置のアドレス。

inref の暗黙的なアドレスを取得する場合、SomeType 型の引数を持つオーバーロードは、inref<SomeType> 型の引数を持つオーバーロードよりも優先されます。 次に例を示します。

type C() =
    static member M(x: System.DateTime) = x.AddDays(1.0)
    static member M(x: inref<System.DateTime>) = x.AddDays(2.0)
    static member M2(x: System.DateTime, y: int) = x.AddDays(1.0)
    static member M2(x: inref<System.DateTime>, y: int) = x.AddDays(2.0)

let res = System.DateTime.Now
let v =  C.M(res)
let v2 =  C.M2(res, 4)

どちらの場合も、inref<System.DateTime> を受け取るオーバーロードではなく System.DateTime を受け取るオーバーロードが解決されます。

byref に似た構造体

byref/inref/outref の 3 つに加えて、byref に似たセマンティクスに従うことができる独自の構造体を定義することができます。 これは IsByRefLikeAttribute 属性を使用して行われます。

open System
open System.Runtime.CompilerServices

[<IsByRefLike; Struct>]
type S(count1: Span<int>, count2: Span<int>) =
    member x.Count1 = count1
    member x.Count2 = count2

IsByRefLikeStruct を意味するものではありません。 どちらも型に存在する必要があります。

F# の "byref に似た" 構造体は、スタックバインド値の型です。 これがマネージド ヒープに割り当てられることはありません。 byref に似た構造体は、有効期間と非キャプチャに関する厳密なチェック セットを使用して適用されるため、ハイパフォーマンス プログラミングに役立ちます。 規則は以下のとおりです。

  • これらは、関数パラメーター、メソッド パラメーター、ローカル変数、メソッドの戻り値として使用できます。
  • これらは、クラスまたは通常構造体の静的またはインスタンス メンバーにすることはできません。
  • これらは、クロージャ コンストラクト (async メソッドまたはラムダ式) によってキャプチャすることはできません。
  • これらは、ジェネリック パラメーターとして使用することはできません。

この最後の点は F# パイプライン スタイルのプログラミングに不可欠です。|> はその入力の型をパラメーター化するジェネリック関数であるからです。 |> に対するこの制限は、将来緩和される可能性があります。これはインラインであり、その本体で非インライン ジェネリック関数を呼び出すことはないからです。

これらの規則によって使用法が厳密に制限されますが、そうするのはハイパフォーマンス コンピューティングの約束事を安全な方法で実現するためです。

byref の戻り値

F# 関数またはメンバーから byref の戻り値を生成して使用することができます。 byref を返すメソッドを使用する場合、値は暗黙的に逆参照されます。 次に例を示します。

let squareAndPrint (data : byref<int>) =
    let squared = data*data    // data is implicitly dereferenced
    printfn $"%d{squared}"

byref 値を返すには、値を含む変数が現在のスコープよりも長く有効である必要があります。 また、byref を返すには、&value (値は現在のスコープよりも長く有効な変数です) を使用します。

let mutable sum = 0
let safeSum (bytes: Span<byte>) =
    for i in 0 .. bytes.Length - 1 do
        sum <- sum + int bytes[i]
    &sum  // sum lives longer than the scope of this function.

複数の連鎖呼び出しを通じて参照を渡すなど、暗黙的な逆参照を回避するには、&x (x は値) を使用します。

戻り値を byref に直接割り当てることもできます。 次のような (必須の) プログラムを考えてみましょう。

type C() =
    let mutable nums = [| 1; 3; 7; 15; 31; 63; 127; 255; 511; 1023 |]

    override _.ToString() = String.Join(' ', nums)

    member _.FindLargestSmallerThan(target: int) =
        let mutable ctr = nums.Length - 1

        while ctr > 0 && nums[ctr] >= target do ctr <- ctr - 1

        if ctr > 0 then &nums[ctr] else &nums[0]

[<EntryPoint>]
let main argv =
    let c = C()
    printfn $"Original sequence: %O{c}"

    let v = &c.FindLargestSmallerThan 16

    v <- v*2 // Directly assign to the byref return

    printfn $"New sequence:      %O{c}"

    0 // return an integer exit code

出力結果は次のとおりです。

Original sequence: 1 3 7 15 31 63 127 255 511 1023
New sequence:      1 3 7 30 31 63 127 255 511 1023

byref のスコープ

let バインドされた値は、その参照が定義されているスコープを超えることはできません。 たとえば、以下は許可されません。

let test2 () =
    let x = 12
    &x // Error: 'x' exceeds its defined scope!

let test () =
    let x =
        let y = 1
        &y // Error: `y` exceeds its defined scope!
    ()

これにより、最適化を使用してコンパイルするかどうかに応じて異なる結果が生成されるのを防ぐことができます。