Goのreflectパッケージにおけるパフォーマンス低下について

2023-03-19

はじめに

こんにちは。株式会社 Belong で Backend Engineer をしている Mohiro です。
本記事では、Go の標準パッケージである reflect パッケージを使用した場合の「パフォーマンス低下」が発生する原因について解説します。
reflect パッケージを使用することで、静的型付け言語である Go において、実行時に型情報を動的に取得できます。 これにより、柔軟なコードを書くことができるのですが、使用時には考慮すべき点がいくつか挙げられます。 今回はその中でも代表的な注意点である「パフォーマンス」に着目しました。

対象読者

  • Go をある程度触ったことのある人
  • reflect パッケージ使用時の基本的な注意点を知りたい人

目次

  • reflect パッケージにおける、パフォーマンス低下について
    1. 主な要因
      1. 動的型情報の取得
      2. メモリアロケーションのオーバーヘッド
      3. ガベージコレクションの影響
    2. 検証

reflect パッケージにおける、パフォーマンス低下について

主な要因

1. 動的型情報の取得

動的に型情報を取得するために多くのコストを必要とします。 これはコンパイル時、静的に型情報を持っていないため、実行時に型情報を調べる必要があるためです。 reflect パッケージにおいて、動的型情報を取得する関数は、主に以下の2つがあります。

  1. reflect.TypeOf()
  2. reflect.ValueOf()

2. メモリアロケーションによるメモリ使用量の増加

reflect オブジェクトは、値に加え、値の型情報などを保持しており、メモリサイズが大きくなりがちです。 そのため、大量の動的メモリアロケーションが必要とされる場合があります。 これがメモリ使用量の増加に繋がります。

3. ガベージコレクション実行回数の増加

Go のガベージコレクタの実行タイミングは

  1. 前回の実行から一定時間後
  2. 割り当てられたメモリが特定の割合を超える(詳細) とされています。 reflect パッケージを使用すると、reflect オブジェクトが大量に生成され、[2] に抵触する場合があります。

検証

以下は、上記で挙げた3項目によりどの程度のパフォーマンス低下が発生するかを検証したコードです。

package main

import (
	"fmt"
	"reflect"
	"runtime"
	"time"
)

type Person struct {
	Name string
	Age  int
}

func (p Person) GetName() string {
	return p.Name
}

func (p Person) GetAge() int {
	return p.Age
}

func main() {
	p := Person{Name: "John Doe", Age: 30}

	// 静的な型情報を使用
	startStatic := time.Now()
	memStatsStatic := new(runtime.MemStats)
	runtime.ReadMemStats(memStatsStatic)
	for i := 0; i < 100000; i++ {
		name := p.GetName()
		age := p.GetAge()
		if i == 99999 {
			fmt.Println("Static: ", name, age)
		}
	}
	elapsedStatic := time.Since(startStatic)
	memStatsStaticAfter := new(runtime.MemStats)
	runtime.ReadMemStats(memStatsStaticAfter)

	// 動的な型情報を使用
	startDynamic := time.Now()
	memStatsDynamic := new(runtime.MemStats)
	runtime.ReadMemStats(memStatsDynamic)
	arg := make([]reflect.Value, 0)
	for i := 0; i < 100000; i++ {
		value := reflect.ValueOf(p)                                 // ※1
		nameMethod := value.MethodByName("GetName")
		ageMethod := value.MethodByName("GetAge")
		name := nameMethod.Call(arg)[0].String()      // ※2
		age := int(ageMethod.Call(arg)[0].Int())

		if i == 99999 {
			fmt.Println("Dynamic: ", name, age, "\n")
		}
	}
	elapsedDynamic := time.Since(startDynamic)
	memStatsDynamicAfter := new(runtime.MemStats)
	runtime.ReadMemStats(memStatsDynamicAfter)

	fmt.Println("実行時間")
	fmt.Printf("Static: %v\n", elapsedStatic)
	fmt.Printf("Dynamic: %v\n\n", elapsedDynamic)
	fmt.Println("メモリアロケーション[byte]")
	fmt.Printf("Static: %d\n", memStatsStaticAfter.TotalAlloc-memStatsStatic.TotalAlloc)
	fmt.Printf("Dynamic: %d\n\n", memStatsDynamicAfter.TotalAlloc-memStatsDynamic.TotalAlloc)
	fmt.Println("ガベージコレクションの実行回数")
	fmt.Printf("Static: %d\n", memStatsStaticAfter.NumGC-memStatsStatic.NumGC)
	fmt.Printf("Dynamic: %d\n", memStatsDynamicAfter.NumGC-memStatsDynamic.NumGC)
}

このコードは、Person 構造体に GetName() と GetAge() メソッドが定義されています。
最初のループでは、静的な型情報を使用して、Person のインスタンスから直接 GetName() と GetAge() を呼び出しています。2 番目のループでは、reflect パッケージを使用して、動的にメソッドを呼び出します。
最後に、それぞれのループにおける実行時間とメモリアロケーション、ガベージコレクションの実行回数を表示します。

実行結果は以下になります。

Static:  John Doe 30
Dynamic: John Doe 30

実行時間
Static: 111.541µs
Dynamic: 83.995083ms

メモリアロケーション[byte]
Static: 1320
Dynamic: 38412520

ガベージコレクションの実行回数
Static: 0
Dynamic: 9

reflect パッケージの使用により、パフォーマンスに大きな差異が生まれたことが分かります。

次に、パフォーマンス低下の主な要因として挙げた3項目の該当箇所を見ていきます。

1 番目の要因である「動的型情報の取得」は以下の部分が該当します。(検証コードの※1)

		value := reflect.ValueOf(p)

ValueOf 関数を使って、p 変数の動的な型情報を取得し、MethodByName 関数を使って、p 変数のメソッドを動的に取得しています。

2 番目の要因である「メモリアロケーションによるメモリ使用量の増加」は以下の部分が該当します。(検証コードの※2)

		name := nameMethod.Call([]reflect.Value{})[0].String()
		age := int(ageMethod.Call([]reflect.Value{})[0].Int())

上記の Call メソッドでは、引数および戻り値に []reflect.Value が使用されます。 今回のケースでは引数は空のスライスなのでメモリアロケーションは発生しませんが、戻り値では値と型情報を格納する段階でメモリアロケーションが発生します。

3 番目の要因である「ガベージコレクション実行回数の増加」は以下の部分が該当します。(検証コードの※1)

		value := reflect.ValueOf(p)

reflect.ValueOf(p) を呼び出すことで、新しく生成された reflect オブジェクトが、ガベージコレクションの対象となります。

最後に

本記事では、冒頭で述べた「reflect パッケージのパフォーマンス低下の要因」という 3 つの項目について、それぞれ検証用のコードを用いて解説しました。 個人的な感想としては、今回使用した検証コードで約 0.1 秒程度のパフォーマンス低下が発生した結果は、今後の開発に役立つ指標になるのではないかと感じました。 今後 reflect パッケージを使用する場合には、本記事を思い出して頂ければ嬉しく思います。

弊社 Belong では一緒にサービスを育てる仲間を募集しています。 もし弊社に興味を持っていただけたら下記リンクをご覧いただけたら幸いです。

HP: https://belong.co.jp/

エンジニアリングチームの紹介: https://entrancebook.belonginc.dev/

参考文献

プログラミング言語 Go 12.9
みんなの Go