【Go】reflectパッケージを使用した処理のベンチマークテスト


## 概要


DBからレコードを取得してテキストファイルに出力する処理の中でreflectパッケージを使っていたが、パフォーマンス的によろしくないという情報が多かったで使わないようにした。


実際にどのくらいパフォーマンスが違うのか気になったのでベンチマークテストを行ってみた。


## 環境

- MacBook Air M1 メモリ16GB、
- go 1.23.0、PostgreSQL 14.5

## 計測方法

- Todosテーブルから10,000件のレコードを取得しテキストファイルに出力する

## サンプルコード


### reflectパッケージ使用


```go
func BenchmarkFileOutPutTodosWithRefrect(b *testing.B) {
	db, _ := dbConn()
	b.ReportAllocs()
	for i := 0; i < b.N; i++ {
		FileOutPutTodosWithRefrect(db, "file_output.txt")
		os.Remove("file_output.txt")
	}
}

func FileOutPutTodosWithRefrect(db *gorm.DB, fileName string) error {
	file, err := os.Create(fileName)
	if err != nil {
		return err
	}
	defer file.Close()

	var todos []Todo
	result := db.Find(&todos)
	if result.Error != nil {
		return result.Error
	}

	for _, todo := range todos {
		val := reflect.ValueOf(todo)
		typ := val.Type()

		var fields []string
		for i := 0; i < typ.NumField(); i++ {
			key := typ.Field(i).Name
			value := fmt.Sprintf("%v", val.Field(i).Interface())
			fields = append(fields, fmt.Sprintf("%v: %v", key, value))
		}
		_, err := fmt.Fprintf(file, "{%s},\n", strings.Join(fields, ", "))
		if err != nil {
			return err
		}
	}

	return nil
}
```


### reflectパッケージ不使用


```go
func BenchmarkFileOutPutTodos(b *testing.B) {
	db, _ := dbConn()
	b.ReportAllocs()
	for i := 0; i < b.N; i++ {
		FileOutPutTodos(db, "file_output.txt")
		os.Remove("file_output.txt")
	}
}

func FileOutPutTodos(db *gorm.DB, fileName string) error {
	file, err := os.Create(fileName)
	if err != nil {
		return err
	}
	defer file.Close()

	var todos []Todo
	result := db.Find(&todos)
	if result.Error != nil {
		return result.Error
	}

	for _, todo := range todos {
		fields := []string{
			fmt.Sprintf("ID: %v", todo.ID),
			fmt.Sprintf("Title: %v", todo.Title),
			fmt.Sprintf("Note: %v", todo.Note),
		}
		_, err := fmt.Fprintf(file, "{%s},\n", strings.Join(fields, ", "))
		if err != nil {
			return err
		}
	}

	return nil
}
```


## 計測結果


```bash
=== RUN   BenchmarkFileOutPutTodosWithRefrect
BenchmarkFileOutPutTodosWithRefrect
BenchmarkFileOutPutTodosWithRefrect-8 18  63923287 ns/op  24169018 B/op  669901 allocs/op

=== RUN   BenchmarkFileOutPutTodos
BenchmarkFileOutPutTodos
BenchmarkFileOutPutTodos-8            34  33159001 ns/op  11290029 B/op  249605 allocs/op
```


### reflectパッケージ使用(BenchmarkFileOutPutTodosWithRefrect)

- **実行回数**: 18回
- **平均実行時間**: 63,923,287 ns/op（約63.9ms/1回）
- **メモリ使用量**: 24,169,018 B/op（約24MB/1回）
- **メモリアロケーション回数**: 669,901回

### reflectパッケージ不使用(BenchmarkFileOutPutTodos)

- **実行回数**: 34回
- **平均実行時間**: 33,159,001 ns/op（33.5ms/1回）
- **メモリ使用量**: 11,290,029 B/op（約11MB/1回）
- **メモリアロケーション回数**: 249,605回

### 比較


|                  | reflect使用                    | reflect不使用                  | 比較       |
| ---------------- | ---------------------------- | --------------------------- | -------- |
| **実行回数**         | 18回                          | 34回                         | 約1.9倍 増  |
| **平均実行時間**       | 63,923,287 ns/op（約63.9ms/1回） | 33,159,001 ns/op（33.5ms/1回） | 約48.1% 減 |
| **メモリ使用量**       | 24,169,018 B/op（約24MB/1回）    | 11,290,029 B/op（約11MB/1回    | 約53.3% 減 |
| **メモリアロケーション回数** | 669,901回                     | 249,605回                    | 約62.7% 減 |


## 結論

- 今回の計測方法ではreflect使用しない方が実行速度、メモリ効率共に優れている結果となった
- reflectは使用する場所を選びたい
- `ストリーム処理` に修正した場合のパフォーマンスが気になるので、そちらもベンチマークテストを行ってみる