MAGAZINE

ルーターマガジン

Ruby

RubyのCSVモジュールをネイティブモジュールに差し替えて6倍高速化する

2023.10.06
Pocket

サマリー

Rubyで高速にCSVを読み取りたければネイティブモジュールのrscsvを使うと良い。

  • 国税庁法人番号公表サイトの全国法人CSV534万件(1.1G)で都道府県別集計
  • Pythonの標準CSVでは9秒
  • それに対してRubyの標準CSVモジュールでは104秒
  • RubyのCSVモジュールをRust製に変えると17秒と6倍になる

PythonのCSVパースがRubyに比べて速い気がしていた

Pythonでデータ分析する際には多くの場合PandasでCSVから読み取ります。 計算する際にはPandasの恩恵に預かれるものの、単にCSVを読むだけでも体感で速い印象です。

大きなCSVのテストとして、国税庁が出している法人番号データベースを利用しましょう。 全国で500万件強の法人が登録されています。20230731版では1.1Gのファイルサイズとなっています。

10カラム目に都道府県が記載されているので、都道府県別に何件の法人が登記されているかを集計するだけのプログラムでテストしてみます。

以下の環境でテストしています

  • Windows + WSL + Ubuntu20
  • Python 3.8.10
  • ruby 3.0.6p216

Pythonソース

from collections import defaultdict
import sys
import csv

pref_cnt_dict = defaultdict(int)
reader = csv.reader(sys.stdin, strict=True)
for row in reader:
    pref_cnt_dict[row[9]] += 1
print(pref_cnt_dict)

Python実行結果

/usr/bin/time -f "Memory %M KB\nReal time %E\nUser time %U\nSystem time %S" \
python3 csv_default.py < 00_zenkoku_all_20230731.csv
defaultdict(<class 'int'>, {'北海道': 220166, '青森県': 36861, '岩手県': 32731, '宮城県': 78128, '秋田県': 29622, '山形 県': 32721, '福島県': 70081, '茨城県': 90139, '栃木県': 71953, '群馬県': 73265, '埼玉県': 247028, '千葉県': 216654, '東 京都': 1225687, '神奈川県': 345276, '新潟県': 73191, '富山県': 34754, '石川県': 42057, '福井県': 29758, '山梨県': 33587, '長野県': 76797, '岐阜県': 69749, '静岡県': 121038, '愛知県': 264853, '三重県': 53407, '滋賀県': 40776, '京都府': 105784, '大阪府': 438099, '兵庫県': 189958, '奈良県': 38252, '和歌山県': 29623, '鳥取県': 19199, '島根県': 22163, '岡山県': 70233, '広島県': 107108, '山口県': 41017, '徳島県': 29703, '香川県': 37998, '愛媛県': 50582, '高知県': 24964, '福岡県': 199072, '佐賀県': 23483, '長崎県': 40497, '熊本県': 66986, '大分県': 45627, '宮崎県': 37295, '鹿児島県': 56030, '沖縄県': 57888, '': 4049})
Memory 9676 KB
Real time 0:08.96
User time 8.10
System time 0.16

約9秒の処理で終わっています。標準入力からのストリーム処理なのでメモリもほとんど使ってません。

次にrubyで同じ集計をします。

Rubyソース

require 'csv'

pref_cnt = {}
CSV($stdin).each do |row|
  pref_cnt[row[9]] = pref_cnt[row[9]].to_i + 1
end

p pref_cnt

Ruby実行結果

/usr/bin/time -f "Memory %M KB\nReal time %E\nUser time %U\nSystem time %S" \
ruby csv_default.rb < 00_zenkoku_all_20230731.csv
{"北海道"=>220166, "青森県"=>36861, "岩手県"=>32731, "宮城県"=>78128, "秋田県"=>29622, "山形県"=>32721, "福島県"=>70081, "茨城県"=>90139, "栃木県"=>71953, "群馬県"=>73265, "埼玉県"=>247028, "千葉県"=>216654, "東京都"=>1225687, "神奈川県"=>345276, "新潟県"=>73191, "富山県"=>34754, "石川県"=>42057, "福井県"=>29758, "山梨県"=>33587, "長野県"=>76797, "岐阜県"=>69749, "静岡県"=>121038, "愛知県"=>264853, "三重県"=>53407, "滋賀県"=>40776, "京都府"=>105784, "大阪府"=>438099, "兵庫県"=>189958, "奈良県"=>38252, "和歌山県"=>29623, "鳥取県"=>19199, "島根県"=>22163, "岡山県"=>70233, "広島県"=>107108, "山口県"=>41017, "徳島県"=>29703, "香川県"=>37998, "愛媛県"=>50582, "高知県"=>24964, "福岡県"=>199072, "佐賀県"=>23483, "長崎県"=>40497, "熊本県"=>66986, "大分県"=>45627, "宮崎県"=>37295, "鹿児島県"=>56030, "沖縄県"=>57888, nil=>4049}
Memory 24024 KB
Real time 1:53.70
User time 104.51
System time 0.32

めちゃくちゃ遅いです。Pythonが8秒なのに対してRubyのCSVパースは1分54秒です。 Pythonのcsvパーサーはcで書かれていますが、RubyのcsvパーサーはRubyで記述されています。 じゃぁってことでRubyにもネイティブライブラリが無いのかと探すとありました。

rscsvというネイティブモジュールを試してみる

rscsvというモジュールがあります。 https://github.com/lautis/rscsv

This is ~3x faster than using native Ruby CSV.generate or CSV.parse.

と記述されています。3倍速くなるとのことです。 native extentionであるためインストールにはRustのビルド環境が必要となりますが、Cのビルド環境を頑張ることに比べるとずっと環境構築はまだシンプルです。

直近はアップデートされてないという点がやや心配ですが、実行してみましょう。

Ruby(rscsv)のソース

csv_rscsv.rb

require 'rscsv'

pref_cnt = {}
Rscsv::Reader.each($stdin.each) do |row|
  pref_cnt[row[9]] = pref_cnt[row[9]].to_i + 1
end

p pref_cnt

Ruby(rscsv)の実行結果

{"北海道"=>220166, "青森県"=>36861, "岩手県"=>32731, "宮城県"=>78128, "秋田県"=>29622, "山形県"=>32721, "福島県"=>70081, "茨城県"=>90139, "栃木県"=>71953, "群馬県"=>73265, "埼玉県"=>247028, "千葉県"=>216654, "東京都"=>1225687, "神奈川県"=>345276, "新潟県"=>73191, "富山県"=>34754, "石川県"=>42057, "福井県"=>29758, "山梨県"=>33587, "長野県"=>76797, "岐阜県"=>69749, "静岡県"=>121038, "愛知県"=>264853, "三重県"=>53407, "滋賀県"=>40776, "京都府"=>105784, "大阪府"=>438099, "兵庫県"=>189958, "奈良県"=>38252, "和歌山県"=>29623, "鳥取県"=>19199, "島根県"=>22163, "岡山県"=>70233, "広島県"=>107108, "山口県"=>41017, "徳島県"=>29703, "香川県"=>37998, "愛媛県"=>50582, "高知県"=>24964, "福岡県"=>199072, "佐賀県"=>23483, "長崎県"=>40497, "熊本県"=>66986, "大分県"=>45627, "宮崎県"=>37295, "鹿児島県"=>56030, "沖縄県"=>57888, ""=>4049}
Memory 24312 KB
Real time 0:17.07
User time 15.49
System time 0.20

Pythonほどではありませんが、17秒と十分高速になりました。配布元では3倍以上とされていましたが、このサンプルでは約6倍の高速化です。

Ruby標準のcsvモジュールに比べると機能はシンプルで、ヘッダをカラム名として解釈するようなオプションもありませんが、速度優先の処理の際には使いどころがありそうです。

さらにここから速度をあげようとすると、CSVのパースではなく入力のオーバーヘッドの調整になります。一行読み込みも読み込みバッファサイズを調整する余地が出てきますが、これ以上の深追いはしません。

スクレイピングとCSV

スクレピングの中間データはRDBで保管することが多いのですが、外部システムとのやりとりにはCSV形式でやりとりすることが大半です。スクリプト言語でサーバーサイドでCSVを操作するテクニックは意外と現役で役にたっています。

ちなみにgoでやってみると

スクリプト言語の比較というよりも、CとRustの違いになってしまってるので、コンパイル言語のgoでも似た処理をやってみます。

package main

import (
    "encoding/csv"
    "fmt"
    "io"
    "os"
)

func main() {
    reader := csv.NewReader(os.Stdin)
    prefCntMap := make(map[string]int)
    for {
        row, err := reader.Read()
        if err == io.EOF {
            break
        }
        prefCntMap[row[9]]++
    }
    fmt.Printf("%v\n", prefCntMap)
}
 /usr/bin/time -f "Memory %M KB\nReal time %E\nUser time %U\nSystem time %S"  \
> go run csv2.go  < 00_zenkoku_all_20230731.csv
map[:4049 三重県:53407 京都府:105784 佐賀県:23483 兵庫県:189958 北海道:220166 千葉県:216654 和歌山県:29623 埼玉県:247028 大分県:45627 大阪府:438099 奈良県:38252 宮城県:78128 宮崎県:37295 富山県:34754 山口県:41017 山形県:32721 山梨県:33587  岐阜県:69749 岡山県:70233 岩手県:32731 島根県:22163 広島県:107108 徳島県:29703 愛媛県:50582 愛知県:264853 新潟県:73191  東京都:1225687 栃木県:71953 沖縄県:57888 滋賀県:40776 熊本県:66986 石川県:42057 神奈川県:345276 福井県:29758 福岡県:199072 福島県:70081 秋田県:29622 群馬県:73265 茨城県:90139 長崎県:40497 長野県:76797 青森県:36861 静岡県:121038 香川県:37998 高知県:24964 鳥取県:19199 鹿児島県:56030]
Memory 49520 KB
Real time 0:04.01
User time 3.16
System time 0.39

4秒と最速です。コンパイル言語ではあるもののスクリプト的にも使えるので比較対象にしてみました。

Pocket

CONTACT

お問い合わせ・ご依頼はこちらから