FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

DSLでビルドツールを自作する (14日目 並列実行についての検討)

この記事は「Qiita Advent Calendar 2019 DSLで自作ビルドツールを作ろう」の14日目の記事です。

14日目 並列実行についての検討

Makeには並列実行の機能があります。つまり、make -j32などと並列数を指定すると、それだけ分のコアを使用してビルド処理を行ってくれる、非常に便利な機能です。これと全く同じことをRumy-Makeで実現しようとすると、ジョブキューを作って指定されたコア数だけジョブを取り出して並列実行するという面倒くさそうな実装になるので、あまりそんなことはやりたくありません。

あまりコア数に気を使う事は無く、今回は単純にRubyの並列実行の機能を使用してジョブの並列化をできないかということを検討しました。

Rubyの並列実行ライブラリ

Rubyの並列化ライブラリを探していると、Parallel Gemというものを見つけました。単純にループ実行を並列化してくれるライブラリのようです。

https://www.xmisao.com/2018/07/22/how-to-use-ruby-parallel-gem.html

これだと、依存関係グラフの依存している複数の子を並列に実行させることができそうです。早速やってみます。

    target_older_depends_list = Array.new
    if target.depend_targets.length == 0 then
      target_older_depends_list = [true]
    else
      target_older_depends_list =  Parallel.map(target.depend_targets){|dep|
        target_older_depends = false
        skip_do_target = false
        if $target_list.key?(dep) and $target_list[dep].is_external then
          # External Target
...

Parallel.mapという機能を使いました。Parallel.mapでは、引数として指定したリストに並列にブロックを適用します。まあ極端な話、

target.depend_targets.each{|dep
    ...
}

としていたこれまでの処理を、

target_older_depends_list =  Parallel.map(target.depend_targets){|dep|
    ...
}

としただけなのであまり驚きはありません。ただし、そこから先でいろいろと考えなければなりません。これまで、依存するターゲットのタイムスタンプに応じて本ターゲットを実際に実行するかどうかを決めていました。このために、すべての依存するターゲットを回るときに、target_older_dependというBool値の変数を1つ用意して、1つでも依存ターゲットに本ターゲットよりも新しいものがあれば、target_older_dependをTrueに設定して本ターゲットを実行していました。しかし、今回はすべての依存ターゲットは並列に実行されるので、変数1つでは足りません。そこで、Parallel.mapは並列に実行した結果をリストで返すという機能を使用して、target_older_depends_listというリストを用意し、各依存ターゲットと本ターゲットの依存関係をリストで取得することにしました。このリストの中に一つでもTrue(ある依存ターゲットが本ターゲットよりも新しい)を見つければ、本ターゲットは再実行されます。

    # 依存ターゲットリストの中に1つでもTrueが存在する、
    # つまり依存ターゲットの中に1つでも本ターゲットよりも新しいものが存在した場合、となる。
    if target_older_depends_list.include?(true) then
      # Execute commands!
      target.commands.each {|command|
        # puts "#{command}"
        result = `#{command}`
        puts result
      }
    end

Parallel.mapの中の処理も、Breakしたりcontinueしたりするとおかしな動きをするので、注意深く書き換え、ブロックの最後に依存判定の結果をTrue/Falseで返すようにします。

    end
...
    if not skip_do_target then
      do_target(dep)
    end
    # puts "[DEBUG] : Depends Target \"#{dep}\" : " + target_older_depends.to_s
    target_older_depends
  }   # Parallel.mapの終わり

これで同様にSwimmer-RISCVのビルドを行います。コア数は4、スレッド数は8で実行を行いました。

RUBYLIB=${HOME}/work/rumy-make/src ./build.rb  84.03s user 53.16s system 533% cpu 25.720 total
RUBYLIB=${HOME}/work/rumy-make/src ./build.rb  46.69s user 30.08s system 87% cpu 1:27.64 total