a tomato

トマトが大好きです

golang1.9 ReuseRecordでCSVのREADパフォーマンスが向上するかも!?

golangを1.9にアップデートしてみました。

Go 1.9 Release Notes - The Go Programming Language

f:id:kzdev:20170908010918p:plainf:id:kzdev:20170908010918p:plain

release noteを見ると機能の追加からパフォーマンスの向上まで色々ありますが、
今回は、CSVのREAD性能が向上したというので、ベンチマークを取ってどのくらい改善されたのか確認します。

検証概要

encoding/csv

The new field Reader.ReuseRecord controls whether calls to Read may return a slice sharing the backing array of the previous call’s returned slice for improved performance.

掻い摘んでいうと、ReuseRecordというプロパティをTRUEにするとSliceを共有することで、メモリアロケーションを減らしてパーフォーマンスもアップするよという内容。

早速、ReuseRecordがFALSEの場合とTRUEの場合を比較してみる。

サンプルデータの準備

まずは、性能検証に読込するCSVを用意します。
とりあえず、ランダムな文字列で150,000行のサンプルデータを用意します。

下のようなサンプルCSV生成コードをmain.goという名前で保存します。

package main                                                                                                                                                                                                                                                                                                                  
                                                                                                                                                                                                                                                                                                                              
import (                                                                                                                                                                                                                                                                                                                      
  "encoding/csv"                                                                                                                                                                                                                                                                                                              
  "flag"                                                                                                                                                                                                                                                                                                                      
  "math/rand"                                                                                                                                                                                                                                                                                                                 
  "os"                                                                                                                                                                                                                                                                                                                        
  "strconv"                                                                                                                                                                                                                                                                                                                   
                                                                                                                                                                                                                                                                                                                              
  "golang.org/x/text/encoding/japanese"                                                                                                                                                                                                                                                                                       
  "golang.org/x/text/transform"                                                                                                                                                                                                                                                                                               
)                                                                                                                                                                                                                                                                                                                             
                                                                                                                                                                                                                                                                                                                              
var rs1Letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")                                                                                                                                                                                                                                               
                                                                                                                                                                                                                                                                                                                              
func RandString1(n int) string {                                                                                                                                                                                                                                                                                              
  b := make([]rune, n)                                                                                                                                                                                                                                                                                                        
  for i := range b {                                                                                                                                                                                                                                                                                                          
    b[i] = rs1Letters[rand.Intn(len(rs1Letters))]                                                                                                                                                                                                                                                                             
  }                                                                                                                                                                                                                                                                                                                           
  return string(b)                                                                                                                                                                                                                                                                                                            
}                                                                                                                                                                                                                                                                                                                             
                                                                                                                                                                                                                                                                                                                              
func main() {                                                                                                                                                                                                                                                                                                                 
  flag.Parse()                                                                                                                                                                                                                                                                                                                
  file, err := os.Create(flag.Arg(0))                                                                                                                                                                                                                                                                                         
  if err != nil {                                                                                                                                                                                                                                                                                                             
    return                                                                                                                                                                                                                                                                                                                    
  }                                                                                                                                                                                                                                                                                                                           
  defer file.Close()                                                                                                                                                                                                                                                                                                          
                                                                                                                                                                                                                                                                                                                              
  writer := csv.NewWriter(transform.NewWriter(file, japanese.ShiftJIS.NewEncoder()))                                                                                                                                                                                                                                          
  writer.UseCRLF = true                                                                                                                                                                                                                                                                                                       
                                                                                                                                                                                                                                                                                                                              
  i := 1                                                                                                                                                                                                                                                                                                                      
  for {                                                                                                                                                                                                                                                                                                                       
    if i > 150000 {                                                                                                                                                                                                                                                                                                             
      break                                                                                                                                                                                                                                                                                                                   
    }                                                                                                                                                                                                                                                                                                                         
    var new_record []string                                                                                                                                                                                                                                                                                                   
    new_record = append(new_record, strconv.Itoa(i))                                                                                                                                                                                                                                                                          
    new_record = append(new_record, RandString1(12))                                                                                                                                                                                                                                                                          
    new_record = append(new_record, RandString1(20))                                                                                                                                                                                                                                                                          
    new_record = append(new_record, RandString1(6))                                                                                                                                                                                                                                                                           
    new_record = append(new_record, RandString1(9))                                                                                                                                                                                                                                                                           
    new_record = append(new_record, RandString1(10))                                                                                                                                                                                                                                                                          
    new_record = append(new_record, RandString1(20))                                                                                                                                                                                                                                                                          
    new_record = append(new_record, RandString1(5))                                                                                                                                                                                                                                                                           
    new_record = append(new_record, RandString1(6))                                                                                                                                                                                                                                                                           
    new_record = append(new_record, RandString1(13))                                                                                                                                                                                                                                                                          
    new_record = append(new_record, RandString1(15))                                                                                                                                                                                                                                                                          
    writer.Write(new_record)                                                                                                                                                                                                                                                                        
    i++                                                                                                                                                                                                                                                                                                                       
  }                                                                                                                                                                                                                                                                                                                           
  writer.Flush()                                                                                                                                                                                                                                                                                                              
                                                                                                                                                                                                                                                                                                                              
}  

下記で実行します。

go run main.go sample.csv

実行と同ディレクトリに、sample.csvというファイルが出来上がります。
これを使って性能検証を行ってみます。

ベンチマーク

benchmark用に以下のファイルをCsvReuseRecord_test.goとして用意します。

package main                                                                                                                                                                                                                                                                                                                  
                                                                                                                                                                                                                                                                                                                              
import (                                                                                                                                                                                                                                                                                                                      
  "encoding/csv"                                                                                                                                                                                                                                                                                                              
  "io"                                                                                                                                                                                                                                                                                                                        
  "os"                                                                                                                                                                                                                                                                                                                        
  "testing"                                                                                                                                                                                                                                                                                                                   
)                                                                                                                                                                                                                                                                                                                             
                                                                                                                                                                                                                                                                                                                              
func BenchmarkCsvDisableReuseRecord(b *testing.B) {                                                                                                                                                                                                                                                                           
  file, err := os.Open("./sample.csv")                                                                                                                                                                                                                                                                                        
  if err != nil {                                                                                                                                                                                                                                                                                                             
    return                                                                                                                                                                                                                                                                                                                    
  }                                                                                                                                                                                                                                                                                                                           
  defer file.Close()                                                                                                                                                                                                                                                                                                          
                                                                                                                                                                                                                                                                                                                              
  b.ResetTimer()                                                                                                                                                                                                                                                                                                              
  reader := csv.NewReader(file)                                                                                                                                                                                                                                                                                               
  reader.LazyQuotes = true                                                                                                                                                                                                                                                                                                    
  reader.ReuseRecord = false                                                                                                                                                                                                                                                                                                  
                                                                                                                                                                                                                                                                                                                              
  for {                                                                                                                                                                                                                                                                                                                       
    record, err := reader.Read()                                                                                                                                                                                                                                                                                              
    if err == io.EOF {                                                                                                                                                                                                                                                                                                        
      break                                                                                                                                                                                                                                                                                                                   
    }                                                                                                                                                                                                                                                                                                                         
    s := make([]byte, 0)                                                                                                                                                                                                                                                                                                      
    for _, v := range record {                                                                                                                                                                                                                                                                                                
      s = append(s, v...)                                                                                                                                                                                                                                                                                                     
    }                                                                                                                                                                                                                                                                                                                         
  }                                                                                                                                                                                                                                                                                                                           
}                                                                                                                                                                                                                                                                                                                             
                                                                                                                                                                                                                                                                                                                              
func BenchmarkCsvEnableReuseRecord(b *testing.B) {                                                                                                                                                                                                                                                                            
  file, err := os.Open("./sample.csv")                                                                                                                                                                                                                                                                                        
  if err != nil {                                                                                                                                                                                                                                                                                                             
    return                                                                                                                                                                                                                                                                                                                    
  }                                                                                                                                                                                                                                                                                                                           
  defer file.Close()                                                                                                                                                                                                                                                                                                          
                                                                                                                                                                                                                                                                                                                              
  b.ResetTimer()                                                                                                                                                                                                                                                                                                              
  reader := csv.NewReader(file)                                                                                                                                                                                                                                                                                               
  reader.LazyQuotes = true                                                                                                                                                                                                                                                                                                    
  reader.ReuseRecord = true                                                                                                                                                                                                                                                                                                   
                                                                                                                                                                                                                                                                                                                              
  for {                                                                                                                                                                                                                                                                                                                       
    record, err := reader.Read()                                                                                                                                                                                                                                                                                              
    if err == io.EOF {                                                                                                                                                                                                                                                                                                        
      break                                                                                                                                                                                                                                                                                                                   
    }                                                                                                                                                                                                                                                                                                                         
    s := make([]byte, 0)                                                                                                                                                                                                                                                                                                      
    for _, v := range record {                                                                                                                                                                                                                                                                                                
      s = append(s, v...)                                                                                                                                                                                                                                                                                                     
    }                                                                                                                                                                                                                                                                                                                         
  }                                                                                                                                                                                                                                                                                                                           
}  

早速、ベンチマーク取ってみる。

$ go test -bench . -benchmem
PASS
BenchmarkCsvDisableReuseRecord-4        2000000000               0.26 ns/op            0 B/op          0 allocs/op
BenchmarkCsvEnableReuseRecord-4         2000000000               0.23 ns/op            0 B/op          0 allocs/op
ok       17.239s

うーん、目に見えて改善というのは自分の端末ではわからなかったが、何度か実行してみると、かなり結果がぶれていた。

実行時のIOにも左右されているのだと思うが、利用する際には実際に実行する環境でベンチを取って判断すると良いと思います。

ちなみに、ReuseRecordはデフォルトではFALSEになっています。