💨

なぜ私はデータ処理においてNimをPythonの代わりに使うのか(翻訳)

2021/09/26に公開

この記事は以下の翻訳です
Why I Use Nim instead of Python for Data Processing

怠け者のプログラマーは、計算の手間をプログラミングの手間に置き換えたがるものです。私はまさにそのようなプログラマーです。私の研究では、テラバイト級の大規模データを対象としたアルゴリズムを設計・実行することがよくあります。NIHのフェローである私は、10万台以上のプロセッサを搭載したクラスターであるBiowulfを利用していますが、大きなMapReduceを実行すればよいのであれば、1つの実験のためにシングルスレッドのパフォーマンスを最適化するために膨大な時間を費やすことは、通常は意味がありません。

このようなリソースがあるにもかかわらず、私はデータ処理タスクにプログラミング言語のNimを使うことが多くなりました。Nimは計算科学の分野ではあまり評価されていませんが、非数値データ処理のためのPythonに代わる非常に有能な言語です。NimはPythonのように簡単に書けて、Cのように高速です。Nimは、コンパイル言語の性能と動的言語の表現力を併せ持つ新世代の言語の一つです。Pythonを知っていれば、Nimの90%を既に知っているも同然で、その恩恵を受けることができます。

次のような簡単な問題を考えてみましょう。から始まる行がヘッダーコメントとして区切られたテキストファイル(つまりFASTA形式)に保存されたDNA配列(A、T、G、Cの4文字からなる文字列)の塊があります。標準的なタスクは、配列中のGとCの割合を計算することで、これはGCコンテンツとして知られる指標です。Pythonでは、簡単な実装は以下のようになります。

# Python
gc = 0
total = 0

for line in open("orthocoronavirinae.fasta"):
    if line[0] == '>': # ignore comment lines
        continue
    for letter in line.rstrip():
        if letter == 'C' or letter == 'G':
            gc += 1
        total += 1

print(gc / total)

このコードは、私のラップトップを使って150MBのコロナウイルスゲノムデータセットを実行するのに23.43秒かかります。代わりにNimを使えば、無料で大幅なスピードアップが図れます。実際、Nimの実装は次のようにほとんど同じです。

# Nim
var gc = 0
var total = 0

for line in lines("orthocoronavirinae.fasta"):
    if line[0] == '>': # ignore comment lines
        continue
    for letter in line:
        if letter == 'C' or letter == 'G':
            gc += 1
        total += 1

echo(gc / total)

コード的な変更は小さくても、パフォーマンス的にはかなり大きな恩恵があります。

言語 実行時間 Nimとの比較
Python 3.9 23.43秒 30.6x
PyPy 7.3 2.54秒 3.3x
Nim 1.4 (-d:danger --gc:orcをコンパイルオプションに付けて) 0.765秒 1.0x

完全に最適化されたコンパイルと実行のサイクルを実行すると、PyPy を使用するよりも速いのです。Nimには、コンパイル後にプログラムを自動的に実行する-rコマンドオプションがあるので、簡単に実行できます。Nimはコンパイル型の言語ですが、コンパイルプロセスが速いので、インタプリタ型の言語の代わりに使うことができます。

ほとんど同じですが、NimのコードとPythonのコードにはいくつかの違いがあります。

  1. 変数は var を使って宣言されます。 Nim では let を使って実行時定数を定義し、コンパイル時に変異の可能性をチェックすることができます。また、const を使ったコンパイル時の定数もサポートしています。
  2. ファイルの内容を一行ごとに繰り返すために、openではなくlines関数が使われます。lines関数の優れた点は、LFやCRLFなどの改行文字を自動的に取り除いてくれるので、doline.rstrip()を使う必要がないことです。
  3. Nimではprintではなくechoを使用します。細かい違いはいくつかありますが、echo の利点は Python 2.x 時代の print-as-a-statement スタイルの表現が復活したことです。 (例: echo "Hello world!")

Nimの便利さを説明するために、私が研究で遭遇した実際の例を考えてみましょう。私のデータには回文が多く含まれており、新しいウイルスの探索に支障をきたしていました。人工物の特徴は、長い部分配列とその逆相(部分配列を逆にして、各塩基を対応するペアに置き換えたもの)が存在することでした。問題の範囲を把握するためには、データセットの各配列を調べ、人工物である可能性が高いかどうかを確認し、それを取り除く必要がありました。まず、Pythonの標準的な方法を見てみましょう。

# Python
import sys
from Bio import SeqIO
from Bio.Seq import Seq

# 配列の長さkのイテレーター
def kmers(seq, k):
    for i in range(len(seq) - k + 1):
        yield seq[i:i+k]

for record in SeqIO.parse(sys.argv[1], "fasta"):
    unique_kmers = set()
    palindrome = False
    for kmer in kmers(record.seq, 25):
        if kmer.reverse_complement() in unique_kmers:
            palindrome = True
            break
        unique_kmers.add(kmer)
    if not palindrome:
        print(record.format("fasta"))

そしてNimです

# Nim
import os
import sets
import bioseq # 筆者が作ったライブラリ。k-merイテレーターとFASTAのパーサーを含みます
for record in readFasta[Dna](paramStr(1)):
  var uniqueKmers = initHashSet[Dna]()
  var palindrome = false
  for kmer in kmers(record, 25):
    if kmer.reverseComplement() in uniqueKmers:
      palindrome = true
      break
    uniqueKmers.incl(kmer)
  if not palindrome:
    echo(record.asFasta)

繰り返しになりますが、これらのプログラムは基本的に同じです。ここでの顕著な違いは

  1. Nimはコマンドライン引数を取得するのにparamStrを使います
  2. 標準の命名規則は、snake_case ではなく camelCase です。Nim は、大文字(最初の文字を除く)とアンダースコアを取り除いた後に同じものがあれば、変数は等しいものとして扱います。つまり、好きなスタイルを使用することができます。

同じように簡単に書けるにもかかわらず、Nimのコードは20倍も速いのです。アルゴリズムに大きな違いがないと仮定すると、PythonがNimに素のパフォーマンスで勝つことは本質的に不可能でしょう。というのも、NimのコンパイルプロセスではCファイルが生成され、それが好きなCコンパイラでコンパイルされるからです。Nim は標準 C 出力を生成するため、Python と互換性があります。NimからPythonを呼び出したりPythonからNimを呼び出したりすることができます。

Nimは経験豊富なPythonユーザーが自分の知識を簡単に翻訳できるようにしてくれますが、Nimが(より速いPythonではなく)独自の言語として輝き始めるのは、よりイディオム的なコードを書くときです。私は、Nimの他の素晴らしい言語機能のおかげで、Nimのプログラムは通常Pythonのプログラムよりも短く簡単に書けることに気付きました。これらの特徴は1つの記事の範囲を超えています。私はこの1年間、日常的にNimを使用してきましたが、そのパフォーマンス、シンプルさ、エレガントさに感銘を受け続けています。データを処理したいときは、ぜひNimを試してみてください。あなたのCPUに感謝することでしょう。

Discussion