PythonとGolangのCSVの処理速度の違いを調べる

大量のデータをバッチ処理するようなプログラムを用意するときに、PythonとGolangのどちらを使うべきかいつも悩む。処理速度は悩むポイントの一つだ。近々大量のCSVを扱うプログラムを書く予定があったので、PythonとGolangでどの程度CSVの処理速度に違いがあるのか実験してみた。

実験は手元のiMac (Intel Core i5 3.4GHz / 40GB 2400MHz DDR4 RAM / Fusion Drive) で行った。

実験1: CSVファイルの書き出し(実験用のCSVデータの準備)

5列, 10列, 20列, 40列 × 10,000行, 100,000行, 1,000,000行 のCSVファイルを用意する。1列目には、後のフィルタリングのテスト用にtype0, type1, type2, type0…といった3行ずつループする文字列の値を、2列目には集計のテスト用に行番号を10で割った余りの数値を、それ以外の列には「{行}_{列}」という文字列が入るようなファイルを用意する。一番大きい40列 × 1,000,000行のCSVファイルで360MBほど、

生成されたテストファイル群

Python(csv.writerを使用)

for cols in [5, 10, 20, 40]:
  col_headers = list(map(lambda x: f"col{x}", range(1, cols + 1)))
  for rows in [10000, 100000, 1000000]:
    with open(os.path.join(out_dir, f"cols_{cols}_rows_{rows}.csv"), 'w') as f:
      writer = csv.writer(f)

      writer.writerow(col_headers)

      for j in range(0, rows):
        row = [f"{j}_{x}" for x in range(1, cols + 1)]

        row[0] = f"type{j%10}"
        row[1] = j%10
        writer.writerow(row)

処理時間(単位ms / 10回実行の平均値)

5列10列20列40列
10,000行33.557.3119.0197.1
100,000行365.7638.91123.82166.8
1,000,000行3733.46557.211768.022734.5

Golang

start := time.Now()
f, err := os.Open(filePath)
if err != nil {
	log.Fatal(err)
}
defer f.Close()

w := csv.NewWriter(f)

header := []string{}
for j := 0; j < cols; j++ {
	header = append(header, fmt.Sprintf("col%d", j+1))
}
w.Write(header)

for j := 0; j < rows; j++ {
	row := []string{}
	for k := 0; k < cols; k++ {
		row = append(row, fmt.Sprintf("%d_%d", j, k))
	}
	row[0] = fmt.Sprintf("type%d", j%10)
	row[1] = fmt.Sprintf("%d", j%10)
	w.Write(row)
}
w.Flush()

timeElapsed := time.Since(start)

処理時間(単位ms / 10回実行の平均値)

5列10列20列40列
10,000行13.422.540.375.6
100,000行134.7224.5409.9734.6
1,000,000行1352.32258.34040.27618.4

比較

Pythonに比べて、Golangの処理時間は1/3程度。PythonではPandasを使ってもCSV出力は可能だが、メモリー上で書き出し用のDataFrameを生成する処理の時間が大きくなるため、省略。

実験2: CSVファイル読み込み

次に、書き出したファイルを使ってCSVファイルの読み込みにかかる時間を調べる。

Python(csv.readerを使用)

start = time.time()
rows_count = 0
with open(path) as f:
  reader = csv.reader(f)
  next(f) # skip header
  for row in reader:
    rows_count += 1

time_elapsed = time.time() - start

処理時間(単位ms / 10回実行の平均値)

5列10列20列40列
10,000行7.815.430.863.1
100,000行88.5166.2344.7671.8
1,000,000行899.41767.33569.37263.9

参考: Python(Pandas DataFrameを使用)

メモリに全て展開/保持される形になるため、ただ一行ずつ読み込んで処理する場合では非効率だが参考値として計測しておく。

start = time.time()
rows_count = len(pd.read_csv(path, dtype=str).index)
time_elapsed = time.time() - start

処理時間(単位ms / 10回実行の平均値)

5列10列20列40列
10,000行20.533.275.1153.8
100,000行120.6326.7800.41754.2
1,000,000行1279.53352.08236.618224.0

Golang

start := time.Now()
f, err := os.Open(filePath)
if err != nil {
	log.Fatal(err)
}
defer f.Close()

r := csv.NewReader(f)
rowsCount := 0

// skip header
if _, err := r.Read(); err != nil {
	log.Fatal(err)
}

for {
	_, err := r.Read()
	if err == io.EOF {
		break
	}
	if err != nil {
		log.Fatal(err)
	}
	rowsCount++
}
timeElapsed := time.Since(start)

処理時間(10回実行の平均値)

5列10列20列40列
10,000行2.55.96.812.5
100,000行24.747.871.8130.1
1,000,000行249.4399.9720.21333.2

比較

読み取りの処理だけをみてみると、Golangが圧倒的に速い。Pythonのcsv.readerの1/5程度の時間で処理が終わった。

実験3: 集計

特定の列の数値の合計の計算にかかる時間を調べる。

Python(csv.readerを使用)

start = time.time()
sum = 0
with open(path) as f:
  reader = csv.reader(f)
  next(f) # skip header
  for row in reader:
    sum += int(row[1])

time_elapsed = time.time() - start

処理時間(単位ms / 10回実行の平均値)

5列10列20列40列
10,000行10.017.032.765.5
100,000行106.6185.5356.7686.1
1,000,000行1093.81976.53784.37475.1

Python(Pandas DataFrameを使用)

start = time.time()
sum = pd.read_csv(path)["col2"].sum()
time_elapsed = time.time() - start

処理時間(10回実行の平均値)

5列10列20列40列
10,000行25.839.490.2186.0
100,000行130.1360.0973.51970.2
1,000,000行1385.43853.09460.019974.4

Golang

start := time.Now()
f, err := os.Open(filePath)
if err != nil {
	log.Fatal(err)
}
defer f.Close()

r := csv.NewReader(f)
sum := 0

// skip header
if _, err := r.Read(); err != nil {
	log.Fatal(err)
}

for {
	row, err := r.Read()
	if err == io.EOF {
		break
	}
	if err != nil {
		log.Fatal(err)
	}
	v, err := strconv.Atoi(row[1])
	if err != nil {
		log.Fatal(err)
	}
	sum += v
}
timeElapsed := time.Since(start)

処理時間(単位ms / 10回実行の平均値)

5列10列20列40列
10,000行2.73.97.012.7
100,000行25.440.581.6131.7
1,000,000行256.6397.9740.71327.8

比較

処理時間から、実験1で計測したCSVの読み込みにかかる時間を引いた時間の比較。Golangの1,000,000行の処理については、結果が僅かにマイナスになってしまうほど処理にかかる時間が少なかったため、もはや比率の計算ができない。

実験4: フィルタリング

特定の値を持つ行だけを抽出する処理にかかる時間を調べる。

Python(csv.readerを使用)

start = time.time()
type0count = 0
with open(path) as f:
  reader = csv.reader(f)
  next(f) # skip header
  for row in reader:
    if row[0] == "type0":
      type0count += 1
time_elapsed = time.time() - start

処理時間(単位ms / 10回実行の平均値)

5列10列20列40列
10,000行7.915.030.961.8
100,000行85.6166.4333.0676.7
1,000,000行903.11746.73548.17145.8

Python(Pandas DataFrameを使用)

start = time.time()
type0 = len(pd.read_csv(path, dtype=str).query('col1 == "type0"').index)
time_elapsed = time.time() - start

処理時間(単位ms / 10回実行の平均値)

5列10列20列40列
10,000行22.938.582.9169.2
100,000行133.1350.3815.61740.4
1,000,000行1351.13430.88313.018157.4

Golang

start := time.Now()
f, err := os.Open(filePath)
if err != nil {
	log.Fatal(err)
}
defer f.Close()

r := csv.NewReader(f)

type0Count := 0
for {
	row, err := r.Read()
	if err == io.EOF {
		break
	}
	if err != nil {
		log.Fatal(err)
	}
	if row[0] == "type0" {
		type0Count += 1
	}
}
timeElapsed := time.Since(start)

処理時間(単位ms / 10回実行の平均値)

5列10列20列40列
10,000行2.94.26.813.0
100,000行25.440.573.0131.7
1,000,000行255.5401.8727.91369.3

比較

こちらも処理時間から、実験1で計測したCSVの読み込みにかかる時間を引いた時間の比較。フィルタリングの処理は、どの実装でも場合によっては単純なCSV読み込みよりも処理時間が短くなって実験結果がマイナスになってしまうなど「誤差」としか言えないような処理時間でパフォーマンスに大きな差が現れなかった。

まとめ/考察

CSVファイルの読み書きでは、PythonとGolangで3倍〜5倍程度のパフォーマンスの差があった。シンプルな集計やフィルタリングについては、処理の内容にも寄るだろうが極端に大きなパフォーマンスの差はないようだった。全体的にGolangが最もパフォーマンスとしては優れているが、やはりPythonに比べると書かなければいけないコードの量は増える。

Pythonではcsv.reader / csv.writerを使う場合とPandasのDataFrameを使う場合でも大きなパフォーマンスの差が現れた。DataFrameを使うと、複雑で高度な処理を簡素に書くことができるが、それが必要ない場合はcsv.reader / csv.writerを使うことで、少なくとも読み込みの部分だけでも1/2〜1/3程度に処理時間を短縮できそうだ。

高度で複雑なデータ集計をしたい場合や、パフォーマンスが問題にならず簡素にデータ処理を書きたい場合はPythonでPandasのDataFrameを、パフォーマンスが最優先の場合はGolangを、コードの生産性/保守性とパフォーマンスのバランスを取りたい場合はPythonでcsv.reader / csv.writerを使うのが良さそう。

Pocket

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です