Hello JOCL
先日Scala2.9.0RC1が出ましたね。IDEも良くなってきて、IDEに頼って生きてきた身としては有り難い限りです。
さて、こちらも少し前の話ですが、JogAmpのJOGL/JOCL/JOALの2.0RCが出ています。
JOGLは少し触ってたのですが、他は見てなかったのでこれを機に触ってみます。まずJOCL。
JOCLとは
"Java binding for the OpenCL"ということで、OpenCLをJavaから使うためのライブラリです。
OpenCL(オープンシーエル、Open Computing Language)は、OpenCL C言語による、マルチコアCPUやGPU、Cellプロセッサ、DSPなどによる異種混在の計算資源を利用した並列コンピューティングのためのフレームワークである。用途には高性能計算サーバやパーソナルコンピュータのシステムのほか、携帯機器などでの利用も想定されており、組み込みシステム向けに必要条件を下げたOpenCL Embedded Profileが存在する。
OpenCL - Wikipedia
もちろんScalaからも使えます。
準備
JogAmpのHow to build JOCLを見てビルドするか、ビルド済みのものが置いてあるのでそれを持ってきましょう。
必要なファイルを以下のような感じでプロジェクトに配置します。
Hello JOCL
では、JogampWikiのJOCLチュートリアルを元にしてScalaで書いてみます。
配列A,B,Cがあって、C(n) = A(n) + B(n) を並列に実行するというものです。
実行結果
上記のJOCLを利用したものと、Scala2.9のParallel Collectionで同様の処理を行ったもの、ベタに線形処理したものをペタリ。
その1
[info] created CLContext [id: 4493152704, platform: Apple, profile: FULL_PROFILE, devices: 1] [info] using CLDevice [id: 16909312 name: Intel(R) Core(TM) i7-2620M CPU @ 2.70GHz type: CPU profile: FULL_PROFILE] [info] used device memory: 16MiB [info] localWorkSize: 1, globalWorkSize: 1444477 [info] a+b=c results snapshot: [info] 116.87144, 96.54628, 165.53079, 148.23895, 147.75995, 30.946493, 95.10199, 100.24969, 92.733475, 55.860107, ...; 1444467 more [info] computation took 1: 199454 micro sec [info] computation took 2: 5878 micro sec [info] computation took 3: 5936 micro sec [info] computation took 4: 5919 micro sec [info] computation took 5: 5926 micro sec [info] computation took 6: 6737 micro sec [info] computation took 7: 5873 micro sec [info] computation took 8: 6457 micro sec [info] computation took 9: 5999 micro sec [info] computation took 10: 5835 micro sec [info] Parallel - availableProcessors: 4 [info] a+b=c results snapshot: [info] 116.87144, 96.54628, 165.53079, 148.23895, 147.75995, 30.946493, 95.10199, 100.24969, 92.733475, 55.860107, ...; 1444467 more [info] computation took: 137413 micro sec [info] computation took: 10175 micro sec [info] computation took: 10248 micro sec [info] computation took: 10013 micro sec [info] computation took: 8839 micro sec [info] computation took: 10014 micro sec [info] computation took: 9133 micro sec [info] computation took: 8972 micro sec [info] computation took: 8906 micro sec [info] computation took: 8920 micro sec [info] Linear [info] a+b=c results snapshot: [info] 116.87144, 96.54628, 165.53079, 148.23895, 147.75995, 30.946493, 95.10199, 100.24969, 92.733475, 55.860107, ...; 1444467 more [info] computation took: 78716 micro sec [info] computation took: 22134 micro sec [info] computation took: 14659 micro sec [info] computation took: 14648 micro sec [info] computation took: 14767 micro sec [info] computation took: 14769 micro sec [info] computation took: 14710 micro sec [info] computation took: 14743 micro sec [info] computation took: 14723 micro sec [info] computation took: 14905 micro sec
その2
[info] created CLContext [id: 107566480, platform: NVIDIA CUDA, profile: FULL_PROFILE, devices: 1] [info] using CLDevice [id: 107566400 name: GeForce GTX 470 type: GPU profile: FULL_PROFILE] [info] used device memory: 16MiB [info] localWorkSize: 256, globalWorkSize: 1444608 [info] a+b=c results snapshot: [info] 116.87144, 96.54628, 165.53079, 148.23895, 147.75995, 30.946493, 95.10199, 100.24969, 92.733475, 55.860107, ...; 1444598 more [info] computation took 1: 230009 micro sec [info] computation took 2: 6853 micro sec [info] computation took 3: 6605 micro sec [info] computation took 4: 6697 micro sec [info] computation took 5: 6668 micro sec [info] computation took 6: 6846 micro sec [info] computation took 7: 6490 micro sec [info] computation took 8: 5926 micro sec [info] computation took 9: 5903 micro sec [info] computation took 10: 5843 micro sec [info] Parallel - availableProcessors: 8 [info] a+b=c results snapshot: [info] 116.87144, 96.54628, 165.53079, 148.23895, 147.75995, 30.946493, 95.10199, 100.24969, 92.733475, 55.860107, ...; 1444467 more [info] computation took: 113953 micro sec [info] computation took: 5854 micro sec [info] computation took: 5863 micro sec [info] computation took: 6205 micro sec [info] computation took: 6049 micro sec [info] computation took: 6591 micro sec [info] computation took: 6518 micro sec [info] computation took: 6511 micro sec [info] computation took: 7761 micro sec [info] computation took: 6342 micro sec [info] Linear [info] a+b=c results snapshot: [info] 116.87144, 96.54628, 165.53079, 148.23895, 147.75995, 30.946493, 95.10199, 100.24969, 92.733475, 55.860107, ...; 1444467 more [info] computation took: 60240 micro sec [info] computation took: 18839 micro sec [info] computation took: 19091 micro sec [info] computation took: 19181 micro sec [info] computation took: 18770 micro sec [info] computation took: 18772 micro sec [info] computation took: 18889 micro sec [info] computation took: 18821 micro sec [info] computation took: 19142 micro sec [info] computation took: 20197 micro sec
JOGLとの連携
OpenGL/OpenCLは互いのメモリにアクセスできますが、JOGL/JOCLでも相互にやりとりできるようです。
次はこれをやってみます。
Enumeration#ValueをキーにしたMapを格納する
Map[String, T]を格納するMongoMapFieldとか、Enumeration#Valueを文字列として格納するEnumNameFieldはあるのに、Map[Enumeration#Value, T]を格納するものがなかったので作ってみた。
といってもMongoMapFieldをコピペして少し変えただけ。
利用しているnet.liftweb.mongodb.Metaがprivate[mongodb]で外から参照できないので、Liftのパッケージの中に入れてあります。
setFromAnyの中のMapの型指定は型消去されるから意味ないよって警告されるけど、どうしたものか分からないのでとりあえず無視。
利用例
出力
class com.example.model.RoboParts={name=test, partsType=BD, joints=Map(HD -> 1, AM -> 2, LG -> 1, BS -> 1), _id=4d8afed910c8ac8d2508b90f} 2
MongoDBへのアクセスにはRogueを使ってます。
Parallel Collectionのベンチマークやってみました
Scala2.9で導入されるParallel Collectionについてベンチマークがなされており、Scala2.9を触ったことなかったのでこれに乗じてみようかと。
scala2.9のparallel collection の benchmark をしてみた - scalaとか・・・
Parallel Collectionのベンチマークやってみた - k4200’s notes and thoughts
準備
Scalaのバージョンを簡単に切り替えられる"svm"ってヤツ作った (冷やし中華終わりました) - ゆるよろ・オブ・ザ・( ;゚皿゚)ノシΣ フィンギィィーーッ!!! 日記
Scalaのインストールに@yuroyoroさんのsvmを利用してます。
svm update-latest
これでnightly-buildをダウンロードしてくれます。DL後にcurrentを切り替えるか聞かれるのでyesで。
> scala -version Scala code runner version 2.9.0.r24471-b20110317020039 -- Copyright 2002-2011, LAMP/EPFL
2.9入りました。
JAVA_OPTSは例に習って以下を設定
export JAVA_OPTS='-Xmx4G -Xms1G'
PCスペック
つい最近新調しました。初MacですがMac使いやすいですね。
このPCの性能を見てみたかったとかそういうこともあります。
ソース
元のものをobjectに突っ込んでコンパイルできるようにしただけです。
object ParBench { def benchmark(times: Int)(f: =>Any) = new scala.testing.Benchmark{ def run = f }.runBenchmark(times) implicit def benchmark2report(list:List[Long]) = new { val average = (list.sum - list.max - list.min).toDouble / (list.size-2) } def test(listSize:Int){ println("--------------" + listSize + "---------------") val normal = 1L to listSize toList //普通のListつくる val parSeq = normal toParSeq //要素が同じparalell版をつくる val normalResult = benchmark(5)( normal.reduceLeft(_ + _) ) //reduceというのが、paralellなcollectionにしかないメソッドで、 //内部でスレッド生成し分割して処理してるらしい val parSeqResult = benchmark(5)( parSeq.reduce(_ + _) ) println( normalResult , parSeqResult , parSeqResult.average / normalResult.average ) } def main(args: Array[String]) { println("availableProcessors: " + scala.collection.parallel.availableProcessors) 2000000 to 20000000 by 2000000 foreach test } }
これをファイルに保存してscalac。
さっそく実行
ファンがガンガン回ってPC酷使してる感じがしますね。
availableProcessors: 4 --------------2000000--------------- (List(39, 24, 13, 13, 14),List(42, 12, 12, 12, 12),0.7058823529411765) --------------4000000--------------- (List(72, 28, 29, 29, 28),List(25, 25, 26, 24, 27),0.883720930232558) --------------6000000--------------- (List(132, 43, 43, 44, 44),List(41, 39, 41, 40, 40),0.9236641221374047) --------------8000000--------------- (List(165, 57, 58, 57, 57),List(52, 52, 51, 51, 52),0.9011627906976744) --------------10000000--------------- (List(154, 80, 85, 84, 83),List(70, 71, 70, 70, 70),0.8333333333333334) --------------12000000--------------- (List(183, 97, 99, 103, 102),List(85, 88, 87, 85, 86),0.8486842105263158) --------------14000000--------------- (List(212, 120, 118, 118, 118),List(100, 99, 98, 100, 100),0.8398876404494382) --------------16000000--------------- (List(252, 140, 140, 140, 138),List(114, 117, 118, 117, 116),0.8333333333333334) --------------18000000--------------- (List(181, 158, 158, 158, 158),List(135, 133, 137, 135, 132),0.8502109704641351) --------------20000000--------------- (List(195, 173, 176, 171, 174),List(149, 144, 146, 150, 148),0.8470363288718928)
あれ?
0.8台半ばで期待したほど早くならない。
REPLで実行
元と違うのはscalacでコンパイルして実行しているとこ、なのでREPLで実行してみる。
Welcome to Scala version 2.9.0.r24471-b20110317020039 (Java HotSpot(TM) 64-Bit Server VM, Java 1.6.0_24). Type in expressions to have them evaluated. Type :help for more information. scala> :load ParBench.scala Loading ParBench.scala... defined module ParBench availableProcessors: 4 --------------2000000--------------- (List(29, 23, 23, 23, 23),List(42, 11, 11, 10, 10),0.46376811594202894) --------------4000000--------------- (List(101, 47, 47, 47, 49),List(28, 26, 26, 27, 27),0.5594405594405595) --------------6000000--------------- (List(175, 74, 72, 73, 72),List(40, 41, 42, 41, 40),0.5570776255707762) --------------8000000--------------- (List(208, 96, 96, 96, 96),List(53, 54, 54, 52, 53),0.5555555555555556) --------------10000000--------------- (List(221, 146, 142, 145, 142),List(73, 70, 72, 71, 70),0.4919168591224018) --------------12000000--------------- (List(272, 176, 172, 178, 174),List(87, 89, 86, 88, 86),0.4943181818181818) --------------14000000--------------- (List(299, 200, 204, 204, 194),List(102, 100, 100, 101, 100),0.49506578947368424) --------------16000000--------------- (List(367, 245, 235, 231, 243),List(118, 119, 117, 119, 114),0.4896265560165975) --------------18000000--------------- (List(309, 268, 273, 271, 271),List(136, 135, 138, 136, 134),0.4993865030674846) --------------20000000--------------- (List(336, 306, 305, 304, 313),List(149, 149, 149, 150, 149),0.4837662337662338)
0.5前後で倍率はよくなってるけどこれは……
並べると
--------------20000000--------------- (List(195, 173, 176, 171, 174),List(149, 144, 146, 150, 148),0.8470363288718928) // コンパイル (List(336, 306, 305, 304, 313),List(149, 149, 149, 150, 149),0.4837662337662338) // REPL
parallelの速度は変わってないですが、normalはコンパイルするとかなり速度が向上してる。
これはJITコンパイラが頑張ってくれてるということかな。
Server VM
そういえばREPLはServer VMで起動してますね。
という訳でJAVA_OPTSに -server を追加してみて再度実行。
export JAVA_OPTS='-server -Xmx4G -Xms1G'
availableProcessors: 4 --------------2000000--------------- (List(39, 13, 12, 13, 14),List(39, 10, 10, 10, 9),0.75) --------------4000000--------------- (List(453, 30, 42, 30, 30),List(22, 21, 22, 22, 22),0.6470588235294118) --------------6000000--------------- (List(93, 41, 41, 41, 41),List(29, 28, 29, 28, 29),0.6991869918699187) --------------8000000--------------- (List(2082, 60, 61, 61, 59),List(42, 42, 42, 42, 42),0.6923076923076923) --------------10000000--------------- (List(2632, 74, 75, 74, 74),List(52, 52, 52, 51, 51),0.695067264573991) --------------12000000--------------- (List(147, 84, 85, 85, 83),List(58, 56, 58, 57, 57),0.6771653543307087) --------------14000000--------------- (List(148, 100, 98, 99, 100),List(67, 67, 67, 67, 66),0.6722408026755853) --------------16000000--------------- (List(3940, 121, 125, 122, 123),List(82, 81, 82, 82, 83),0.6648648648648648) --------------18000000--------------- (List(4628, 138, 139, 137, 138),List(95, 95, 95, 95, 95),0.6867469879518072) --------------20000000--------------- (List(3829, 152, 151, 149, 150),List(123, 124, 119, 119, 119),0.7969094922737306)
--------------20000000--------------- (List(195, 173, 176, 171, 174),List(149, 144, 146, 150, 148),0.8470363288718928) // コンパイル (List(336, 306, 305, 304, 313),List(149, 149, 149, 150, 149),0.4837662337662338) // REPL (List(3829, 152, 151, 149, 150),List(123, 124, 119, 119, 119),0.7969094922737306) // コンパイル(Server VM)
更に高速化しました。
normalの最初で極端に遅くなるのはGCっぽいかな?-Xmsは-Xmxと同じにした方がいいかも。REPLではここまでひどい数値は出ないので他に何かあるんだと思いますが。
sbt起動スクリプトの引数
日本語出力したら文字化けしてたので -Dfile.encoding=utf-8 を追加。
他のオプションは、LiftのWiki*1で頻繁なOutOfMemoryErrorを回避するために付けようってあるけど、これ書いてもjetty起動しっぱなしにして色々してると結構落ちるよね。
#!/bin/sh java -Dfile.encoding=utf-8 -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -jar `dirname $0`/sbt-launch.jar "$@"
メモリは余裕あるし最大サイズはもっと取っておくべきか。
Scalaで再帰的な構造のXMLを生成する
もう誰かやっている気がするけども。
元ネタ
GroovyのMarkupBuilderで再起的な構造のXMLを生成する - No Programming, No Life
GroovyでKey、ValueをXMLにクールに出力する-keyValueXml.groovy- - Togetter
C# で Key、Value な コレクションを XML にしてみた - お だ のスペース
ポリモーフィズム版
本体
テスト
Scalaで書くからには型安全にしたく、Valueクラスをみんな継承してやってます。
あと属性を指定できるようにしました。
Elemのコンストラクタさんは第一引数(prefix)に空文字列を渡すとnullにしろ(nullかよ)とか、第四引数(scope)にnullを渡すとTopScopeにしろとか言ってくれるのは有り難いんだけど、第三引数(attributes)にnull渡しても動作する癖にPrettyPrinterでformatしようとするとぬるぽが出てしばらく詰まった。
scala.xml.Nullを渡さないといけないんですね、最初からソース読めよってことでした。
その渡すものを作ってる部分、
def meta = ((Null: MetaData) /: attributes){ (meta, attr) => new UnprefixedAttribute(attr._1, attr._2, meta) }
みんな大好きfoldLeftさん、別名/:。初期値を(Null: MetaData)として型を指定していますが、これは Null /: attributes だと戻り値の型がscala.xml.NullってことになりUnprefixedAttributeはダメって怒られるからです。共通の親であるMetaDataなら安心。
以下は書いててちょっと不満に思った点なんですが、
case classのサブなコンストラクタを使うときにnewって書かなきゃいけないのが面倒。どうにかなんないのかなこれ。ほんとは TupleN => KeyValue なimplicitを書きたいところだったんだけど、暗黙型変換は1回しかやらないってルールなのでそれはしょうがないにしても。
あと、XMLリテラルが間の改行とかインデントまで考慮してくれちゃうのはありがた迷惑な気が致します。StringのstripMarginみたいなノリで空白文字だけのテキストを無視したXMLを返してくれるモノはないのかしらん。
PrettyPrinterが正にそれをやってるじゃないかと思ったところ、
case Text(s) if s.trim == "" => ;
これで無視してStringBuilderに渡してないってだけですね。でも同じようにして出来るのかな。
パターンマッチング版
本体
テストはポリモーフィズム版とほぼ一緒なので省略。
どっちのやり方がいいかって議論は何かで読んだ気がするけど何だっけ。個人的に好きなのはポリモーフィズム版なんだけど、こちらの方がよりScalaらしいのかな。
親に操作を追加すると子をすべて編集しなきゃいけない vs データの種類を増やすと操作をすべて編集しなきゃいけない(けど全部一箇所にまとまってるよね)、でケースバイケースという話だったかな。
create_projectを改造してみた
(この記事は Scala Advent Calendar jp 2010 : ATND の7日目です。)
sbt-android-pluginのcreate_projectスクリプトを改造してみました。
こちら → https://gist.github.com/738232
sbt-idea を利用してIntelliJ IDEAで開発できるようにします。
使い方
プロジェクト名とpackage
$ create_project HelloProject com.example.hello
これは元のcreate_projectとは挙動が違って、sbtプロジェクトがないところでsbtコマンド打った感じになります。
(オプション指定はスクリプトいじってください)
Scalaバージョン指定
$ create_project HelloProject com.example.hello --scala-version 2.8.1
デフォルトで実行環境のバージョンになる、かな?
Android用プロジェクト
$ create_project HelloProject com.example.hello --android
デフォルトの設定でAndroidプロジェクトを作成します。
元のcreate_projectで名前とpackageだけ指定したのと大体同じ。
Android / APIレベル、platform、Activity
$ create_project HelloProject com.example.hello --api-level 8 --platform android-8 --activity HelloActivity
元と同じ。この辺指定する場合は --android つけなくていいです。
--api-level は --platform を上書きするので注意です。(例の場合、--platform android-8 は不要)
sbt-ideaを利用
$ create_project HelloProject com.example.hello --idea
sbt-ideaを利用するための設定を行います。
Plugins.scalaに書き足したり、Project.scalaでIdeaProjectミックスインしたり、idea.properties作成したり。
idea.propertiesの設定
--project-jdk-name <name> --project-output-path <path> --java-language-level <level> --exclude-sbt-project-definition-module --exclude-libmanaged-folders --compile-with-idea
この辺指定する場合は --idea つけなくていいです。
--exclude-sbt-project-definition-module は include.sbt.project.definition.module=false にします。
それぞれの項目についてはsbt-ideaのREADMEを参照してください。
(--project-output-pathはちゃんと動かないかも。idea.propertiesだけじゃなくProject.scalaも変えなきゃいけないと思うけど時間足りなかった)
.gitignore生成
$ create_project HelloProject com.example.hello --gitignore
プロジェクトのルートに .gitignoreファイルを作ります。
内容変えたい場合はスクリプトいじってください。
要するに
$ create_project HelloProject com.example.hello --android --idea --gitignore $ cd HelloProject $ sbt update $ sbt idea
これでIDEAで開けるAndroidプロジェクトを作れるよー。ついでに.gitignoreも作っときます。
というスクリプト。
注意
Cygwin用だよ
#!/bin/sh exec scala -deprecation `cygpath -m $0` "$@" !#
Cygwin用に書き換えてあります。そんなもん使わねーよって人は戻してください。
Scala2.8↑用だよ
c.copy(project = c.project.copy(scalaVersion = i.next()))
こういうコードがあるのでスクリプト実行する環境は2.8じゃないといけません多分。
sbt0.7.4用だよ
中身書き換えたら大丈夫かもしれないけど、0.7.5は試してません。
AndroidSDKは最新(2010/12/13現在)のだよ
def androidPlatformToolsPath = androidSdkPath / "platform-tools" override def adbPath = androidPlatformToolsPath / adbName
最新のSDKでadb.exeの位置が変わってるので、↑を定義してあります。これ消せば最新じゃなくても動くはず。
でもandroid-9には対応してないよ
すみません。
IntelliJ IDEA 9 用だよ
Android使う場合。10だとうまくいかなかった。10でAndroidサポート入ってるので干渉してるかもしれない。
Android使わなければ10で大丈夫。
スクリプトの中身の話
重要そうなところだけ。
中身は大きく分けてコマンド引数を解釈して設定を作成する部分と、設定を元に必要なファイル郡を生成する部分の2つに分かれます。
config
def config(i: Iterator[String]) = { @annotation.tailrec def cfg(i: Iterator[String], c: Config = null): Option[Config] = //... cfg(i) }
コマンド引数を解釈する再帰関数。元のスクリプトは引数をループで回してvarを書き換えるという方法でやっていたのでScalaらしく?してみました。
cが状態変数ですね。@tailrec便利です。(参照: Scala Advent Calendar 2010 [Day 3]再帰再入門)
呼び出しごとにIteratorを1つ(or 2つ)進めて、解釈したものをcase classのcopyメソッドを使って状態変数に反映、次が無くなるまで再帰呼び出しします。
成功したらcをSomeに包んで、失敗したらNoneを返します。
val name = i.next() if ((name matches """[a-zA-Z]\w+""" tap { b => if(!b) println("invalid project name: "+name) }) && i.hasNext){ val pkg = i.next() if (pkg matches """[a-z]+\.([a-z]+\.?)+[a-z]$""") cfg(i, Config(ProjectConfig(name, name, pkg))) else None tap { _ => println("invalid package name, need 2 package components (e.g. com.bar)") } } else None
ごちゃってますが途中でエラーメッセージを出すために tap というのも使ってみました。
(参照: Scala Advent Calendar 2010 [Day 4]どうしても副作用があるコードを書かないといけない場合に使うと便利かもしれないもの)
def setAndroid(c: Config)(f: AndroidConfig => AndroidConfig):Config = c.copy(android = Some(f(c.android getOrElse AndroidConfig()))) //... case "--platform" => if (!i.hasNext) None else cfg(i, setAndroid(c) { _.copy(platform = i.next()) })
このような具合で、copyメソッドは一部だけ内容を変えたオブジェクトを簡単に作れるので便利です。
generate
設定を元にファイル郡を生成する関数。時間が足りなくて元からあんまり変わってないです。
Androidプロジェクトじゃなくても使えるようにしたいので、Project.scalaで継承・ミックスインするものを設定によって変えるようにしてあります。
val mainMix = { val buf = new ListBuffer[String] buf += (if (withAndroid) "AndroidProject(info)" else "DefaultProject(info)") if (withAndroid) { buf += "Defaults" buf += "MarketPublish" buf += "TypedResources" } if (withIdea) buf += "IdeaProject" buf.toList } mkString ("extends ", " with ", "")
非常に手続き的ですが……
ListBufferで必要なもののリストを作って、mkStringで連結します。
"extends AndroidProject(info) with Defaults with MarketPublish with TypedResources with IdeaProject"
こんな文字列ができます。
TypedResourcesについてはこちらの記事が詳しいです。
→ sbt-android-pluginで型安全にリソース取得 - papamitra
object CreateProject
sbt-android-plugin | ブログ.武田ソフト.jp こちらの記事を参考にして、全体をobjectで包んでいます。
object定義の前と後を削除してコンパイルしたらscalaコマンドから使えるんじゃないかな。
おわりに
こんな記事を最後まで読むほどScalaに興味がある人は Scala Advent Calendar jp 2010 : ATND に参加するといいと思います。
現時点であと2人空いてるようですよー。