【Go】ファイルへの書き込みをバッファリングに修正してみた


## 概要


DBからレコードを取得してテキストファイルに出力する処理の中で`fmt.Fprintf`を使用している箇所を`bufio.Writer` のバッファリングを利用するように修正してみた。


どのくらいパフォーマンスが向上したのかベンチマークテストも行ってみた。


## 環境

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

## 計測方法

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

## サンプルコード


### fmt.Fprintf使用


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

func FileOutPutTodosWithFprintf(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
}
```


### bufio.Writer使用


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

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

	writer := bufio.NewWriter(file)
	defer writer.Flush()

	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),
		}
		line := fmt.Sprintf("{%s},\n", strings.Join(fields, ", "))
		_, err = writer.Write([]byte(line))
		if err != nil {
			return err
		}
	}

	return nil
}
```


## 計測結果


```bash
=== RUN   BenchmarkFprintf
BenchmarkFprintf
BenchmarkFprintf-8  31  33787055 ns/op  11289062 B/op  249601 allocs/op

=== RUN   BenchmarkBufio
BenchmarkBufio
BenchmarkBufio-8    61  19502351 ns/op  13212685 B/op  269613 allocs/op
```


### fmt.Fprintf使用(BenchmarkFprintf)

- **実行回数**: 31回
- **平均実行時間**: 33,787,055 ns/op（約33.8ms/1回）
- **メモリ使用量**: 11,289,062 B/op（約11.2MB/1回）
- **メモリアロケーション回数**: 249,601回

### bufio.Writer使用(BenchmarkBufio)

- **実行回数**: 61回
- **平均実行時間**: 19,502,351 ns/op（19.5ms/1回）
- **メモリ使用量**: 13,212,685 B/op（約13.2MB/1回）
- **メモリアロケーション回数**: 269,613回

### 比較


|                  | fmt.Fprintf使用                | bufio.Writer使用              | 比較       |
| ---------------- | ---------------------------- | --------------------------- | -------- |
| **実行回数**         | 31回                          | 61回                         | 約1.9倍 増  |
| **平均実行時間**       | 33,787,055 ns/op（約33.8ms/1回） | 19,502,351 ns/op（19.5ms/1回） | 約42.2% 減 |
| **メモリ使用量**       | 11,289,062 B/op（約11.2MB/1回）  | 13,212,685 B/op（約13.2MB/1回） | 約17.0% 増 |
| **メモリアロケーション回数** | 249,601回                     | 269,613回                    | 約8.0% 増  |


### パフォーマンスの差の要因


I/O回数がfmt.Fprintf使用にくらべてbufio.Writer使用の方が少ないことが要因か

- fmt.Fprintf使用のI/O回数 → 10,000回(ループの回数分)
- bufio.Writer使用のI/O回数 → 212回(Flushの回数を計測した結果)

## 結論

- 今回の計測方法ではbufio.Writerのバッファリング処理の方が実行速度いる結果となった
	- メモリに関してはfmt.Fprintfを使用したほうが若干優れている結果となった
		- バッファリングの場合は内部バッファの追加メモリ確保により、メモリ使用量とアロケーション数が増加した？
- 処理速度を気にする場面ではバッファリングを使用していきたい