[C++][ADAPT]データ分析、処理ライブラリADAPTの更新情報(1)。

ADAPTの更新情報である。リポジトリを公開してから3ヶ月間で色々と修正や機能追加してきたが、そのあたりについて簡単にまとめておく。

ADAPTについての説明はこちらへ。
ADAPTのGitHubリポジトリはこちらへ。

DTable、STableの追加

ADAPTがもともと有していたコンテナは、DTreeとSTreeである。これらは階層構造を基本としており、構造化されたデータを扱いやすい反面、速度的にはどうしてもテーブルに劣ってしまう。そこで、より高速なDTable、STableを追加した。

DTableは-1層と0層の要素しか持つことが出来ない代わりに処理がかなり簡素化されており、DTreeと比べて余計なオーバーヘッドがない。すごく大雑把に言えば、Treeと比べて走査や抽出などが2~3倍ほど速い。 ただし、STableは0層までしか定義しないSTreeと機能的に全く同等である。もう少し正確に言えば、0層までしか定義しないSTreeは今回の更新でSTableと同等の機能を呼び出すように変更した。0層までのSTreeは従来より高速化し、STableと同等になったわけである。よって、STableを敢えて使う意味は、テーブルであることを明示したいとき以外には特にない。そんな無意味なものを用意したのは、対称性を気にしてしまう物理屋の性だ。

使い方は0層までしか持たないTreeとほぼ同等である。違うのはDTableの0層の構造を定義するときに呼び出す関数だけ。

//DTreeの場合
DTree tree;
tree.SetTopLayer({ "root_field_name", FieldType::Str } });
tree.AddLayer({ { "field_name0", FieldType::I32 }, { "field_name1", FieldType::F64 } });
//treeの場合はさらに1層、2層を追加できる。

//DTableの場合
DTable table;
table.SetTopLayer({ "root_field_name", FieldType::Str } });
table.SetLayer(0, { { "field_name0", FieldType::I32 }, { "field_name1", FieldType::F64 } });
//DTableは1層以下を扱えない。SetLayer(0, ...)と0を与えているのは将来的な拡張の可能性があるため。

Extractの並列化

次のように既存のTreeから新たにTreeを生成するExtractという機能があるが、これを並列処理に対応させた。

DTree extract = tree | Filter(...) | Extract(...);

スレッド数はデフォルトではstd::hardware_concurrency()によって取得されるその処理系のスレッド数に一致する。明示的に指定したい場合はadapt::SetNumOfThreads(num_of_threads)を呼ぶ。
またadapt::SetGranularity(gran)とすることで、各スレッドの処理する粒度を指定することができる。デフォルトでは128である。この粒度granに基づき、0層要素をgran個ごとに区切って、区切られた区画ごとにスレッドが処理をしていく。granの値が小さすぎるとスレッド間の競合が増えるのでパフォーマンスが落ちるが、粒度が大きすぎるとスレッドごとの処理負荷が均一でなくなるためこれもパフォーマンスが落ちる。適切な粒度の値はtreeの構造や要素数によって異なるだろうし、tableを相手にするならもっと大きな値のほうが良いかもしれない。デフォルトの128という値も今後変更する可能性がある。

8コア16スレッドのRyzen 7 7700Xで、最大階層2、全245万要素のDTreeからExtractするテストを行ったところ、シングルスレッドで135ms、マルチスレッドで19msとまずまず順当に高速化できていた。DTableだと素の速度が早いことと各スレッドから得た結果をマージするコストが相対的に大きいことからそれほど効果はなく、粒度を調整してもせいぜい2-4倍程度の向上に留まった。
近いうちToVectorも並列化するつもりである。

Treeの要素の追加、削除などを行う関数を追加

Treeの要素を追加する関数としては、今まではReserveとPushのみが事実上使用可能な状態だった。ここに新たにResize、Assign、Insert、Erase、Popの5種類の関数を追加した。

//下層の要素数をsize個にする。std::vectorのresizeに近い。
void Resize(BindexType size);

//自身の各フィールドにそれぞれvs...を代入する。代入演算子のようなものと考えて良い。全フィールドに一括代入したい時に。
template <class ...Fields>
void Assign(Fields&& ...vs);

//下層要素のindex番目にvs...をフィールドとして持つ新たな要素を挿入する。std::vectorのinsertに近い。
//現時点では複数個を一気に挿入することはできない。
template <class ...Fields>
void Insert(BindexType index, Fields&& ...vs);

//下層の末尾要素を削除する。std::vectorのpop_backみたいなもの。
void Pop();

//下層要素に対して、指定されたindexからsize個分を削除する。std::vectorのeraseみたいなもの。
void Erase(BindexType index, BindexType size);

AND/OR演算子の短絡評価を可能に

Placeholderを用いたAND/OR演算のラムダ関数を定義するとき、内部的に短絡評価できていなかったので、これを改善した。例えば以下のコードでは、Filter関数に与えたlambdaを実行する際、x == 5がfalseであった場合にもy == 10が評価されていた。明らかに無駄な計算なので、短絡計算するよう修正した。

DTree tree;
auto [x, y, z] = tree.GetPlaceholder("x", "y", "z");
auto lambda = (x == 5 && y == 10);
tree | Filter(lambda) | Show(z);

なお、ラムダ関数中で使えるif_/switch関数も同様に、余計な評価を回避するようにした。

細かな修正

  1. KeyJoint使用時にコンパイルができない場合がある不具合の修正。
  2. first/last階層関数の追加。
  3. DTreeの要素数変化を伴う操作を行うときに、全要素がtrivially_copyableである場合に余計なオーバーヘッドを減らすように修正。

他にも色々とバグ修正をしたような気がするが、細かいところは覚えていない。

雑記

案の定大して使ってもらえていないようだが、仕方ない。OSSなんてそれなりに人目に触れる場所で宣伝しなければ気づいてさえもらえないことは、私もよく知っている。
たとえ需要がなくとも、自分自身が欲しているので更新は続ける。研究に導入できるかどうかは他人を説得できるかどうかに掛かっているのではっきり言って大博打だが、導入できれば利便性について大きなメリットがある。

Extractの並列化が終わったので(かなり冗長でよろしくない書き方になっているので、後々修正したいが……)、そろそろ3Dビューアの制作に着手したいところである。研究の方でかなり大型のファイルを扱う機会が増えており、旧ADAPTをベースとした3Dビューアでは持て余すようになってきたのだ。OpenADAPTは従来比でメモリ使用量60%減、速度10倍くらいに向上させたので1、大規模データ解析を行う上では何としてもこちらに切り替えたい。
しかし色々とハードルがある。特に気がかりなのは文字列式からラムダ関数を生成する機能で、任意のデータを可視化するためには必須なのだが、原理的に可能なもののコンパイル時間の肥大化が心配だ。今のところ、根本的な解決方法は思いついていない。どうしたものか。


  1. 旧ADAPTは私がC++歴2-3年の頃に設計したもので、当時の私の技術不足がよく分かる数字である。

[SlackLogViewer]Slack過去ログ閲覧ツール更新(8)。

1年近くほったらかしていたSlackLogViewerの更新である。特に機能追加などはないので、主として不具合修正である。

SlackLogViewerの説明はこちらへ
Windows、macOS版のダウンロード先はこちらへ

Qt5のサポート終了

メンテナンスをしていて初めて、OpenSSL 1.1.1のサポートが終了していることを知った。SlackLogViewerはC++とQtを使って作られているが、Qt5はOpenSSL 3.xをサポートしていないことから、Qt6へと完全移行し、Qt5のサポートは打ち切ることにした。Qt6は6.2.4の時点だと若干バグがあったりしたのだが、今では修正されているようだったので、問題はないと思っている。

いやまあ、Qt5でのビルドが全くできなくなったわけではないし、OpenSSL 1.1.1を何処かから拾ってきたりして導入すれば今でもQt5版を使うことはできる。どうしてもQt5が良ければ私は関知しないので自己責任の範囲でご自由にどうぞ。もともと完全自己責任で使うべきOSSではあるので、どのようなトラブルに遭おうが全ては使用者の責任である。と、責任逃れをしておく。

JSONフォーマットの異常への対応

Slackのエクスポートファイルは多数のJSONファイルで成り立っているのだが、これらの中の情報はあちこちが欠損していて、なぜだかメッセージ内容が消えていたり、添付ファイル情報が丸ごと消えていたり、スレッドの親メッセージが消えていたりする。こうした情報欠損は本当に厄介で、どう考えても必須の情報が普通に欠損しているので、「このフィールドが存在しないなんてありえない」、と決めつけた箇所がそこかしこにあった過去バージョンでは頻繁にクラッシュが発生していた。
今回、こうした情報がどれほど不可欠な情報であっても欠損する可能性があると想定したコードに書き換えた。どこかに抜けはあるかもしれないので、そのあたりは見つけ次第直していく。
ただ、GitHubのIssuesに届いている不具合報告のうちどれだけを修正できたかは不明である。これらの修正の多くはすでにテストビルド版として不具合を訴える人たちに提供してみたのだが、あまり直らなかったようなので、まだ色々と不具合が潜んでいる可能性は高い。

Slackが公開範囲の限られたチャットツールである以上、ファイルを開けない不具合はたいてい修正不可能である。エクスポートファイルを提供してもらえないのだからバグの再現ができないのだ。これは私の能力の問題ではなく、単に怠慢なわけでもなく、チャットツールの性格上の問題である。不具合報告自体は受け付けるが、エクスポートファイルを私に公開する勇気のない人は修正されることを期待しないでほしい。

ところで

実は今回の不具合修正のうち大半は去年の8月頃に行ったものである。が、いかんせんその頃から本業のほうが急激に忙しくなって、今の今までリリースに至らず放置されていたのだ。いや、うん、申し訳ない。こっちは時間も金もない中で何とか暇を捻出しながらサポートしている状態なので、今後もバグ報告等に即時対応できる可能性は低い。OSSは利益にならないなぁ、ホント。

[C++]C++20対応のインタプリタを使いたい。Clingの導入。

C++20に対応したインタプリタを使ってみた話。CERNのROOTというデータ分析ツールのプロジェクトチームが同時にClingというC++インタプリタを開発しており、これを導入してみた話である。ClingはLLVMに基づくインタプリタであり、公式にはClangにできることは全てできると宣伝されている。ROOT開発者の言うことなんか信じないぞ!

なお、C++インタプリタについてはxeus-clingROOTに同梱されているClingなどもあるのだが、前者はC++20へ対応出来ていない様子で、後者はpre-compiled binaryがC++17までしか対応していない1上にROOTの本体が付随してきて鬱陶しいので使わない。ただし、ROOT本体がくっついてきても特に気にならない、C++20が必要ないのならROOTのpre-compiled binaryをインストールするのが最も楽である。

今回はWSL(Ubuntu 22.04)上で行った。いずれWindows上でも試してみたいが、果たして。

Clingのビルド

Clingはビルド済みバイナリも配布しているようであるが、こちらは2020年11月を最後に更新が止まってしまっているし、そもそもWindows用は配布されていなかった。当然最新のLinux向けも存在しない。なので、ソースコードからビルドすることにする。

Git、GCC、CMake、Ninjaあたりは事前にインストールされているものとする。GCC 13で試した。
なお、CMakeではビルド時の一時ファイル保存先とインストール先のディレクトリを分けることが多いと思うが、Clingの場合は分けないほうが良い。ビルド時のファイルとインストールした先のファイルの双方を残しておく必要があるためだ。ビルドの際に作成された何かしらのファイルを実行時にも要求するらしく、削除したら動かなくなった。

git clone -b cling-llvm16 https://github.com/root-project/llvm-project.git llvm_src
git clone https://github.com/root-project/cling.git cling_src
mkdir cling
cd cling
cmake -G Ninja -DCMAKE_CXX_STANDARD=20 -DCMAKE_BUILD_TYPE=Release -DLLVM_BUILD_TOOLS=Off -DLLVM_EXTERNAL_PROJECTS=cling -DLLVM_EXTERNAL_CLING_SOURCE_DIR=../cling_src -DLLVM_ENABLE_PROJECTS=clang -DLLVM_TARGETS_TO_BUILD="host;NVPTX" ../llvm_src/llvm
cmake --build . -j4
cmake --build . --target install

必要であれば、.bashrcに次のように追記しclingのインストール先にパスを通しておく。

export PATH=$PATH:/path_to_cling_install_dir/bin

動かしてみる

WSL上でclingと入力すると、インタプリタが開始する。あとは#include ...やらstd::vector<double> v...やら必要なコードを一行ずつ書いていけばその通りに動く。

ROOTでは色々なヘッダが予めincludeされていたりusing namespace std;されていたりと鬱陶しかったが、Clingではそのようなことはないらしい。

ライブラリを読み込ませる

Clingは外部からヘッダを読ませたり、共有ライブラリを読ませたりできるので、サードパーティのライブラリなども共有ライブラリを使えるのなら実行可能である。cling実行時にオプションで渡すだけで良い。

cling -I/path/to/include/directory -L/path/to/lib/directory -lyourlibname.so

試しにMatplot++を共有ライブラリにして呼んでみたが、ちゃんと動いた。 ただしスタティックライブラリは対応していない。サポートすること自体は可能らしいが、人手不足によって進んでいない様子だった。

マクロにして与える

一般的なインタプリタ言語のように、テキストファイルでマクロを渡して実行させることももちろんできる。
マクロのファイル名と同じ名前の関数を定義しておくことで、cling実行時にその関数が実行される。ROOTと同じ仕様である。

//testmacro.C
#include <iostream>

void testmacro()
{
    std::cout << "testmacro" << std::endl;
}

あとはこれを.xオプションでclingに渡せば良い。

cling .x testmacro.C

確認したバグ

マクロにまとめて実行したときは問題なかったが、一行ずつインタプリタにて入力しているとき、構造化束縛がうまく動かなかった。構造化束縛で宣言した変数にアクセスすると、undeclared variable扱いされるのだ。現代のC++でこれを使えないのはちょっと厳しい。後日Issueを投げておこうと思う。

また、cling実行時ではなく、既に実行されているclingの中で.xオプションでマクロを与えてもちゃんと動くのだが、同じマクロを連続して(clingを終了することなく続けて)再実行すると、謎のエラーを吐いて落ちる。これについてはエラーメッセージが意味不明だったので、よくわからない。

閑話

C++コンパイル型言語である。これは揺るぎない事実である。私の用途上、プログラミング言語は速度に優れていることから分野内でデファクトスタンダードと化しているC++を選択せざるを得ないので、私は常にコンパイルにそれなりの時間的コストを支払いながら作業している。もちろん、それ相応に実行時のコストが減るので、別に言語自体に不満を持ってはいない。
しかし、たまにはインタプリタ型言語的にインタラクティブに何かを実行したいときもある。科学計算系ではこういうとき、主にPythonが使われる。ただPythonには 死ぬほど遅い という地獄のような欠点が常に付きまとう。

スクリプト言語的に使いたい、しかし時にはコンパイルして速度を出したい。これを両立する良い方法はないものだろうか?もちろんC++Pythonを使い分けるのも良い。ただ普段C++で使っているライブラリをPythonで呼び出す、あるいはその逆をしようと考えると、それはそれで余計な悩みが増えたりもする。私はPybind11で自作ライブラリのPythonバインディングを書いたことがあるが、あれもバージョン互換性など色々な問題でコンパイルエラーを起こしたりするので、それほど容易ではないのだ。またファイルの読み出しの部分をPythonで書けない(Pythonからデータ格納処理を呼ぶと恐ろしく時間がかかる)という問題も発生し、なかなか使いづらい状況が出来上がってしまった。

一番嬉しいのは、普段使っているライブラリなども全てひっくるめて、C++の文法そのままにインタラクティブに実行できることだ。ちょっとしたコードはスクリプト的に動かして、計算が遅ければ全く同じコードをただコンパイルするだけでよい。そんなことができれば日頃の作業が格段に楽になる。Clingは案外、その理想に近い所まで来ているようだった。先日公開したOpenADAPTが、意外とそのまま動いてしまったのだ。完全ではないが、このサンプルコードのQuickStart_dtreeがほぼそのまま動いた。C++20を随所に取り入れておりそれなりにややこしい設計になっているこれが普通に動いてしまうとは……。まあ一部手直しした部分もあったし、QuickStart_streeは動かなかったし、奇妙なバグも見つけはしたが、普段解析をするときはDTreeが動作すれば事足りるので、それほど大きな問題ではない。
ただ流石にヘッダオンリーで書いてしまったせいで、実行時には数秒程度であるがコンパイル時間がかかる。また最適化オプションもあるものの実行速度は通常のデバッグビルドに毛が生えた程度らしい。やはり部分的にでも共有ライブラリ化できるように整理したほうが良いかもしれない。

こんなしょうもないことをしているのも、周囲の人間がROOTを使ってインタラクティブにデータ分析しているのを傍で眺めて「俺もあれやりたい!」と思ってしまったからだ。私はROOTが嫌いでずっと使ってこなかった人間なので、今まではC++Pythonを組み合わせて何とかしようとしてきた。が、もう何度も何度も何度も何度もPythonのナメクジみたいな遅さに足を引っ張られ続け、ついにはPybind11が謎のエラーを吐いて自作モジュールのビルドすら一筋縄ではいかなくなり、いい加減Pythonが嫌になったのだ。

そんなわけでC++インタプリタなるどこの誰が使うんだか分からないもんに手を出しているわけである。ROOTはPythonからも動くようになったのに、未だにC++で書かれることが多いのは、ビッグデータ解析のためにPythonなんか使ってられない業界もある、ということなのだろう。日頃からTB単位のデータ処理、解析を行っている私も多分似たようなものだ。Clingのおかげで、普段の些細な解析はインタプリタで、大規模な処理はコンパイルして、と使い分けることが出来そうで、希望が見えてきた。ちょっとだけ見直したよ、ROOT。ちょっとだけね。


  1. ROOTの6.28/04以降であれば、自前でビルドすることでC++20を有効化することはできるようだ。公式に案内されているビルド手順に加え、ROOTビルド時のCMakeのオプションに-DCMAKE_CXX_STANDARD=20を与えれば良い。