🦔

SRTP のススメ

2024/12/04に公開

この記事は 2024 年 F# アドベント カレンダー の 4 日目の記事となります。


はじめに

F# には Statically Resolved Type Parameters (SRTP) という機能があります。
SRTP の概要については こちらの記事 を参考にしていただければと思いますが、本記事では SRTP のパフォーマンス上のメリットについて紹介していきます。

補足

上記の SRTP の入門記事 は古い内容となります。現在は、こんなに面倒なメソッドやプロパティ呼び出しは必要ないです。F# 7 より、以下のように関数本体内で簡単に呼び出せるようになっています。

before
let inline fx<^T when ^T: (member move: ^T -> ^T)>  (p1: ^T) (p2: ^T) =
  let v = (^T: (member move: ^T -> ^T) p1, p2)
  printfn "%A" v
after
let inline fx<^T when ^T: (member move: ^T -> ^T)> (p1: ^T) (p2: ^T) =
  let v = p1.move p2
  printfn "%A" v

パフォーマンス比較

SRTP を利用すると、型パラメータの箇所はコンパイル時に実際の型に置き換えられます。
つまり、Interface を利用したときとは異なり、実際の型を指定したときと同様のコストとなり、より高いパフォーマンスで動作させられます。

実際に簡単なコードで比較した結果が以下になります。

Method Mean Error StdDev Allocated
SRTP 272.3 ns 0.69 ns 0.61 ns -
Interface 300.2 ns 1.11 ns 0.93 ns -
Class 272.6 ns 0.82 ns 0.64 ns -

これを見ても分かる通り、Interface 経由で呼び出すよりも高速に動作します。

Benchmark code
benchmarkopen BenchmarkDotNet.Attributes
open BenchmarkDotNet.Running
open System
open System.Collections

[<Interface>]
type IFoo =
  abstract member fx: int -> int

type Foo() =
  member __.fx (i: int) = i * i
  interface IFoo with
    member __.fx (i: int) = i * i

let inline fx<^T when ^T: (member fx: int -> int)> (v: ^T) i = v.fx i
let inline fx' (v: IFoo) i = v.fx i
let inline fx'' (v: Foo) i = v.fx i

[<PlainExporter; MemoryDiagnoser>]
type Benchmark () =
  let xs = [| 0..1000 |]
  let foo = Foo()
  
  [<Benchmark>]
  member __.SRTP() =
    let mutable acc = 0
    for i in xs do
      acc <- acc + fx foo i
    acc
    
  [<Benchmark>]
  member __.Interface() =
    let mutable acc = 0
    for i in xs do
      acc <- acc + fx' foo i
    acc
  
  [<Benchmark>]
  member __.Class() =
    let mutable acc = 0
    for i in xs do
      acc <- acc + fx'' foo i
    acc
  
do
  BenchmarkRunner.Run<Benchmark>() |> ignore

このようにパフォーマンス上、大変有効な機能ですので率先して利用していきたいですね。
昨今パフォーマンスが求められる場面が増えていると思いますが、SRTP はその一助になると思います。

Discussion