次の方法で共有


Byrefs (バイレフ)

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

  • マネージド ポインターである byref/inref/outref 型。 実行時に無効なプログラムをコンパイルできないように、使用に制限があります。
  • byrefのような構造体。これは、byref<'T>と同じセマンティクスと同じコンパイル時制限を持つ 構造体 です。 1 つの例として、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>、基になる値の読み取りと書き込みを行うマネージド ポインターです。

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

byrefs の使用

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>と同じですが、1 つの違いを除いて、outref<'T> パラメーターを持つメソッドは、[<Out>] パラメーターを使用してメソッドを呼び出すときと同様に、タプルの戻り値の型に暗黙的に構築されます。

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 ref キーワードと out ref キーワードがサポートされています。 次の表は、C# が出力する内容を F# が解釈する方法を示しています。

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 トリオに加えて、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-like" 構造体は、スタック バインド値型です。 これがマネージド ヒープに割り当てられることはありません。 byrefのような構造体は、有効期間と非キャプチャに関する一連の強力なチェックで適用されるため、高パフォーマンスプログラミングに役立ちます。 ルールは次のとおりです。

  • これらは、関数パラメーター、メソッド パラメーター、ローカル変数、メソッドの戻り値として使用できます。
  • クラスまたは通常の構造体の静的メンバーまたはインスタンス メンバーにすることはできません。
  • クロージャ コンストラクト (async メソッドまたはラムダ式) ではキャプチャできません。
  • ジェネリック パラメーターとして使用することはできません。
    • F# 9 以降では、allow ref 構造体の反制約を使用してジェネリック パラメーターが C# で定義されている場合、この制限は緩和されます。 F# では、byref のような型を持つ型とメソッドで、このようなジェネリックをインスタンス化できます。 いくつかの例として、これは BCL デリゲート型 (Action<>Func<>)、インターフェイス (IEnumerable<>IComparable<>)、およびユーザー指定アキュムレータ関数 (String.string Create<TState>(int length, TState state, SpanAction<char, TState> action)) を持つジェネリック引数に影響します。
    • F# で byref のような型をサポートする汎用コードを作成することはできません。

|> は入力型をパラメーター化するジェネリック関数であるため、この最後のポイントは F# パイプライン スタイルのプログラミングにとって非常に重要です。 この制限は、インラインであり、その本体でインラインでないジェネリック関数を呼び出さないため、今後、|> で緩和される可能性があります。

これらのルールは使用を強く制限しますが、安全な方法でハイパフォーマンス コンピューティングの約束を果たすために行われます。

byref の戻り値

F# 関数またはメンバーからの Byref の戻り値は、生成および使用できます。 byref-returning メソッドを使用すると、値は暗黙的に逆参照されます。 例えば:

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!
    ()

これにより、最適化を使用してコンパイルするかどうかにかかわらず、異なる結果が出ることを防ぎます。