【Go】ファイルへの書き込み処理のパフォーマンス改善(


## 概要


DBからレコードを取得してテキストファイルに出力する処理のパフォーマンス改善。


`sync.Pool` , `writer.WriteString` を使用してメモリ割り当てを抑えてみた。


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


## 環境

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

## 計測方法

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

## サンプルコード


### Before


```go
func BenchmarkBufio(b *testing.B) {
	db, _ := dbConn()
	b.ReportAllocs()
	for i := 0; i < b.N; i++ {
		FileOutPutTodosWithBufio(db, "file_batched.txt")
		os.Remove("file_batched.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
}
```


### After


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

var bufWriterPool = &sync.Pool{
	New: func() any {
		return bufio.NewWriter(nil)
	},
}

func getBufWriter(w io.Writer) *bufio.Writer {
	bufw := bufWriterPool.Get().(*bufio.Writer)
	bufw.Reset(w)
	return bufw
}

func putBufWriter(bufw *bufio.Writer) {
	bufw.Reset(nil)
	bufWriterPool.Put(bufw)
}

// sync.Poolを使用
func FileOutPutTodosWithPool(db *gorm.DB, fileName string) error {
	file, err := os.Create(fileName)
	if err != nil {
		return err
	}
	defer file.Close()

	writer := getBufWriter(file)
	defer putBufWriter(writer)
	defer func() {
		flushErr := writer.Flush()
		if err == nil {
			err = flushErr
		}
	}()

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

	for _, todo := range todos {
		fields := [][2]string{
			{"ID", strconv.FormatInt(int64(todo.ID), 10)},
			{"Title", todo.Title},
			{"Note", todo.Note},
		}
		_ = writer.WriteByte('{')
		for i, f := range fields {
			if i > 0 {
				_, _ = writer.WriteString(", ")
			}
			_, _ = writer.WriteString(f[0])
			_, _ = writer.WriteString(": ")
			_, _ = writer.WriteString(f[1])
		}
		_, err = writer.WriteString("},\n")
		if err != nil {
			return err
		}
	}

	return nil
}
```


## 計測結果


```text
=== RUN   BenchmarkBufio
BenchmarkBufio-8	60	19,552,349 ns/op	13,212,360 B/op	269,609 allocs/op

=== RUN   BenchmarkPool
BenchmarkPool-8	  75	16,104,963 ns/op	8,947,325 B/op	179,726 allocs/op
```


### Before(BenchmarkBufio)

- **実行回数**: 60回
- **平均実行時間**: 19,552,349 ns/op（約19.6ms/1回）
- **メモリ使用量**: 13,212,360 B/op（約13.2MB/1回）
- **メモリアロケーション回数**: 269,609回

### After(BenchmarkPool)

- **実行回数**: 75回
- **平均実行時間**: 16,104,963 ns/op（16.1ms/1回）
- **メモリ使用量**: 8,947,325 B/op（約8.9MB/1回）
- **メモリアロケーション回数**: 179,726回

### 比較


|                  | Before                       | After                       | 比較       |
| ---------------- | ---------------------------- | --------------------------- | -------- |
| **実行回数**         | 60回                          | 75回                         | 約1.25倍 増 |
| **平均実行時間**       | 19,552,349 ns/op（約19.6ms/1回） | 16,104,963 ns/op（16.1ms/1回） | 約17.8% 減 |
| **メモリ使用量**       | 13,212,360 B/op（約13.2MB/1回）  | 8,947,325 B/op（約8.9MB/1回）   | 約32.6% 減 |
| **メモリアロケーション回数** | 269,609回                     | 179,726回                    | 約33.3% 減 |


## 結論


`sync.Pool` , `writer.WriteString` を使用するとメモリ効率が改善された。


パフォーマンスを気にする場面では`sync.Pool` , `writer.WriteString` を使用していきたい。