RubyでのApache Arrowの使い方(Parquetもあるよ)
Apache Arrow、名前は知ってたけど縁遠いなあと思っていた。最近CSV処理するスクリプト書いてて、そういやデータフォーマットでArrowってあったなっと思って、ちょっと触ってみたのでした。
結論、「正しく使えば」、速くなる。
Apache Arrow
まだよく分かってないんだけど、Apache Arrowは新しいデータフォーマット。
インメモリーでデータを扱う時に最適化されたフォーマット
言語やプロセス間でのデータ交換を高速にするのを目標としている
列指向のフォーマット
という特徴を持っているらしい。PythonのPandasの開発者も開発の中心的人物みたい。詳しくは公式サイトで: https://arrow.apache.org/
Arrowでのファイル読み込み
CSV、Arrow、Parquetのベンチマーク
「列指向」ってことだから(CSVは反対に行指向フォーマット)、一行一行を舐めるような操作は遅いのかな、とか思ったりしつつも、ほんとのところどうなんだろうとベンチマークを取ってみました。
環境
Ubuntu 18.04 on WSL
Ruby 2.6.5
CSV 3.1.2
Red Arrow 0.15.1
CPUコア 8個
WSLはIOが遅いことで有名なのでその辺は差し引いてください。
対象ファイル
% xsv count sample-data.csv 82141 % ruby -rarrow -rparquet -e 't = Arrow::Table.load("sample-data.csv"); t.save("sample-data.arrow"); t.save("sample-data.parquet")' % ls -lh sample-data.* Permissions Size User Date Modified Name .rw-r--r-- 95M kitaitimakoto 20 Dec 18:28 sample-data.arrow .rw-rw-rw- 61M kitaitimakoto 20 Dec 18:27 sample-data.csv .rw-r--r-- 33M kitaitimakoto 20 Dec 18:28 sample-data.parquet
というわけで、
CSV 61MiB、Arrow 85MiB、Parquet 33MiBのファイルサイズ
8万行ちょっとのデータ量
のファイルを元にベンチマークを取ってみました。
余談だけど、CSVファイルを調べるならxsvはすごく便利なコマンドなので是非入れよう。
ベンチマーク
後でスクリプトも置くけど、「ある列(数値)の合計値を計算する」という処理を行っています。
最初がRuby標準添付のCSVライブラリーで処理を行った結果、
次がFastestCSVっていうCで実装したgemでの結果、
三番目がRed ArrowのCSV読み込み機能でCSVファイルを読み込んだ時の結果、
以降三つがRed Arrowでの各メソッドでの処理結果、
以降三つがRed Parquestでの同様の結果、
となっている。
% ruby ./arrowbenchmark.rb
(snip)
user system total real
CSV(Ruby 標準添付CSVライブラリー) 850541456
24.625000 0.625000 25.250000 ( 24.844013)
CSV(FastestCSV RubyGem) 850541456
1.343750 0.171875 1.515625 ( 1.507318)
CSV(Red Arrow each_record_batch) 850541456
11.609375 4.328125 15.937500 ( 10.657816)
CSV(Red Arrow each_record) 850541456
68.671875 3.078125 71.750000 ( 67.946809)
CSV(Red Arrow 対象カラムだけ each) 850541456
4.281250 2.781250 7.062500 ( 2.939687)
Arrow(each_record_batch) 850541456
10.390625 0.125000 10.515625 ( 10.410525)
Arrow(each_record) 850541456
68.359375 0.078125 68.437500 ( 68.751072)
Arrow(対象カラムだけ each) 850541456
2.203125 0.000000 2.203125 ( 2.236634)
Parquet(each_record_batch) 850541456
11.125000 1.968750 13.093750 ( 10.353967)
Parquet(each_record) 850541456
12.828125 1.093750 13.921875 ( 12.133342)
Parquet(対象カラムだけ each) 850541456
4.031250 0.781250 4.812500 ( 2.798203)
これ見て分かるのは、
「正しく使えば」 FastestCSV > Arrow > Parquet > CSV の順で速い
Arrowで行操作すると遅い
特に
each_record
はピュアRubyのCSVより遅いぐらいなので必ずeach_record_batch
使うこと可能なら対象カラムだけ取り出して(
find_column
)操作するべし
ということかな。
追記。「更に正しく使えば」Arrowが圧倒的に速かった。こちらの記事参照:RubyでのCSV処理はApache Arrowが速い
後掲のスクリプト見ると分かるように、取り出したデータに対して to_i
を読んでいる。CSVを読み込んで何もせずArrowやParquet形式で保存しているので、対象カラムが文字列のままだからだ。
本当は、ここをuint32とかuint64とかにして保存してやるとさらに高速化できるんだろうなと思いつつ、Arrowのテーブルからそういう別のテーブルを作る方法がいまいち分からないというか、気軽にベンチマークとる上ではめんどそうなので今日はやめました。仕事で使っててどうにもパフォーマンスが欲しいとなったら挑戦するかも。あと何となく時間できた時とか。
ベンチマークスクリプトはこちら:
require "benchmark"
require "csv"
require "fastest-csv"
require "arrow"
require "parquet"
CSVFILE = "sample-data.csv"
ARROWFILE = "sample-data.arrow"
PARQUETFILE = "sample-data.parquet"
Benchmark.bmbm do |x|
x.report "CSV(Ruby 標準添付CSVライブラリー)" do
amount = 0
CSV.foreach CSVFILE, headers: true do |row|
amount += row[4].to_i
end
puts amount
end
x.report "CSV(FastestCSV RubyGem)" do
amount = 0
headers = true
FastestCSV.foreach CSVFILE do |row|
if headers
headers = false
next
end
amount += row[4].to_i
end
puts amount
end
x.report "CSV(Red Arrow each_record_batch)" do
amount = 0
Arrow::Table.load(CSVFILE).each_record_batch do |records|
records.each do |record|
amount += record[4].to_i
end
end
puts amount
end
x.report "CSV(Red Arrow each_record)" do
amount = 0
Arrow::Table.load(CSVFILE).each_record do |record|
amount += record[4].to_i
end
puts amount
end
x.report "CSV(Red Arrow 対象カラムだけ each)" do
amount = 0
Arrow::Table.load(CSVFILE).find_column(4).each do |record|
amount += record.to_i
end
puts amount
end
x.report "Arrow(each_record_batch)" do
amount = 0
Arrow::Table.load(ARROWFILE).each_record_batch do |records|
records.each do |record|
amount += record[4].to_i
end
end
puts amount
end
x.report "Arrow(each_record)" do
amount = 0
Arrow::Table.load(ARROWFILE).each_record do |record|
amount += record[4].to_i
end
puts amount
end
x.report "Arrow(対象カラムだけ each)" do
amount = 0
Arrow::Table.load(ARROWFILE).find_column(4).each do |record|
amount += record.to_i
end
puts amount
end
x.report "Parquet(each_record_batch)" do
amount = 0
Arrow::Table.load(PARQUETFILE).each_record_batch do |records|
records.each do |record|
amount += record[4].to_i
end
end
puts amount
end
x.report "Parquet(each_record)" do
amount = 0
Arrow::Table.load(PARQUETFILE).each_record do |record|
amount += record[4].to_i
end
puts amount
end
x.report "Parquet(対象カラムだけ each)" do
amount = 0
Arrow::Table.load(PARQUETFILE).find_column(4).each do |record|
amount += record.to_i
end
puts amount
end
end