おさかなせいざ

プログラミングメモや日記がわりに

PackageCompiler と高速な実行

 この記事は,Julia Advent Calendar 2018の18日目の記事です. 17日目のphigasui - Qiitaさんは17日時点ではまだ掲載されていません. リンクがわかり次第追記します.

Julia いいよね

 みなさん Julia 使ってますか?

Julia いいですよね.

C のように速く, Python のように書きやすく, MATLAB のような高度な数値計算もできる(https://julialang.org). そんな甘い謳い文句に誘われ私も Julia を書き初めました.

 噂の通り Julia は大変書きやすく,また高速に動作します. 数値計算やコンピューティングに関して多くのコミュニティがあり, Github 上にも各分野に特化した Organization があります.

 去年度もそうでしたが, 今年もハマっている言語で学園祭の紹介ページを建てようと考えたので, Julia の Genie.jl というフレームワークを使用し, こんなページの Web サーバサイドを書きました (フロントエンドの多くは友人たちが作ってくれました). まだ他言語のような Web における有名なフレームワークはないようですが, 皆 Web 大好きですので,これからどんどんとでてくるのではないでしょうか.

それはそれとして起動速度が問題

 さて,バリバリと勢いにのる Julia ですが,現在ある問題(だと自分は思っている)を抱えています.
それは,初回の実行が遅いことです. Julia の動作手順はInitialization of the Julia runtime · The Julia Languageに詳しく書かれていますが, かいつまむと起動,パース,JIT Compile,実行,といった流れです. この中で実行までに最も時間がかかる処理が JIT Compile だと思われます.
Julia では関数ごとに型推論を行い,型を確定して生成できる関数について実行時コンパイルを行います. これがどのくらいかかるかというと AtCoder では Julia v0.5 において JIT Compile 時に, ほとんど時間を持っていかれるとのことです.

ただし,Julia で問題に回答されている方もいらっしゃるので,まったく出来ないというわけではないようです. また,julia v0.5 から v1.0 では多くの最適化が行われているので,AtCoder では現状より高速になると考えられます. しかし,JIT Compile に時間がかかるのは間違いないありません. そこで PackageCompiler.jl が使えるのではないかと考えました.

PacakgeCompiler.jl

 特に断りがない限り Ubuntu 18.04 で以下の環境を想定しています.

Julia Version 1.0.2
Commit d789231e99 (2018-11-08 20:11 UTC)
Platform Info:
  OS: Linux (x86_64-pc-linux-gnu)
  CPU: Intel(R) Core(TM) i7-4710MQ CPU @ 2.50GHz
  WORD_SIZE: 64
  LIBM: libopenlibm
  LLVM: libLLVM-6.0.0 (ORCJIT, haswell)

 PackageCompiler は Julia のコード(Module)を BaseCoreなどの基本モジュール, 実行に必要な諸々を含め共有ライブラリ(Linux であれば .so)にコンパイルしてまとめるパッケージです. 必要であれば,C言語から共有ライブラリの特定の関数を呼び出す形で実行ファイル生成してくれます.

 それでは PackageCompiler.jl を追加しましょう.juliaコマンドを実行して,]でパッケージモードに入ります.

(v1.0) pkg> add PackageCompiler
  Updating registry at `~/.julia/registries/General`
  Updating git-repo `https://github.com/JuliaRegistries/General.git`
 Resolving package versions...
 Installed ProgressMeter ───── v0.9.0
 Installed Distances ───────── v0.7.4
 Installed DecFP ───────────── v0.4.7
 Installed OffsetArrays ────── v0.9.1
 Installed Reactive ────────── v0.8.3
 Installed CategoricalArrays ─ v0.5.2
 Installed GtkReactive ─────── v0.5.3
  Updating `~/.julia/environments/v1.0/Project.toml`
 [no changes]
  Updating `~/.julia/environments/v1.0/Manifest.toml`
  [324d7699] ↑ CategoricalArrays v0.5.1 ⇒ v0.5.2
  [55939f99] ↑ DecFP v0.4.6 ⇒ v0.4.7
  [b4f34e82] ↑ Distances v0.7.3 ⇒ v0.7.4
  [27996c0f] ↑ GtkReactive v0.5.2 ⇒ v0.5.3
  [6fe1bfb0] ↑ OffsetArrays v0.9.0 ⇒ v0.9.1
  [92933f4c] ↑ ProgressMeter v0.8.0 ⇒ v0.9.0
  [a223df75] ↑ Reactive v0.8.2 ⇒ v0.8.3
  Building DecFP → `~/.julia/packages/DecFP/XA6lk/deps/build.log`

表示が異なることがあるかと思いますが,エラーがなければ問題ありません.

PackageCompiler には juliac.jl というコマンドラインツールが含まれています. 今回はこのコマンドラインツールを使用してコンパイルを行ってみましょう.

 以下が比較するコードです.竹内関数です.
jtest.jljulia jtest.jl と実行するコードです.
test.jlにはC言語から呼び出される関数が macro と共に定義されています.

ではコンパイルです.

$ julia ~/.julia/packages/PackageCompiler/jBqfm/juliac.jl -vaer test.jl 

を実行して

All done

がでればコンパイル完了です.

.
├── builddir
│   ├── test
│   ├── test.a
│   └── test.so
├── jtest.jl
└── test.jl

ディレクトリは上記のような構成になります. builddir/testが実行ファイルです.

それでは複数回実行し,バイナリとコードの実行時間を比べてみます.

$ time for i in `seq 1 10`; do  julia jtest.jl; done
tak(12,6,0) = 12
c = 12604861
tak(12,6,0) = 12
c = 12604861
tak(12,6,0) = 12
c = 12604861
tak(12,6,0) = 12
c = 12604861
tak(12,6,0) = 12
c = 12604861
tak(12,6,0) = 12
c = 12604861
tak(12,6,0) = 12
c = 12604861
tak(12,6,0) = 12
c = 12604861
tak(12,6,0) = 12
c = 12604861
tak(12,6,0) = 12
c = 12604861

real    0m5.055s
user    0m6.445s
sys 0m5.894s

$ time for i in `seq 1 10`; do ./builddir/test; done
tak(12,6,0) = 12
c = 12604861
tak(12,6,0) = 12
c = 12604861
tak(12,6,0) = 12
c = 12604861
tak(12,6,0) = 12
c = 12604861
tak(12,6,0) = 12
c = 12604861
tak(12,6,0) = 12
c = 12604861
tak(12,6,0) = 12
c = 12604861
tak(12,6,0) = 12
c = 12604861
tak(12,6,0) = 12
c = 12604861
tak(12,6,0) = 12
c = 12604861

real    0m4.899s
user    0m6.496s
sys 0m5.779s

バイナリになってちょっとだけ速くなった...?
あまりぱっとしません. 実行してみた方は分かるかと思いますが, ほんのすこし実行速度が早くなっている代わりに,それ以上のコンパイル時間を必要とします. Julia そのものを build しているのと変わらないでそれはそうです.

しかし,共有ライブラリを生成することで多くの環境で動作させるられるので, 配布する目的であれば julia のない環境でも動作し大変有用です.

ただし,多くのものを共有ライブラリに含めるため,ファイルサイズが大きくなります.

$ ls -l builddir/test*
-rwxrwxr-x 1 pisces pisces     13176 12月 17 22:38 builddir/test
-rw-rw-r-- 1 pisces pisces 130327352 12月 17 22:38 builddir/test.a
-rwxrwxr-x 1 pisces pisces 125952168 12月 17 22:38 builddir/test.so

おそらく libjulia.so などを適宜設定をすれば回避可能と思いますが,詳しくないためここでは省きます.

結論

 結果から,実行速度を向上させるという目的では PackageCompiler.jl は適切ではないと考えられます. C 言語からの活用,配布といった目的にあったパッケージであると感じました.

蛇足

 以下は PackageCompiler 以外の方法で高速に julia を起動実行するのに必要そうなことです.

.ji ファイル?

.ji は Julia の precompile 済みファイルである. 単純に .jl ファイルを実行するだけでは生成されない. 基本的には add 済みの Module がusing, import, requireによって読み込まれた際に生成される. 先程追加した PackageCompiler であればおそらく ~/.julia/compiled/v1.0/PackageCompiler/ にjiファイルがあるはずである.

Julia v1.0 ドキュメントjulia コマンド の switches には記載がないが, Julia v0.6 ドキュメントには

--output-ji name Generate a system image data file (.ji)

の通り.jiの記述がある. issue など流し読みしたところでは,Julia 自体の開発用途以外でこれらの switch は使用されること稀なため, このページ,およびコマンドヘルプからは削除されたようである(Developer Document には記載がある). precompile は内部的にはこの switch を使用している. ライブラリを使用する際, precompile 済みのものを使用したい場合には以下を実行する.

$ julia --compiled-modules=yes 

通常 Pakcage のアップデートがない限り,通常 precompile 済みのものが使用される. 試しに,--compiled-modules=noとしてusing Images, TestImages, ImageView(JuliaImages のパッケージ)としたところ

$ time julia --compiled-modules=no -e "using Images, TestImages, ImageView"

real    0m48.914s
user    0m48.800s
sys 0m0.801s

$ time julia --compiled-modules=yes -e "using Images, TestImages, ImageView"

real    0m9.838s
user    0m9.827s
sys 0m0.722s

precompile を使用するかどうかで5倍以上も速度が異なる.
ここから分かるとおり,.jl ではなく .ji から実行できれば JIT Compile も少なく高速な動作を期待できると考える. しかし,現状では .ji を自体を実行する手段がない,もしくは私が単純に見つけられていないため, 知っている方がいらっしゃればご教授願いたい. 基本的に Package としてある Module のみを対象にしているようである1

高速実行

 方法として一応,Package にして main の内容を __init__ に指定,git の管理下に置き pkg> add . とすることで, $ julia -e 'using Hoge' と2度(一度目に precompile される)すれば,高速に実行できなくもないが手順が多すぎる.

いい感じの方法が見つかったらまた書きます.(見つけたら うおざ (@p1scesCom) | Twitter に教えてもらえると嬉しいです)

Julia よくなってくね

 ここまで読んでいただき有難うございます.

 先程確認したところドキュメントに julia v1.0.3 が追加されていました. julia は毎月の勢いでアップデートしていますね.すごいです. 変化に対応しながら,今後も継続して Julia を使っていきたいと思います.

 明日はY_oHr_N - Qiitaさんの記事です.

Clojure * duct * opencv で高専時計(美人時計)を作った

この記事は Clojure Advent Calendar 2017 12日目の記事です。

※注:半分くらい日記、技術内容は下のほうなので、そこだけ見られたい方は本題までホイールをギュインとして下さい。

高専

先月11月、私が通う高専では一般にいう文化祭である高専祭が行われました。

初日は雨が降ったり、そのせいであまり人が来なかったり、子どもの台パンで機材が壊れないかハラハラしておりました。 二日目は天候にも恵まれて、公私共々楽しめるものになったのではないかと思います。

さて、この高専祭には伝統的に高専祭期間中だけに活動する高専祭部活というものがあります。 その中に E project (通称:Eプロ)という本校の4学科ある中の電気情報工学科に所属する学生だけで構成される部活があり、私はここに所属しています。 学科にちなんで、回路の体験コーナや、VR、去年度から続けて音ゲーなんてものも作って展示していました。

もちろん、情報とつくからにはインターネットでの展示も必要ということで、数年前からWebページも作り公開していたわけです。 ここでようやく題名がでてきます。

高専時計

公開しているWebページ、これが高専時計です。

ようこそ高専時計へ | Welcame to Kosen Clock

高専時計は、高専祭に向けて準備している学生や学校関係者の姿を写真に収め、それらに時刻を表示するようにして公開するというものです。 少し古いかもしれませんが、美人時計というと想像がつきやすいのではないでしょうか。 上のリンクから見られます。

今年も高専時計をやるとなり、気になっていた私は参加することにしました。 参加するからには新しいことに取り組みたいと考え、こんな提案をしました。

「当日の来校者にも高専度計に参加してもらおう」

これがアホみたいに自分を苦しめることになります。

今まで通り、学生の姿を収めることはもちろん、当日来て頂いた方たちにもスマートフォンから画像をアップロードしていただいて、それも時計にして表示する。 新しい取り組みとしては良かったと思っています。

ですが、突然他の部活に引き抜かれてデザイン担当が消えたり、他の展示の準備を手伝って時間がなくなったり、 そもそも参加人数が考えていたよりも少なく参加者に配る予定だったお菓子が余りまくったり、画像に時刻を埋め込む位置が全然違う場所になって

と言われたり、これらの片鱗はサイトからも解ると思います。 それでも、二日間で合計1500PVと100枚以上の画像をアップロードをして頂き、高専祭当日の雰囲気も感じられるサイトになりました(主観)。

本題

といったことをするために、考えなければいけなったのは環境です。 使用言語には現在もはまりにはまっている Clojure を使用しました。 理由は使いたかったから。

PHP でやればもっと簡単にできるやん。」

と言われたこともありましたし、実際そうすればよかったと作製中何度も思いましたが、結果的に Clojure で書ききりました。 夏休み前に少し書き動くようにし、💪を構成する基本単位達のインターンでこの話をしたのですが、残念ながら落ちてしまいました。 インターン行きたかった…。 次はもう少し力を付けて行きます。

プログラムの全体は大きく3つに分けられます。

  • 各URLの処理管理を行うルーティング部
  • html の生成を行うテンプレート部
  • 時刻を埋め込む画像処理部

テンプレート部とパーサ部はごちゃ混ぜになって routing.clj にあります。 画像処理部は img.clj になります。 使用した主なライブラリは

ルーティング部に duct

GitHub - duct-framework/duct: Server-side application framework for Clojure

テンプレート部にSelmer

GitHub - yogthos/Selmer: A fast, Django inspired template system in Clojure.

画像処理部にOpencCV

OpenCV library

の3つです。

ディレクトリの構成は以下の通りです。duct のテンプレートを使用しました。

.
├── dev
│   ├── resources
│   │   ├── dev.edn
│   │   └── local.edn
│   └── src
│       ├── dev.clj
│       ├── local.clj
│       └── user.clj
├── img
│   ├── local
│   └── show
├── lib
│   ├── Imshow.jar
│   ├── libopencv_java320.dylib
│   ├── libopencv_java320.so
│   └── opencv-320.jar
├── logs
│   └── dev.log
├── Procfile
├── profiles.clj
├── project.clj
├── README.md
├── resources
│   └── kosen_clock
│       ├── config.edn
│       ├── private
│       │   └── img
│       │       ├── fes52ndmarkA.png
│       │       ├── fes52ndmarkB.jpg
│       │       └── fes52ndmarkC.jpg
│       ├── public
│       │   ├── css
│       │   │   ├── bootstrap.css
│       │   │   ├── bootstrap.css.map
│       │   │   ├── bootstrap.min.css
│       │   │   ├── bootstrap.min.css.map
│       │   │   ├── bootstrap-theme.css
│       │   │   ├── bootstrap-theme.css.map
│       │   │   ├── bootstrap-theme.min.css
│       │   │   ├── bootstrap-theme.min.css.map
│       │   │   └── style.css
│       │   ├── fonts
│       │   │   ├── glyphicons-halflings-regular.eot
│       │   │   ├── glyphicons-halflings-regular.svg
│       │   │   ├── glyphicons-halflings-regular.ttf
│       │   │   ├── glyphicons-halflings-regular.woff
│       │   │   └── glyphicons-halflings-regular.woff2
│       │   ├── img
│       │   │   └── no-image.jpg
│       │   ├── js
│       │   │   ├── bootstrap.js
│       │   │   ├── bootstrap.min.js
│       │   │   └── npm.js
│       │   └── test
│       │       └── test.html
│       └── template
│           ├── base.html
│           ├── list.html
│           ├── non-upload.html
│           ├── not-found.html
│           ├── root.html
│           └── upload.html
└── src
    └── kosen_clock
        ├── boundary
        ├── handler
        │   ├── not_found.clj
        │   └── routing.clj
        ├── img.clj
        ├── info.clj
        ├── main.clj
        └── port.clj

以下ではそれぞれのライブラリの使用した内容や気づいたことについて述べます。

ルーティング部

ルーティング部には duct を使用しました。 正確に言えば duct はルーティングライブラリではありません。 duct は Clojure でhttp の処理を行う際に使われる ring 、 compojure を integrant のデータ駆動アーキテクチャ(よくわかってない)という方法を用いて データの定義や管理、integrant-replを用いて repl 駆動開発を行う際に使用するライブラリです。

duct に関して現在日本語で公開されている情報は、全体的に少し古い情報が多いです。 最新のバージョンで使用する際には、公式の readme や Wiki を参考にするほうがよいと思います。

(defmethod ig/init-key :kosen-clock.handler/routing [_ _]
  (context @access-root []
    (wrap-cookies
     (wrap-multipart-params
      (routes
       (GET "/" [] (parser/render-file "template/root.html"
                                       {:access-root @access-root
                                        :global-last-upload-image @global-last-upload-image}))
        (GET "/upload" {cookies :cookies}
             {:cookies {"markA" {:max-age 604800
                                 :value (let [v (:value (cookies "markA"))] (if-let [tmp (some #(when (= v %) %) (into @making-img @root-imgs))]
                                                                              tmp
                                                                              ""))}
                        "markB" {:max-age 604800
                                 :value (let [v (:value (cookies "markB"))] (if-let [tmp (some #(when (= v %) %) (into @making-img @root-imgs))]
                                                                      tmp
                                                                      ""))}
                        "markC" {:max-age 604800
                                 :value (let [v (:value (cookies "markC"))] (if-let [tmp (some #(when (= v %) %) (into @making-img @root-imgs))]
                                                                      tmp
                                                                      ""))}}
              :headers {"Content-Type" "text/html; charset=utf-8"}
              :body (upload-html {:last-markA (let [v (:value (cookies "markA"))] (some #(when (= v %) %) (into @making-img @root-imgs)))
                                  :last-markB (let [v (:value (cookies "markB"))] (some #(when (= v %) %) (into @making-img @root-imgs)))
                                  :last-markC (let [v (:value (cookies "markC"))] (some #(when (= v %) %) (into @making-img @root-imgs)))})})
        (POST "/upload" {params :params cookies :cookies}
              (upload-image (get params :mark) (get params "file") cookies))
        (GET "/list" {params :params}
             (make-list (get params :page)))

        (GET "/festival" [] (res/redirect (str "http://tokei.maizuru-ct.ac.jp") :moved-permanently))
        (context "/festival/" []
                 (GET "*" [] (res/redirect (str "http://tokei.maizuru-ct.ac.jp") :moved-permanently)))

        (context "/img" []
                 (route/files "/" {:root @img-root})
                 (route/resources "/" {:root "kosen_clock/public/img"})
                 (route/not-found {:headers {"Content-Type" "image/jpeg"}
                                   :body no-image}))
        (route/resources "/public/" {:root "kosen_clock/public"})
        (route/resources "/fonts/" {:root "kosen_clock/public/fonts"})
        (route/resources "/test/" {:root "kosen_clock/public/test"})
        (route/resources "/css/" {:root "kosen_clock/public/css"})
        (route/resources "/js/" {:root "kosen_clock/public/js"}))))))

これが実際に使用したルーティング部です。 duct は ring のラッパの側面もあるので、ここはまんま ring です。 最初の defmethod ig/init-key :kosen-clock.handler/routing ig/init-key が integrant に関数の登録を行う部分です。 config.edn で役割を定義しています。 これらを定義することで、コードの変更を行っても直ぐに repl 上から反映することができます。

以下が config.edn の中身です。

{:duct.core/project-ns  kosen-clock
 :duct.core/environment :development

 :duct.module/logging {:environment :development}
 :duct.module/web {}

 :duct.router/cascading
 [#ig/ref :kosen-clock.handler/routing]

 :duct.handler.static/not-found {:response "This page cannot be found"
                                 :headers {"Content-Type" "text/html; charset=utf-8"}
                                 :body #ig/ref :kosen-clock.handler/not-found}

 :kosen-clock.handler/routing {}
 :kosen-clock.handler/not-found {}
 :kosen-clock/port {}
 }

使用するプラグインなどもここで指定します。

使用してみて、変更をすぐに反映したり、複雑な設定が必要な部分を担ってくれて大変ありがたかったです。

テンプレート部

テンプレート部には Selmer を使用しました。 Selmer はテンプレート元となるhtmlデータに処理を行い、コードを返すというシンプルなライブラリです。

例えば、使用するヘッダは同様ということは多いです。その時に、

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8" />
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
</head>
    <body>
    {{body|safe}}
    </body>
</html>

というテンプレートファイルを用意しておき、

(selmer.parser/render-file "template.html" {:body "<p>hello</p>"})

とすると

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8" />
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
</head>
    <body>
    <p>hello</p>
    </body>
</html>

と返ってきます。もちろん<>がそのまま埋め込まれるのは危険なので、通常は &lt &gt に置換されます。 今回であれば、必要であるのでテンプレートにsafe オプションを入れることでそのままタグとして使用しています。

Selmer 以外にも テンプレートライブラリはありますが、個人的に一番シンプルで分かりやすかったので使用しました。

画像処理部

ほとんどjavaです。もちろん Clojure で書いたのですが、画像処理に使用した Opencv が参照を使って中を直接弄りまくるせいで Clojure っぽさがほとんどありません。 java のラッパを使っているのですから(そもそも C++ ですし)当たり前です。

例えば以下を見て下さい。

(dorun (map (fn [r]
    (let [sm (.submat dst r)
           smc (.submat dst-clone r)
           tmp (Mat.)]
       (Imgproc/GaussianBlur smc smc (Size. 31 31) 8 6)
         (.release sm)
         (.release smc)
         (.release tmp)))
       @roiRects))

これは時刻を表示する位置にぼかし処理を行う部分なのですが、なんだよ release って! python のラッパのように戻り値を採用してほしかったです。 このような処理がたーくさんあり、今読み返しても何を書いているのかわからないところが多いです。

使用する言語選択を失敗した悪い例ではないでしょうか。 目的にあった言語を選びましょう。

まとめ

どのライブラリも使いやすく、期限ギリギリではありましたが高専時計を完成させることが出来ました。 html や css の知識が足りず少し物足りない見た目にはなってしまいましたが、 自分の中で、webサービスとしてここまで大きな物を完成されられたことはいい経験となりました。

教訓:人の管理はちゃんとしよう。

追記

最後に高専時計のソースコードは以下にありますので、よろしければご覧ください。

github.com

明日は anolivetree さんの AndroidClojureを使う話 です。 楽しみ。

最近知ったこととしたこと

イカで述べるClojureはversion 1.8.0 についてである。

Clojure について

Clojure の (case example ...)は (condp = example ...)と等価でない。 Enum の比較を行う際に case ではうまく動作しなかったが、condp であれば動いた。 理由がわからないままなので、Clojureメッチャデキル方はぜひとも教えて頂ければ幸いです。

JavaFX について

と書いたくせにまたClojureなんだけど、Clojurejavafx.scene.control.Label を使おうとして

(def label (Label. "Label"))

とかしたらコンパイルできずに怒られもしないし動かない。

Java 8 で Clojure を使う (2): JavaFX - tnoda-clojure

によると、これはLabelだけでなく、control に含まれるクラス全体らしい。 なのでこれを解決する場合する場合には

(def label (promise))

(defn init []
  (deliver label (Label. "Label")))

のようにすると動いたがバットノウハウ感がある。

JavaFX について

 今度はちゃんと JavaFX についてで、Scene には標準で setOnKeyPressed というキーボードが押された際に発火するhandlerを設定できるメソッドがあるが、 これは KeyCode を含まない。これは KeyEvent/KEY_PRESSED にもともと含まれていないことに起因する。 なので Scene 上で KeyCode が欲しかったら addEventFilter で KeyEvent/KEY_TYPED をとってやるハンドラを設定することで解決できる。 「JavaFXはク○」とか浅はかな知識で思ってごめんなさい。

PDF発表ツール

 良いっぽいのがあった

GitHub - pdfpc/pdfpc: A presenter console with multi-monitor support for PDF files.

けど自分の環境では動かなかったため、ClojureでPDF発表のためのツール開発を初めた。

github.com

上のClojureJavaFXを触っているのはこれを作るため。 現状はPDFの表示とページの移動まで実装できた。 もし気になったりすることがあったら使ってみて下さい。