2️⃣

Go 1.25: reflect.TypeAssert vs reflect.Value#Interface + 型アサーション

に公開

reflect.TypeAssert

Go 1.25でreflectパッケージへ導入が予定されている関数でreflect.Value#Interface + 型アサーション を代替するためのものです

rv := reflect.ValueOf(new(time.Time)).Elem()
- v, ok = rv.Interface().(time.Time)
+ v, ok = reflect.TypeAssert[time.Time](rv)

元となったプロポーザル[1]によるとreflect.Value#Interfaceによるアロケーションを防ぐことがモチベーションとのことなので実際にベンチマークテストで比較していきます

検証

https://github.com/miyamo2/reflect_type_assert_vs_type_assertion

package reflect_type_assert_vs_type_assertion

import (
	"reflect"
	"testing"
	"time"
)

func BenchmarkTypeAssertion(b *testing.B) {
	rv := reflect.ValueOf(new(time.Time)).Elem()
	b.ReportAllocs()
	for b.Loop() {
		_, ok := rv.Interface().(time.Time)
		if !ok {
			b.Fatal("expected true but got false")
		}
	}
}

func BenchmarkReflectTypeAssert(b *testing.B) {
	rv := reflect.ValueOf(new(time.Time)).Elem()
	b.ReportAllocs()
	for b.Loop() {
		_, ok := reflect.TypeAssert[time.Time](rv)
		if !ok {
			b.Fatal("expected true but got false")
		}
	}
}

プロポーザルに記載のサンプルコードを参考に、上記のテストコードで検証していきます

go test -bench '^BenchmarkTypeAssertion$|^BenchmarkReflectTypeAssert$' -count 6
goos: linux
goarch: amd64
pkg: github.com/miyamo2/reflect_type_assert_vs_type_assertion
cpu: AMD EPYC 7763 64-Core Processor                
BenchmarkTypeAssertion-16               35453163                34.60 ns/op           24 B/op          1 allocs/op
BenchmarkTypeAssertion-16               34472107                35.62 ns/op           24 B/op          1 allocs/op
BenchmarkTypeAssertion-16               35326629                34.59 ns/op           24 B/op          1 allocs/op
BenchmarkTypeAssertion-16               35174138                34.63 ns/op           24 B/op          1 allocs/op
BenchmarkTypeAssertion-16               34724137                34.60 ns/op           24 B/op          1 allocs/op
BenchmarkTypeAssertion-16               33512199                34.64 ns/op           24 B/op          1 allocs/op
BenchmarkReflectTypeAssert-16           294901711                4.058 ns/op           0 B/op          0 allocs/op
BenchmarkReflectTypeAssert-16           293091184                4.113 ns/op           0 B/op          0 allocs/op
BenchmarkReflectTypeAssert-16           295116627                4.052 ns/op           0 B/op          0 allocs/op
BenchmarkReflectTypeAssert-16           296398741                4.044 ns/op           0 B/op          0 allocs/op
BenchmarkReflectTypeAssert-16           296427202                4.047 ns/op           0 B/op          0 allocs/op
BenchmarkReflectTypeAssert-16           296555486                4.043 ns/op           0 B/op          0 allocs/op
PASS
ok      github.com/miyamo2/reflect_type_assert_vs_type_assertion        14.460s

reflect.Value#Interface + 型アサーションではアロケーションが発生するのに対し、TypeAssertはゼロアロケーションであることが確認できました
次にbenchstatで処理時間の平均を取って比較していきます

go tool benchstat -filter ".unit:(ns/op)" benchstat.txt
goos: linux
goarch: amd64
pkg: github.com/miyamo2/reflect_type_assert_vs_type_assertion
cpu: AMD EPYC 7763 64-Core Processor                
                     │ benchstat.txt │
                     │    sec/op     │
TypeAssertion-16         34.62n ± 3%
ReflectTypeAssert-16     4.049n ± 2%
geomean                  11.84n

reflect.TypeAssertがおおよそ8.5倍速いです

どちらもゼロアロケーションとなるケース

func BenchmarkTypeAssertionToPointer(b *testing.B) {
	rv := reflect.ValueOf(new(time.Time))
	b.ReportAllocs()
	for b.Loop() {
		_, ok := rv.Interface().(*time.Time)
		if !ok {
			b.Fatal("expected true but got false")
		}
	}
}

func BenchmarkReflectTypeAssertToPointer(b *testing.B) {
    rv := reflect.ValueOf(new(time.Time))
    b.ReportAllocs()
    for b.Loop() {
        _, ok := reflect.TypeAssert[*time.Time](rv)
        if !ok {
            b.Fatal("expected true but got false")
        }
    }
}

次はreflect.Value#Elemを介さず、*Tに型アサーションします

go test -bench '^.*Pointer$' -count 6
goos: linux
goarch: amd64
pkg: github.com/miyamo2/reflect_type_assert_vs_type_assertion
cpu: AMD EPYC 7763 64-Core Processor                
BenchmarkTypeAssertionToPointer-16              192637333                6.229 ns/op           0 B/op          0 allocs/op
BenchmarkTypeAssertionToPointer-16              192209920                6.242 ns/op           0 B/op          0 allocs/op
BenchmarkTypeAssertionToPointer-16              192223932                6.237 ns/op           0 B/op          0 allocs/op
BenchmarkTypeAssertionToPointer-16              192543849                6.249 ns/op           0 B/op          0 allocs/op
BenchmarkTypeAssertionToPointer-16              192620155                6.228 ns/op           0 B/op          0 allocs/op
BenchmarkTypeAssertionToPointer-16              192099916                6.239 ns/op           0 B/op          0 allocs/op
BenchmarkReflectTypeAssertToPointer-16          321093446                3.742 ns/op           0 B/op          0 allocs/op
BenchmarkReflectTypeAssertToPointer-16          321307184                3.735 ns/op           0 B/op          0 allocs/op
BenchmarkReflectTypeAssertToPointer-16          321660358                3.732 ns/op           0 B/op          0 allocs/op
BenchmarkReflectTypeAssertToPointer-16          321054388                3.740 ns/op           0 B/op          0 allocs/op
BenchmarkReflectTypeAssertToPointer-16          320267023                3.735 ns/op           0 B/op          0 allocs/op
BenchmarkReflectTypeAssertToPointer-16          320237029                3.736 ns/op           0 B/op          0 allocs/op
PASS
ok      github.com/miyamo2/reflect_type_assert_vs_type_assertion        14.402s
go tool benchstat -filter ".unit:(ns/op)" benchstat.txt
goos: linux
goarch: amd64
pkg: github.com/miyamo2/reflect_type_assert_vs_type_assertion
cpu: AMD EPYC 7763 64-Core Processor                
                              │ benchstat.txt │
                              │    sec/op     │
TypeAssertionToPointer-16         6.238n ± 0%
ReflectTypeAssertToPointer-16     3.736n ± 0%
geomean                           4.827n

どちらもゼロアロケーションですが、処理時間に関しては若干reflect.TypeAssertに優位性があり、おおよそ1.6倍速いです

reflect.Value#Interface + 型アサーション に優位性があるケース

type ITime interface {
    String() string
    GoString() string
    Format(layout string) string
    ...
}

func BenchmarkTypeAssertionToInterface(b *testing.B) {
	rv := reflect.ValueOf(new(time.Time)).Elem()
	b.ReportAllocs()
	for b.Loop() {
		_, ok := rv.Interface().(ITime)
		if !ok {
			b.Fatal("expected true but got false")
		}
	}
}

func BenchmarkReflectTypeAssertToInterface(b *testing.B) {
    rv := reflect.ValueOf(new(time.Time)).Elem()
    b.ReportAllocs()
    for b.Loop() {
        _, ok := reflect.TypeAssert[ITime](rv)
        if !ok {
            b.Fatal("expected true but got false")
        }
    }
}

最後はインターフェイスへ型アサーションします

go test -bench '^.*Interface$' -count 6
goos: linux
goarch: amd64
pkg: github.com/miyamo2/reflect_type_assert_vs_type_assertion
cpu: AMD EPYC 7763 64-Core Processor                
BenchmarkTypeAssertionToInterface-16            31883131                35.70 ns/op           24 B/op          1 allocs/op
BenchmarkTypeAssertionToInterface-16            33785035                35.60 ns/op           24 B/op          1 allocs/op
BenchmarkTypeAssertionToInterface-16            33294741                35.43 ns/op           24 B/op          1 allocs/op
BenchmarkTypeAssertionToInterface-16            34111364                35.60 ns/op           24 B/op          1 allocs/op
BenchmarkTypeAssertionToInterface-16            33124920                35.32 ns/op           24 B/op          1 allocs/op
BenchmarkTypeAssertionToInterface-16            33972009                35.59 ns/op           24 B/op          1 allocs/op
BenchmarkReflectTypeAssertToInterface-16        29676243                39.54 ns/op           24 B/op          1 allocs/op
BenchmarkReflectTypeAssertToInterface-16        30570763                39.59 ns/op           24 B/op          1 allocs/op
BenchmarkReflectTypeAssertToInterface-16        30284253                39.46 ns/op           24 B/op          1 allocs/op
BenchmarkReflectTypeAssertToInterface-16        30373454                39.87 ns/op           24 B/op          1 allocs/op
BenchmarkReflectTypeAssertToInterface-16        29724702                39.51 ns/op           24 B/op          1 allocs/op
BenchmarkReflectTypeAssertToInterface-16        29738011                39.72 ns/op           24 B/op          1 allocs/op
PASS
ok      github.com/miyamo2/reflect_type_assert_vs_type_assertion        14.267s
go tool benchstat -filter ".unit:(ns/op)" benchstat.txt
goos: linux
goarch: amd64
pkg: github.com/miyamo2/reflect_type_assert_vs_type_assertion
cpu: AMD EPYC 7763 64-Core Processor                
                                │ benchstat.txt │
                                │    sec/op     │
TypeAssertionToInterface-16         35.59n ± 1%
ReflectTypeAssertToInterface-16     39.57n ± 1%
geomean                             37.53n

どちらも1アロケーション発生しますが、このパターンではreflect.Value#Interface + 型アサーションの方がおおよそ1.1倍速いです

まとめ

今回は

  1. reflect.Value#Elemを介してTに型アサーションするケース
  2. reflect.Value#Elemを介さず*Tに型アサーションするケース
  3. T implements U -> Uに型アサーションするケース

の3パターンを検証し、1と2でreflect.TypeAssertに優位性があることが確認できました
これを機にreflectパッケージの実装を深堀したかったのですが挫折してしまったのでまたの別の機会にリベンジしたいです

脚注
  1. https://github.com/golang/go/issues/62121 ↩︎

GitHubで編集を提案

Discussion