計算機科学において抽象グラフの最も一般的な用途は、依存関係の追跡である。 私たち(このドキュメントを読んでいるあなたを含めて)の日常における依存関係 追跡の一例を示すと、コーディングしたプログラムソースファイルにおける コンパイル依存性で、これらの依存性は、make や Visucal C++ のような IDE といったビルドシステム内部で、いくらかの変更が行われた ソースファイルに対して、再コンパイルしなければならないファイル数を 最小限にするために使用される。
図1に、killerapp というプログラムを 作成するために使用されるソースファイル、オブジェクトファイル、ライブラリファイルそれぞれを頂点として表現したグラフを示した。グラフ中の線は、 どのファイルが他のファイルを作成するのに使用されるか示す。 矢印の方向をどちらにするかという点は任意であるが、慣習的に私たちは、 矢印方向の意味を「使用された」といように使っており、反対方向の意味を「依存する」 というように使っている。
make といったビルドシステムは、次のような質問に答える事ができる:
これから、依存関係グラフにおけるこれらの質問を公式化し、解決できるようなグラフ・アルゴリズムを例で示していく。 図1 のグラフは、これから示す全ての例で利用される。このサンプルのソースコードは examples/file_dependencies.cpp にある。
ここにグラフデータの構築方法を示す。 私たちは単純に手作業でグラフを構築してきた。しかし make といったビルドシステムは、代わりに Makefile からファイルのリストを得て、依存関係の調査設定を行う。グラフを表すために adjacency_list クラスを使う。 vecS という選択子は std::vector を意味しており、辺(頂点間)リスト(効率的な経路横断をもたらす)を表すのに使用される。 directedS という選択子は 有向グラフを利用するという意味であり、 color_property はグラフの各頂点に色プロパティを付加する。色プロパティは、次節以降のアルゴリズムで利用していく。
enum files_e { dax_h, yow_h, boz_h, zow_h, foo_cpp, foo_o, bar_cpp, bar_o, libfoobar_a, zig_cpp, zig_o, zag_cpp, zag_o, libzigzag_a, killerapp, N }; const char* name[] = { "dax.h", "yow.h", "boz.h", "zow.h", "foo.cpp", "foo.o", "bar.cpp", "bar.o", "libfoobar.a", "zig.cpp", "zig.o", "zag.cpp", "zag.o", "libzigzag.a", "killerapp" }; typedef std::pair<int, int> Edge; Edge used_by[] = { Edge(dax_h, foo_cpp), Edge(dax_h, bar_cpp), Edge(dax_h, yow_h), Edge(yow_h, bar_cpp), Edge(yow_h, zag_cpp), Edge(boz_h, bar_cpp), Edge(boz_h, zig_cpp), Edge(boz_h, zag_cpp), Edge(zow_h, foo_cpp), Edge(foo_cpp, foo_o), Edge(foo_o, libfoobar_a), Edge(bar_cpp, bar_o), Edge(bar_o, libfoobar_a), Edge(libfoobar_a, libzigzag_a), Edge(zig_cpp, zig_o), Edge(zig_o, libzigzag_a), Edge(zag_cpp, zag_o), Edge(zag_o, libzigzag_a), Edge(libzigzag_a, killerapp) }; using namespace boost; typedef adjacency_list<vecS, vecS, directedS, property<vertex_color_t, default_color_type>, property<edge_weight_t, int> > Graph; Graph g(N, used_by, used_by + sizeof(used_by) / sizeof(Edge)); typedef graph_traits<Graph>::vertex_descriptor Vertex;
1つのプロジェクトで make される最初の事項は、全てのファイルをコンパイルする事である。様々なファイル間で与えられた依存関係により、果たして正しい順序でコンパイルとリンクを行えるだろうか? 最初に構築されたグラフを公式化する必要がある。コンパイル順序を見つけることは、グラフの頂点を並び替える事と同じである。順序付けの制約条件とは、辺として表現したファイルの依存関係である。言い換えれば、グラフ中の (u,v) という辺に対して、v は u よりも前に現れないようにする順序付けと説明できる。この種の制約条件による順序付けは topological sort (トポロジカル・ソート)と呼ばれるもので行う。それゆえに、コンパイル順序に対する質問の答えは、BGL アルゴリズム topological_sort() を呼び出すだけでよい。トポロジカル・ソートの伝統的な出力形式は、ソート済み頂点の連結リスト構造である。 BGL アルゴリズムは(伝統的な出力形式を使用せず)任意の OutputIterator (出力イテレータ)へソート済み頂点を出力するので、より柔軟な出力を得る事ができる。ここでは、std::list<Vertex> の先頭へ頂点を挿入できる出力イテレータとして std::front_insert_iterator を生成し、使用した。他にもファイルへの出力イテレータ、異なる STL コンテナや特別に作成したコンテナに対する挿入イテレータを利用することができる。
typedef std::list<Vertex> MakeOrder; MakeOrder make_order; boost::topological_sort(g, std::front_inserter(make_order)); std::cout << "make ordering: "; for (MakeOrder::iterator i = make_order.begin(); i != make_order.end(); ++i) std::cout << name[*i] << " "; std::cout << std::endl;
上記出力結果:
make ordering: zow.h boz.h zig.cpp zig.o dax.h yow.h zag.cpp \ zag.o bar.cpp bar.o foo.cpp foo.o libfoobar.a libzigzag.a killerapp
ビルドシステムがおそらく答えなければならない質問は、”どのファイルとどのファイルが並列にコンパイルできるだろうか?”であった(以降、コンパイルという用語がリンクも含まれるという意味でビルドに置き換わる)。 この質問に対する答えは、多数のスレッドや複数のプロセッサを利用することによるビルドの処理速度向上をもたらすであろう。 この質問は、次のような軽い問いかけに置き換える事ができる。並列に無制限のファイルをビルドできると仮定して、最短ビルド回数は何回だろうか?要点は、あるファイルがビルドできるという事は、そのファイルが依存している全てファイルが既にビルド済みであるという事である。問題を簡略化するために、各頂点で表される1ファイルは(ヘッダファイルでさえ)必ずビルドされると仮定する。何回目にビルドを行うかというスケジュールを決定する主たる着眼点は、あるファイルが依存している全てのファイル中で、依存連鎖しているファイルに対する最大の累積ビルド回数を選択するということである。
始点方向の隣接頂点における計算値を基準として、新たなの計算値を求める考え方は、Dijkstra(ダイクストラ)の単一始点最短経路アルゴリズムと同じである( dijkstra_shortest_paths() を見よ)。この状況と最短経路アルゴリズムとの大きな違いは隣接頂点の最小値の代わりに最大値を選択していくという事である。加えて、頂点が単一始点ではないことも違うが、これについては入次数が0となるような頂点(言い換えれば、どの有向辺も入り込んでいない頂点)全部を始点として取り扱うことにする。従って、デフォルトのパラメータの代わりに、いくらかの特別なパラメータを与えて、ダイクストラのアルゴリズムを使用する。
dijkstra_shortest_paths() を使うには、このアルゴリズムで使用される頂点と辺のプロパティを最初に設定しなければならない。回数プロパティ(ダイクストラのアルゴリズムにおける距離プロパティを置き換えたもの)と、辺の重みプロパティが必要である。回数プロパティを保持するために std::vector を利用する。辺の重みプロパティは、”グラフ設定”時の組込機能を通じて既に設定されているため、この内部の重みプロパティを重みマップとして、ここで宣言する。
std::vector<int> time(N, 0); typedef std::vector<int>::iterator Time; using boost::edge_weight_t; typedef boost::property_map<Graph, edge_weight_t>::type Weight; Weight weight = get(edge_weight, g);
次に行うことは、入次数が0の頂点を最短経路検索の”始点”頂点として識別することである。入次数は、以下のループ(繰り返し)で計算することができる。
std::vector<int> in_degree(N, 0); Graph::vertex_iterator i, iend; Graph::out_edge_iterator j, jend; for (boost::tie(i, iend) = vertices(g); i != iend; ++i) for (boost::tie(j, jend) = out_edges(*i, g); j != jend; ++j) in_degree[target(*j, g)] += 1;
次には、コンパイルの”コスト”(累積されたコンパイル回数)を決定する必要がある。この場合、私たちは、それぞれのファイルに対して、そのファイルが直接依存しているファイル中から、累積コンパイル回数の大きいものが選択されることを望んでいる。ゆえに、比較の関数オブジェクトを std::greater<int> として定義する。また、ダイクストラのアルゴリズムにコンパイル回数の和を計算する方法を与えねばならず、和の関数オブジェクトを std::plus<int> として定義する。
std::greater<int> compare; std::plus<int> combine;
ここに至って、言わば uniform_cost_search() という検索アルゴリズムを呼び出す準備が整った。 私たちは、グラフ中の全頂点を探査し、入次数が0の頂点ならば、この検索アルゴリズムを呼び出す。
for (boost::tie(i, iend) = vertices(g); i != iend; ++i) if (in_degree[*i] == 0) boost::dijkstra_shortest_paths(g, *i, distance_map(&time[0]). weight_map(weight). distance_compare(compare). distance_combine(combine));
最後に、それぞれの頂点に対して計算された(何回目にコンパイルするという)スケジュールを出力する。
std::cout << "parallel make ordering, " << std::endl << " vertices with same group number" << std::endl << " can be made in parallel" << std::endl << std::endl; for (boost::tie(i, iend) = vertices(g); i != iend; ++i) std::cout << "time_slot[" << name[*i] << "] = " << time[*i] << std::endl;
parallel make ordering, vertices with same group number can be made in parallel time_slot[dax.h] = 0 time_slot[yow.h] = 1 time_slot[boz.h] = 0 time_slot[zow.h] = 0 time_slot[foo.cpp] = 1 time_slot[foo.o] = 2 time_slot[bar.cpp] = 2 time_slot[bar.o] = 3 time_slot[libfoobar.a] = 4 time_slot[zig.cpp] = 1 time_slot[zig.o] = 2 time_slot[zag.cpp] = 2 time_slot[zag.o] = 3 time_slot[libzigzag.a] = 5 time_slot[killerapp] = 6
ビルドシステムが答えることができる別の質問は、”ファイル依存関係中に循環参照が含まれていないか?”である。もし循環参照があれば、ビルドシステムはユーザに対して循環参照を取り除く事ができるようにエラーとして報告する必要がある。循環参照を調べる簡単な方法の1つは、 depth-first search (深度優先探査) を実行する事であり、その探査が(探査中の木の中で)訪問済頂点を再度探査する場合には、そこに循環が存在する事になる。1度訪問された頂点は灰色に塗られるので、深度優先探査中のこの色に対するチェックを対象とする事ができる。 BGL 中のグラフ探査アルゴリズム( depth_first_search() 深度優先探査関数も含む)は、全て visitor (ビジタ)という機構を通じて拡張可能である。ビジタは関数オブジェクトに似ている。しかし、 operator() のような関数オブジェクトとは異なり、複数のメソッドを持っている。ビジタのメソッドは、グラフの探査アルゴリズム中の特定条件を満たす箇所で呼び出されるような機能拡張をユーザに与える。 ビジタに関する詳細な記述は Visitor Concepts (ビジタ・コンセプト) の節を見よ。
ここでビジタ・クラスを作成し、DFSVisitor コンセプトのメソッドで、DFS(深度優先探査)が新たな辺を探査するときに呼び出される back_edge() メソッドを実装する。 これは色プロパティの値をチェックすることにより、循環依存が存在するかどうかを判定することのできるメソッドである。 dfs_visitor<> から継承することにより、ビジタ・コンセプトに必要な他のメソッドが、無動作の状態で実装される。 一旦このビジタ・クラスを実装すれば、 BGL アルゴリズムとして何度でも探査するために構築し流用できる。ビジタ・オブジェクトは BGL アルゴリズム内部では値渡しなので、このビジタ・オブジェクト中の has_cycle フラグは値として保持される。
template <class Color> struct cycle_detector : public dfs_visitor<> { typedef typename boost::property_traits<Color>::value_type C; cycle_detector(Color c, bool& has_cycle) : _has_cycle(has_cycle), color(c) { } template <class Edge, class Graph> void back_edge(Edge e, Graph& g) { _has_cycle = true; } protected: bool& _has_cycle; Color color; };
ここで BGL depth_first_search() アルゴリズムを呼び出し、循環参照探査ビジタとして探査する。
bool has_cycle = false; cycle_detector<Color> vis(color, has_cycle); boost::depth_first_search(g, visitor(vis)); std::cout << "The graph has a cycle? " << has_cycle << std::endl;
図1 におけるグラフ中には(点線は無視する)循環参照が存在しない。しかし、もしここで bar.cpp と dax.h に依存関係を加えたならば、循環参照が出来上がる。このような依存関係はユーザエラーとして記録される。
add_edge(bar_cpp, dax_h, g);
Copyright © 2000-2001 | Jeremy Siek, Indiana University (jsiek@osl.iu.edu) |
Japanese Translation Copyright (C) 2003 OKI Miyuki
オリジナルの、及びこの著作権表示が全ての複製の中に現れる限り、この文書の複製、利用、変更、販売そして配布を認める。このドキュメントは「あるがまま」に提供されており、いかなる明示的、暗黙的保証も行わない。また、いかなる目的に対しても、その利用が適していることを関知しない。