C#とCGアプリの相性について

etopirika5 さんが下記のように書かれているので、http://www.quatouch.com/products/hisui/index.html を作っている者として、ちょっと反応してみるよ。

C#は業務用の小さなアプリケーションをちょこっと作るには最適だと思うのですが,一年間かなり使ってみた結果CGアプリや数値計算アプリを作るには全く適していないことがはっきり分かりました.

http://d.hatena.ne.jp/etopirika5/20100612/1276299740

僕としては「全く適していない」とは思ってないのだけれど、でも etopirika5 さんがそう思われるのもよく理解できる。

氏は研究者。研究者というのはとても厳しい職業だなあと以前から畏敬の念をもって見上げているのだけれど、彼らは「世界一じゃないと存在意義が示せない」人達なのだ。たとえ狭い分野であろうとも、その分野では一番の結果を出さないと論文が書けない。これはホント僕には無理。論文を読むといつも「すごいなー、こんなアルゴリズム僕には絶対思いつかないなー」と感心することしかできない。その論文の弱点を見つけてそれを上回るアルゴリズムを研究しようなんていう能力は到底僕にはない。そういう厳しい世界で etopirika5 さんは戦っているのだろうと思う。

そういう研究者が使うべき言語は、そりゃあ C# じゃないでしょう、C/C++でしょう、と思う。数値計算で論文書くなら計算時間などの実測値を掲載する必要があるだろうし、特にCGならば論文発表の際にインパクトのあるデモも必要になるだろうと思う。そういう戦いの場でC#使った眠たいデモはアウトなんだろうと思う。

つまり、CGとか数値計算の分野で真正面からの横綱相撲が必要とされている方はやっぱりC/C++。そこに異論は全くない。だけれど僕は、はなっから横綱は狙ってない感じ。一昔前の舞の海みたいな相撲を狙っているといえばいいのかな。小兵には小兵なりの戦い方があるさ、という感じ。

僕が狙っているビジネスの一つは例えば「業務用の小さな *CG* アプリケーションをちょこっと作る」というニーズ。あるいはトップレベルのパフォーマンスがそこまで要求されないアプリ。こういうニーズにはC#はとても適していると思う。

C#OpenGL

やっぱり生の OpenGL API を直接ガンガン叩こうと思ったら、C# からはやりにくい。ラッパーを丁寧に作りこんで使い易い環境を作り上げていく必要があるだろう。
拙作の ヒスイ では、Hisui.OpenGL.dll というDLLがあって、コイツがOpenGLの薄いラッパーの役割を果たしている。また、Hisui.Geom.dll という幾何ベクトルなどのライブラリもあり、これら2つが連動するように作ってある。このおかげで OpenGL の呼び出しはとても快適になった。まー、ここまでライブラリを育てるのはかなり時間がかかったけれど。

C#C++ の接続が難しい

これは同意!
C# と C の接続は別に難しくない。でも C#C++ はとても面倒だ。C++ を呼び出そうとしたら C++/CLI でラップしないといけなくなって、やり始めるとあらゆるクラスを全部ラップしないといけなくなって、delete のタイミングとか考え始めると気が狂いそうになったりする。
どちらかというと、C++ を C 形式の API でくるんで、それを C# から呼び出す方がいいんじゃないか、と僕は思っている。そういう時は、オブジェクト指向脳は意図的にOFFにしてC言語脳に切り替えてAPIを設計する必要あり。
あと、C# から C/C++ を呼び出すときのパフォーマンスってどーなんだろ?やっぱり .NET 内のメソッド呼び出しよりコストがかかるんじゃないかしらん?あまり頻繁にネイティブコードの呼び出しが発生するのはパフォーマンスに悪影響しそうな気がする。(この辺は知識ないので単なる予想です、すいません)

追記

調べてみたら、DllImport する際は SuppressUnmanagedCodeSecurity 属性も付ければ速くなるらしい。セキュリティチェックが省略されるので呼び出し先が信頼できる場合しか使えないようだけど。し、知らなかった・・・。知識不足露呈(恥。

C# は遅いか

分かりません>< 知りません>< ごめんなさい m(_ _)m

ただ使っている身としては、そんなに使いものにならないほど遅いという感覚は持っていない。かなーり昔に Java の実行速度は C/C++ と比べてもそんなに遜色ないというベンチマークをみた記憶もあって、それならば C# だって結構イケルだろうと予想して僕は C# を選択した。もし仮に今は遅かったとしても、将来的には同等な速度までJITコンパイラが進化するだろう、と。

まあ、メモリは・・・、喰ってると思いますが・・・。

はっきりとC/C++と違いを感じるのは以下の部分。

  • 配列へのアクセスは圧倒的に遅い! → unsafe コード使わないと話にならん!逆にいうと、unsafe 使えば結構イケる。
  • C++STL >>> C#(.NET Framework) の System.Collections.GenericsSTL が恋しい・・・。

あと、C# は GetHashCode() の実装でアホをやらかすとメッチャ遅くなるので注意。一度ハマった・・・orz。

追記

Java のパフォーマンスがどこまで参考になるのか分からないけど・・・。

  • 32/64 ビットの数値演算 ファイル入出力 例外処理 では C と同等の性能を示す。
  • コレクション、オブジェクトの生成、解放、メソッド呼び出しの性能 では、Java の方が C++ より性能が高い。
  • 配列の操作は C の方が高速である。
  • 三角関数 の性能は C の方が高い[41]。
Javaの性能 - Wikipedia

C# も動作原理は似ているはずなので似たような傾向になるのではないかしら。コレクション系がJavaの方がC++より高速というのは意外だったけど。C++STL はクールと思ってたけど、必ずしも速いわけではない?

メモリアロケータ

これがパフォーマンスに影響しているかどうかはハッキリと分からないけれど、僕はアロケータのようなものを自作して工夫しているのでちょっと紹介してみよう。これは C/C++ でも使えるテクニックで、結構有名なものだと思う。

ヒスイ には Hisui.Core.Storage というコンテナクラスが定義してある(ここにちょっと説明あり)。簡単に言うと int 型の整数をキーとして値を格納出来るコレクションクラスだ。この中で簡単なアロケータを実装している。

CGアプリでは例えば次のような座標値を大量に扱う。これは絶対 struct でしょう。絶対 class にしてはいけない。

public struct Point3d
{
  public double X, Y, Z;
}

これを Hisui.Core.Storage に対して次のように格納できる。

var points = new Hisui.Core.Storage<Point3d>();
int id = points.Put( new Point3d { X = 1, Y = 2, Z = 3 } );  // (1, 2, 3)が格納されて、空いている ID が割り当てられる
var p = points[id];  // id をキーとして (1, 2, 3) が取得出来る
points.Remove( id ); // id をキーとして (1, 2, 3) が削除される

ここで重要なのは、Put() も Remove() も points[id] も O(1) の計算量で実現出来ていること。通常、配列を使ったら Remove() は O(n) の計算量が必要となってしまうのだけれど、Storage ではそれが必要ない。
このカラクリを説明するために、簡単のために Storage には最大で 256 個までしか要素を格納出来ないことにしよう。その場合 Storage は、まずいきなり 256 個分の連続したメモリ領域を確保してしまう。その 256 個分の領域のうちどこが使われていてどこが空いているのかを Storage が自分で管理するのだ。Put() 関数がキーとして返す ID は 0 〜 255 のインデックス値(実はヒスイでは 0 は null 扱いなので 1 〜 256 までの値)となる。
その最初に確保される 256 個分のメモリ領域は、ただの何も書かれていない空の配列ではない。次の図のようなチェイン構造が埋め込まれている。

このチェインは空き領域をリスト状につないだ空き領域チェインだ。図中の「空き位置」というのはこの空き領域チェインの先頭を指すポインタだ。
Put() が呼ばれると、「空き位置」ポインタが指している先に値が格納され、そのインデックス値がIDとして返る。このとき「空き位置」ポインタはチェインを辿ったもう一つ先の領域を指すように更新される。
Remove() が呼ばれると、id が指す領域が空き領域チェインに挿入され、「空き位置」ポインタはたった今 Remove された領域を指すように更新される。
つまりなんのことはない、連続したメモリ領域上にリンクリスト構造を展開して、配列とリンクリストのいいとこ取りをしたような設計といえる。これはたぶん古典的で有名な方法なので、絶対ネット上にもっと丁寧な解説があると思うのだけれど、すぐには見つからなかった。なんかこの手法には名前がついているのかな?(追記:「メモリプール」ではないかとコメ欄でご指摘頂きました)

まあとにかく、僕はこんな実装をやっている。ベンチマークをしたわけじゃないのでこの実装がパフォーマンスに寄与しているのかどうか分からないし、計測しないでパフォーマンスに言及するのはとても危険なことなんだけど、もしかしたら、本当にもしかしたらだけど、こういった工夫がそれなりの効果を上げているおかげで僕は C# でもなんとか CG アプリが組めているのかもしれない。