Pascoe本記事は、原著者の許諾のもとに翻訳・掲載しております。Pascoeは、部屋に座っているAdRollのシニアデータサイエンティストです。プログラミング言語Dは、効率的にタスクをこなすためにデータサイエンスチームがすぐに好んで使うようになりました。今では重要なインフラストラクチャに欠かせない言語になっています。それはD言語が多くを提供するからです。他の典型的なデータサイエンスワークフローと比較して、D言語を使用する最も明確な利点の1つは、マシンコードにコンパイルできるという点です。Hadoopフレームワーク、R、Pythonのような他のツールよりかなり高速にデータ中からリッピングできます。D言語のコンパイラは、多くの場合、まるでスクリプト言語のように高速に実行できます。100万の一様ランダム変量を生成し、ソートし、十分位数を見つける処理について、D言語とPythonを比較してみましょう。0m0.032sD言語では以下のようになります。0m0.177s待ってください。時間がかかっていますね。それは、最初にコードを実行した際のコンパイル時間が含まれているからです。コードを変更せずに再度実行すると、次のようになります。0m0.004sよくなりましたね。rdmdは、コードの変更がなければ、わざわざ再コンパイルしません。コードの計算がより複雑になって、かなりの量のデータ上で多くの計算を行わなくてはならなくなったら、こういった節約がかなり重要になります。本当に重要なのはそこではありません。超効率的なコードが必要な場合、コンパイラ型言語が一番だということは分かっています。推奨されるCやC++のような他の効率的な言語とD言語を区別する重要な点は、D言語なら一定の時間に最も快適に感じるスタイルで自由にプログラミングできるということです。上記のコードは、よりシンプルなPythonでさらに頭を抱えることなく、D言語で素早く”スクリプト”を記述する方法を示しています。D言語はオブジェクト指向のコードを書き始める場合にもきれいに記述できます。0.4177また、D言語ならハイパフォーマンスなニーズにいつでも応えられます。高速な逆平方根計算をしたい場合、(リンク先のWikipediaの記事から軽く修正された)何らかのポインタvoodooを使用できます。160.249577最大限のパフォーマンスを引き出したい場合、D言語を使えばインラインアセンブリを書くことすらできるでしょう。これは全てただのお遊びです。D言語は現実世界のシナリオでどのように役立つでしょうか。データ中からのリッピングAdRollで仕事をしている過程で、D言語でうまく機能していたインフラストラクチャがいくつかありましたが、ある時点で、データの問題がコードで設計された範囲を超えた時、最適化しなければならなくなりました。信じられないかもしれませんが、その問題は区切り形式のファイルからフィールドを引き出すくらいによくあることでした。以下は、私たちがやったことの要点です。この特定のログファイルには、ASCIIレコードセパレータによって区切られた広告データがいくつか含まれています。タイムスタンプとそのデータがどの国から来たのかを引き出したいとしましょう。想像できるでしょうが、単純なD言語のソリューションはかなり読みやすいのですが、たぶん期待するほど動きはよくありません。この1つのログファイルはAdRollで毎日生成されている約130TBのログファイルの単なるサンプルにすぎず、圧縮されていません。問題をばらまいてしまう可能性はありますが、クラスタをスピンアップするのに時間がかかりますし、私たちのモットーの1つは、”少ないものでより多くのことを行う”です。ここでパフォーマンスを向上させてスケーラビリティを抑えるにはどうすればよいでしょうか。私たちができる最適な対処法の1つとして割り当てるメモリの量を極小化する方法があります。現在行っている手順として、行を読み込むごとに新たにchar[]を割り当ています。この場合、一度読み込んでchar[]に代入した行をレコードセパレータで分割するために、再度読み込むことになります。この分割によってその行にある全てのフィールドにおいて配列を生成し、その全てにメモリを割り当てるようになるのです。効率的な観点から見ると、単純なはずのコードが実際にはかなり乱雑なものなっていることは明らかです。メモリの割り当てについて最初にできる対処法は、読み込むために既に割り当てられているバッファを設けるということです。ここでの問題は、前の行に続いて次の行が即座に同じバッファに入り、最終的にバッファに収まりきらなくなるということです。最悪、扱おうとしているフィールドを部分的にしか取得できないかもしれないのです。この解決法として2つのバッファを用意して交換しながら使用する方法があります。扱うフィールドが両方のバッファにまたがっているかもしれないので、簡単に復元できるよう2倍長のバッファを構成するようにもなります。現行のバッファの終わりまで到達すると、それを”最終バッファ”とします。一方のバッファにさらなるデータを読み込んでいき、それを現行のバッファにします。注意点として、両方のバッファを合わせたものより長い行がないことを確認する必要があります。次に考えるべきことは非効率な分割についてです。完全に行を分離するのではなく、メモリに割り当てられるごとに、ただ順次それを読み込んでいきましょう。バッファ(配列のインデックス)と現在の行(それまでにあった区切り文字の数)の両方を通して進行をたどっていきます。ある数の区切り文字に遭遇したとき、次の区切り文字を探し出すだけで、それらの間にある内容がフィールドだと分かります。改行を読み込むことで行の最後に行きついたことが分かると、行の進行をリセットします。最後に、全てのフィールドが集まると、次の改行に行きつくまでバッファリングするだけになります。説明はこのくらいにして、コードを見ていきましょう。process_file(args[1]);}同じ動作をするシンプルなプログラムですが、前よりかなり洗練されました。詳しく見ていきましょう。bufferAとbufferBは2つの補助的なバッファで、current_bufferまたはlast_bufferによって指定されます。これはnum_buffでたどったときの状態によって変わってきます。current_bufferによる進行をたどる場合はindexを使い、行による進行をたどる場合はそれまでにあった区切り文字の数を表すためにnum_delを使用します。最後にline_endで行の最後に行きついたかどうかを判断します。FastReaderを使用した例が簡単です。それはbufferAから始めます。新しい行の最初に行きついたときにreset_progress()が呼び出され、そのことを反映させるために状態をアップデートします。swap_and_load()はトグリングを行うメソッドになります。2と同等であることを確認してください。file.rawRead()メソッドを使用することで生じる問題もあります。char[]を用意し、そこにFileからデータを入れていくのですが、ファイルの最後まで行きつくと、char[]はその前の内容をEOFの先に入れてしまいます。test456789私たちがやろうとしている目的として、この動作はあまり好ましくありません。実際にいつFileや最終行の末尾に到達したか知りたいからです。結局、file.rawRead()でも新しくなった配列の内容の一部に前の内容を含めてchar[]で返すようになると分かります。file.rawRead(buffer)に変更するだけで解決します。testswap_and_load()では同じ構築を使用し、これにより最終的にcurrent_bufferの進行が0にリセットされます。get_field()は特に複雑ですが、それでも分かりやすいメソッドです。行の末尾に到達している場合は検索するフィールドがないため、これは不明であると記述されます。区切り文字のカウントはcurrent_bufferの場所から始めます。current_bufferの末尾に到達したらswap_and_load()を実行します。区切り文字が正しくカウントされたら、次のフィールドを検索する必要があります。基本的には同じコードを使用しますが、この処理でバッファをスワップするかどうかを把握している必要があります。改行に到達したら、その改行をフィールドの末尾としてカウントします。フィールドの構築は単純です。スワップを行わなかった場合、フィールドは開始から終了までのインデックスのcurrent_bufferの一部分になります。それ以外の場合はlast_bufferとcurrent_bufferを組み合わせたものになります。advance_to_next()メソッドも同じ方法で区切り文字を検索しますが、改行文字を検索する代わりに次の行に移動してreset_progres()を実行します。ここで気を付けるべき点があります。それはeof()メソッドがあることです。EOFと検出されてもまだ読み取る行が残っている可能性があります。読み取る行が残っていない場合は、indexの長さがcurrent_bufferと同じになるはずです。最後に、process_file()の呼び出しで必要なフィールドが全て取得されてから次の行に移動していることを確認します。フィールドの検索は番号順に行うことに注意してください。行の検索は順番に行われ、さかのぼることはないため、これは厳守する必要があります。コードに少しボリュームが出てきました。これは決して無駄な増量ではありません。こちらをご覧ください。0m0.068sなんと、時間が6分の1に短縮されました。同じ時間でより多くのデータを検索できるようになりましたね。エラーがないか、念のため差分を確認してみましょう。country_info_fast%>問題ないようです。マルチコアのマシンで実行しているなら、一度に複数行を読み取りたいと思いませんか。その場合、どうすればいいでしょう。}}意外と簡単でした。パフォーマンスはどう変化するでしょうか。0m0.781s4倍の量のデータを実行しているのに、元の方法よりスピードが5分の1に短縮されました。パフォーマンスが20倍も向上したということです。終わりではありません。rdmdが実行する最適化の回数がこれまでよりも減っています。コードが最適化を必要とする状態になったら、スクリプト構文で実行するのではなくdmdでコンパイルすることが有効になります。公正を期すため、元の単純な方法と並列化されていない方法でもコンパイルを行います。0m1.264s全てをコンパイルすることによって、この高速な方法は正統な方法と比べてパフォーマンスが12倍近く向上します。並列化された方法で4倍の量のデータを実行した場合には、22倍も向上しています。AdRollデータサイエンスのメンバーは、すっかりD言語に夢中です。その理由はもうお分かりでしょう。新しいインフラストラクチャと分析タスクのプロトタイプは簡単に作成できます。効率が重視されるようになれば、同じコードベースを最大限のパフォーマンスが引き出されるようにリファクタリングできるのです。効率的で無駄のないコードを使ってビッグデータの問題を解決したいという方は、ぜひ私たちの仲間になってください。ツイート翻訳に対するフィードバックがございましたら、メールまたはGitHubのIssueよりお寄せください。Ltd.リクルートグループサイトへ。