🦔
SRTP のススメ
この記事は 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