Travis CI上で、g++とclang++の複数バージョンでビルドする
C++でライブラリを作っていて、それをTravisCI上でテストします。そのとき、GCCとClangの両方でテストしたっかったのですが、いろいろ面倒だったのでメモです。
基本的には、Travis CIのmatrixという機能を使って様々なバージョンのコンパイラを使ったテストを独立・並行に立ち上げることができます(GCCとClangの複数バージョンをテストしたいので、単にcompiler
を設定するだけでは不足です)。
以下が面倒なポイントです
- GCC, Clangのそれぞれについて、バージョンに応じたリポジトリを追加・パッケージを入れる必要がある
- GCC:
apt / source
に、ubuntu-toolchain-r-test
を加える - Clang: それに加え、
3.8
までと3.9
以降で追加リポジトリが違う。Clangのバージョンに応じてllvm-toolchain-precise-X.Y
もしくはllvm-toolchain-trusty-X.Y
を加える
- GCC:
- Clangの注意:
- C++11/14以降を使うためには
libc++
が必要なので、libc++-dev
,libc++abi-dev
をインストールする - C++で書かれた依存ライブラリが必要な場合は、それも
libc++
でビルドする必要がある。(C++ のABIが違うので)。aptパッケージなどのビルド済みバイナリは使えない -stdlib=libc++
をCMAKE_CXX_FLAGS
に加えた状態でビルド
- C++11/14以降を使うためには
といったあたりがややこしいポイントです。
自分が現在開発しているucx-playgroundというライブラリでは、以上の試行錯誤を全部詰め込んだ結果、無事に動きました(ライブラリというよりは実験コード置き場ですが)
GitHub - keisukefukuda/ucx-playground
Travis CI - Test and Deploy Your Code with Confidence
そのときに使った .travis.yml
は、
これです。リポジトリ内にある travis/gen_yaml.py
というPythonスクリプトで自動生成しています。
MPI Communicatorのattributesを利用して、MPI_Finalize()時にコールバック関数を起動する
MPIのややマニアックな機能の1つに、attriburtesがあります。これは、コミュニケータに、整数をキーとした辞書のようなデータ構造で値(属性)を登録できるというものです。関係する関数としては、 MPI_Comm_create_keyval
、 MPI_Comm_set_attr
などがあります。
一方、定義済みコミュニケーターの1つに、 MPI_COMM_SELF
というものがあります。これは、そのプロセス自分自身のみが属する大きさ1の特殊なコミュニケータです。このコミュニケーターは、普通はあまり役に立たないのですが、MPIの終了時に最初に破壊されることが保証されているという特殊な性質を持っており、これを利用して MPI_Finalize()
時のコールバック関数を登録することができます。
MPI 3.1仕様書の当該箇所を引用します。
8.7.1 Allowing User Functions at Process Termination
There are times in which it would be convenient to have actions happen when an MPI process finishes.
This can be accomplished in MPI by attaching an attribute to MPI_COMM_SELF with a callback function. When MPI_FINALIZE is called, it will first execute the equivalent of an MPI_COMM_FREE on MPI_COMM_SELF. This will cause the delete callback function to be executed on all keys associated with MPI_COMM_SELF
日本語訳
8.71. プロセス終了時に関数を呼び出す
MPIプロセスの終了時に動作を起こすことができると便利な場合があります。(中略)これは、
MPI_COMM_SELF
にコールバック付きのattributeを設定することで、MPIに実行させることができます。MPI_FINALIZE
が呼ばれるとき、最初にMPI_COMM_FREE
相当の処理をMPI_COMM_SELF
に対して行います。これにより、attributesの削除コールバックが起動されます。(以下略)
というわけで、この仕組を使うと、MPIの終了時にフックした関数を実行できます。以下は、具体的なコード例です。
#include <stdio.h> #include <mpi.h> // MPI_Comm_delete_attr_function int my_comm_attr_delete_fun(MPI_Comm comm, int keyval, void* attr_val, void* extra_state) { fprintf(stderr, "my_comm_attr_delete_fun is called()\n"); MPI_Comm *newcomm = (MPI_Comm*) attr_val; MPI_Comm_free(newcomm); return MPI_SUCCESS; } int main(int argc, char **argv) { MPI_Init(&argc, &argv); /* 独自のCommunicatorを作成。サンプルとして、終了時にこのCommunicatorを削除してみる */ MPI_Comm newcomm; MPI_Comm_dup(MPI_COMM_WORLD, &newcomm); /* 新しく作る属性のKey */ int keyval; /* 属性を作成 */ MPI_Comm_create_keyval(MPI_COMM_NULL_COPY_FN, my_comm_attr_delete_fun, &keyval, NULL); /* COMM_SELFに属性を設定 */ MPI_Comm_set_attr(MPI_COMM_SELF, keyval, &newcomm); /* 通常通り MPI_Finalize() を呼ぶ */ MPI_Finalize(); return 0; }
# 実行例 $ mpicc test.c $ mpiexec -n 2 ./a.out my_comm_attr_delete_fun is called() my_comm_attr_delete_fun is called()
コールバックが実行されていることがわかります。
Pyhtonでクロージャを使おうとして、Pythonの変数スコープにハマった話
クロージャは便利です。
以下のようなコードを書いて、期待と動作が違ったので数時間を溶かしてしまいました。詳しい人から見れば常識なのでしょうが…
import threading import itertools import time A = [1,2,3] B = ['a', 'b', 'c'] def main(): for prod in itertools.product(A,B): # `prod` を束縛したクロージャを作る def f(): for i in range(10): # このクロージャは、prodの値を10回表示して終了する print(prod) time.sleep(0.5) if prod == (1,'a'): # `prod`の値が (1,'a')のときだけクロージャ発動 t = threading.Thread(target=f) t.start() time.sleep(1) main()
さて、クロージャf
は変数prod
を束縛して、それをただ表示させているだけです。prod
の値が(1,'a')
のときだけクロージャを作成して起動しているのですから、出力は以下のようになると期待しました。
# 期待される出力 (1, 'a') (1, 'a') (1, 'a') (1, 'a') (1, 'a') (1, 'a') (1, 'a') (1, 'a') (1, 'a') (1, 'a')
ところが実際の出力はこうなります
# 実際の出力 (1, 'a') (1, 'a') (1, 'b') (1, 'b') (1, 'c') (1, 'c') (2, 'a') (2, 'a') (2, 'b') (2, 'b')
こうなる理由は以下のとおりです。スコープ外の変数を参照するクロージャを作った場合、そのクロージャのスコープに束縛されるのではなく、スコープ内で代入されない限りは上位レベルのスコープが参照されます。なので上位スコープ(参照サンプルコードの場合はグローバルスコープ)のprod
の値が参照されるので、forループによって変数の値が変更されると、それを反映してしまいます。一般的に、このルールはグローバル変数への適用において遭遇することが多いですが、関数スコープ内でクロージャを作成するときにも適用されるんですね。
コードを以下のように直せば、期待通りに動きます
import threading import itertools import time A = [1,2,3] B = ['a', 'b', 'c'] def main(): for prod in itertools.product(A,B): # `prod` を束縛したクロージャを作る def f(): prod2 = prod # <---- 新たな変数prod2へ代入する for i in range(10): # このクロージャは、prod2の値を10回表示して終了する print(prod2) time.sleep(0.5) if prod == (1,'a'): # `prod`の値が (1,'a')のときだけクロージャ発動 t = threading.Thread(target=f) t.start() time.sleep(1) main()
まとめ
Hy使おう(錯乱)