👋

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

2024/12/27に公開

概要

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

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

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

環境

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

計測方法

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

サンプルコード

Before

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

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
}

計測結果

=== 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 を使用していきたい。

GitHubで編集を提案

Discussion