Grani Engineering Blog

株式会社グラニはC#を中心として、ASP.NET、Unity、VR開発を行っています。

グラニ x カヤック合同勉強会レポート。ネイティブ開発をテーマに gRPC, Unity, アセット管理, GitLFS について

CTOの河合です。

2/24(金)に面白法人カヤックさんと合同で、弊社の休憩/セミナースペースにて勉強会を開催しました!

カヤックさんには以前にもお越しいただいて、その時はVRがテーマだったので、今回はネイティブアプリケーション開発をテーマに、特に弊社では最近gRPCが熱いのでgRPCを切に希望しつつ、お悩みどころでもあるアセット管理などについて、お互いの知見を共有し合いました。

Golang x gRPC の話

カヤック矢吹さんより、GolangとgRPCの話をしていただきました。詳細はカヤックさんのレポート 【旅する勉強会】Grani & カヤックで合同勉強会を開催しました! へ。

gRPCの採用を検討し最終的には……!理由は非常に納得の行くもので、しっかりした検証と、しかしより良い形を目指して現実的な採用を進めるところは、さすがだと思いました。

NextGen Server/Client Architecture - gRPC + Unity + C#

カヤックさんの話を受けて、今度はgRPCを採用した、という話です。最もネックになるUnityで使えないじゃん!というところを自分で移植し、見えてる地雷を踏みまくりながら突き進んでいるのは自覚しつつも、C#企業であるというアイデンティティーとしても、決死の覚悟で成功事例を作っていきたいと思っています。

紹介しているOSSライブラリ群は以下のものです。

まだ開発中で正式アナウンスまでは至っていませんが、安定度を高めて近日中に公開していきたいところです。

アセットパイプライン、ビルドパイプラインの話

カヤック清水さんより、Unityのビルド周りの話をしていただきました。こちらも詳細はカヤックさんのレポート 【旅する勉強会】Grani & カヤックで合同勉強会を開催しました! へ。

GitHubを中心に、非常に作り込まれていて、羨ましいー、とかなり思ったり。弊社でも同様のビルド/配信システムは存在しているものの、ここまで柔軟に連携取れた動きはできていないので、参考にさせて頂きます!

Git LFS移行のお話

GitLFS移行のお話 / migrate to git lfs // Speaker Deck from Ryuichi Saito

最後は弊社インフラエンジニアの@sri_1128からGit LFSに関しての話です。

やむにやまれぬ事情からスタートし、すったもんだで今も大変といえば大変ですが、Git LFSの本格利用を開始しています。 特に移行時や運用面時の辛いポイントに関して、GitLFSを検討している方にぜひ読んでいただければと思います。

曰く、Git LFSはこれから良くなっていく!とのことなので期待して今後のバージョンアップを待ちましょう!

まとめ

懇親会では、グラニ社内バーコーナーにてお酒を片手に、色々と語り合いました。

グラニでも、今後も他社さんとの合同勉強会を積極的に開催していきたいと考えていますので(こちらからもお邪魔したいです!)、C#、Windowsインフラ、Unity、UniRx などなどご興味ありましたら @neuecc までお声掛けください。是非、一緒に高め合いましょう。

C#のswitch文のコンパイラ最適化について

CTOの河合(@neuecc)です。グラニもエンジニアブログはじめました!グラニの中心的テクノロジーであるC#関連は元より、Unity関連やUniRx、最近力を入れているVR関連についての情報を色々と発信していけたらと思っています。私自身は、思いたったときが書き時ということで、私が社内にふっと流したくなったものを、外向けに発信していこうかな、と思っています。

さて、今回のテーマはswitch文のコンパイル結果について。C#は割と素直なコンパイル結果(ILへの変換)を得られるのですが、一部のものはアグレッシブな変換を行います。有名所ではラムダ式のクロージャなどは、隠れたクラスを作ってくれる、作ってしまうなど、コードの見た目からイメージした通りではない結果を出力しますが、実はswitch文もかなりアグレッシブな変換を行います。そこで、今回はそうしたswitchの最適化を見ていきましょう。

なお、最適化はコンパイラの実装によって変わります。Unity(monoの古いバージョン)でもRoslyn(VS2015)以前と以降でも、あるいはその前でも主にVSのバージョンによって細かく向上していっています。今回の検証はViaual Studio 2017 RCによるものです。また、C#コンパイラによる最適化は中間言語(IL)への変換までで、実行時最適化についてはJITコンパイラの仕事になってきます。

case数による最適化

switchは中身が少ない場合に、if文に置換される場合があります。例えばintでたった2ケースの場合

static int SmallCase1(int x)
{
    switch (x)
    {
        case 0: return 0;
        case 1: return 1;
        default:
            return -1;
    }
}

これは以下のように変換されます。

if (x != 0)
{
    if (x != 1)
    {
        // default:
    }
    else
    {
        // case 1:
    }
}
else
{
    // case 0:
}

量が少ない場合はifのほうが速い、という決定でしょう。3ケース以上では、書いたままの挙動です。しかしそもそも書いたままとはどういうことか、というと、具体的にはILにおいてはOpCodes.Switch、つまりジャンプテーブルを利用した分岐に変わります。

// Emitのイメージ
OpCode.Switch(label1, label2, label3)

MarkLabel(label1);

MarkLabel(label2);

MarkLabel(label3);

悪くなさそうです。

文字列のswitch

C#のswitchは数値以外にも文字列が使えます。文字列も同様に少ない場合はifに書き換わります。書き換わる数はintの場合とは異なり、もう少し多めです。

static int StringSwitch(string x)
{
    switch (x)
    {
        case "aaa": return 0;
        case "bbb": return 1;
        case "ccc": return 2;
        case "ddd": return 3;
        case "eee": return 4;
        case "fff": return 5;
        default:
            return -1;
    }
}

というswitchは

static int StringSwitch(string x)
{
    int result;
    if (!(x == "aaa"))
    {
        if (!(x == "bbb"))
        {
            if (!(x == "ccc"))
            {
                if (!(x == "ddd"))
                {
                    if (!(x == "eee"))
                    {
                        if (!(x == "fff"))
                        {
                            result = -1;
                        }
                     
    // 以下elseが続くので省略
}

に書き換わります。なるほど、若干理想のイメージと異なる気はしますが、shoganai。それより大きな場合は更に異なるものになり、

static int StringSwitch(string x)
{
    switch (x)
    {
        case "aaa": return 0;
        case "bbb": return 1;
        case "ccc": return 2;
        case "ddd": return 3;
        case "eee": return 4;
        case "fff": return 5;
        case "ggg": return 6;
    default:
            return -1;
    }
}

これは、以下のような原型をとどめてる気があまりしないものに変わります。

private static int StringSwitch(string x)
{
    uint num = <PrivateImplementationDetails>.ComputeStringHash(x);
    int result;
    if (num <= 1488952485u)
    {
        if (num != 876991330u)
        {
            if (num != 1426598844u)
            {
                if (num == 1488952485u)
                {
                    if (x == "bbb")
                    {
        // elseは中略
    }
    else
    {
        if (num <= 3003732817u)
        {
            if (num != 2339852366u)
            {
                if (num == 3003732817u)
                {
                    if (x == "fff")
                    {
            // elseは中略
        }
        else
        {
            if (num != 3815745472u)
            {
                if (num == 3848007555u)
                {
                    if (x == "ddd")
                    {
                    // (略)
            }
            else
            {
                if (x == "ccc")
                {
                // (略)
            }
        }
    }
    result = -1;
    return result;
}

えー、全然違うじゃん!何に書き換わるかというとまずどこからか出てくる

<PrivateImplementationDetails>.ComputeStringHash

によって文字列のハッシュコードが計算され、そこから二部探索的に目的のコード部分を探り当てるコードに変換されます。switch使えば文字列も一発で引いてくれるんだ、などという甘い幻想はなかった。普通のstring.GetHashCodeではなく、わざわざ隠れたComputeStringHashメソッドを使うのは、ハッシュ値の算出をライブラリに依存せずコンパイル時に固定するためでしょう(文字列のハッシュコード値は、環境によって異なる(同じであるとは保証されていない)ため、永続化させてはいけない)。

バラバラの数値の場合

文字列が思ってたのと違うような形に変換される他に、数値であっても、その中身によって変換結果が変わってきます。例えば、以下のようにバラバラの数値をswitchにかける場合

static int SwitchCase(int x)
{
    switch (x)
    {
        case 1230: return 0;
        case 324: return 0;
        case 24432: return 0;
        case 99: return 0;
        case 93429: return 0;
        case 929: return 0;
        case 199: return 0;
        case 94249: return 0;
        case 53: return 0;
        case 1234: return 0;
        default:
            return 0;
    }
}

これは以下のようなifの羅列になります。

if (x <= 929)
{
    if (x <= 99)
    {
        if (x == 53)
        {
        }
        if (x == 99)
        {
        }
    }
    else
    {
        if (x == 199)
        {
        }
        if (x == 324)
        {
        }
        if (x == 929)
        {
        }
    }
}
else
{
    if (x <= 1234)
    {
        if (x == 1230)
        {
        }
        if (x == 1234)
        {
        }
    }
    else
    {
        if (x == 24432)
        {
        }
        if (x == 93429)
        {
        }
        if (x == 94249)
        {
        }
    }
}

二分探索のifのコードになっていて、つまりstringのComputeStringHashを除いた場合と同様、ってことですね。

さて、何故こうなってしまうかというと、OpCodes.Switch を改めて見てもらうと

switch (N, t1, t2... tN) | Jumps to one of N values.

飛び番のテーブルは作れません。しょーがないよね、そりゃそうですね、ということなのであった。なお、別に0始まりである必要はなくて、連番であれば構いません。連番も、1,2個の欠番ならば塞いでくれます。以下のようなコードは

static int SwitchCase(int x)
{
    switch (x)
    {
        case 30: return 1;
        case 31: return 2;
        // 32, 33...
        case 34: return 3;
        case 100: return 4;
        case 101: return 5;
        case 102: return 6;
        default:
            return -1;
    }
}

以下のような、ILの原理を理解した上でイメージする最大限にいい感じのコードに書き換わってくれます。

switch (x)
{
    case 30:
        return 1;
    case 31:
        return 1;
    case 32:
    case 33:
        break;
    case 34:
        return 3;
    default:
        switch (x)
        {
        case 100:
            return 4;
        case 101:
            return 5;
        case 102:
            return 6;
        }
        break;
}
return -1;

C#コンパイラもなかなか賢いですね!

なお、switchはintや文字列よりも、Enumと組み合わされるケースが多いと思いますが、その場合はその後ろのプリミティブの型(何も指定していない場合はint)で適用されます。

// Enum定義
public enum MyEnum1 // defaultは : int
{
    Foo, // defaultは0から連番
    Bar,
    Baz,
}

// 数字を弄るパターンも割りとよくある(10XXXは魔法、20XXXは物理、みたいなゾーン分けなど)
public enum MyEnum2
{
    Foo = 100,
    Bar = 200,
    Baz = 300,
}

つまり、上記のようなEnumをswitchした場合、前者のほうが速い、ということになります。

switch非対象の型でswitchしたい場合の最適化手段

intや文字列以外でもswitchのようなことをしたい、場合は全然あると思います。ifの羅列、もいいですが、数が多くなる場合は、今回見たstringの生成結果を模してみるのも効果的かもしれません。

// 数が十分に多い場合。10以下ぐらいならif列挙で良いでしょう(stringと同じように)
static readonly Dictionary<Type, int> jumpTable = new Dictionary<Type, int>(3)
{
    {typeof(FooClass), 0 },
    {typeof(BarClass), 1 },
    {typeof(FooBarClass), 2 },
};

static void TypeJump(Type t)
{
    int key;
    if (!jumpTable.TryGetValue(t, out key)) return;

    switch (key)
    {
        case 0:
            // case FooClass
            break;
        case 1:
            // case BarClass
            break;
        case 2:
            // case FooBarClass
            break;
        default:
            break;
    }
}

int側は0からの連番にすることにより、ジャンプテーブルが間違いなく使われることが期待できます。他に、より書きやすい代替としてDictionaryにActionを詰める(Dictionary<T, Action>)手法もありますが、intからのswitch-caseのほうが効率的に動作するでしょう。

まとめ

コード高速化の手段としてILGeneratorを用いて、ILを直接動的に書き込む手段は(AOT/IL2CPP環境など動的生成が許されていない環境でなければ)非常に有効です。動的生成ならではの、ifなどの分岐を生成時に決定して除去する、定数的なものを埋め込んでしまうなど、その生成時に最適化されたコードパスを通すなどのような手段が取れることも大変強いです。しかし、ILを直接書き込むことには弱点があり、今回見たようなC#コンパイラの行う最適化は事実上できません。やろうと思えば出来ますが、途方もない手間であり、実際は、素直なILを書く程度に留まらざるをえないでしょう。一概に動的コード生成すれば最速なんだ、というわけではないし、また、ではより速いコードを作るにはどうすればいいのか。何故やや遅くなってしまうのか、の原則は、こういうところに隠れているものかもしれません。

なお、強調しすぎてもしすぎることはないですが、「速い」といっても、これは本当に本当に些細な差ですので、よし、Enumは連番のほうが速いから連番にしよう!とかはやめるべきです。理屈上そうだというだけで、実際のアプリケーションで有意な差にまで繋がることはほぼないでしょう。

CES2017でみた独自コントローラと一人称移動ゲーム

f:id:yasa0:20170201171550j:plain CES2017では、昨年に引き続き各ブースでVR関連製品が展示されていました。目立ったものでも、ワイヤレスPC、ヘッドマウントディスプレイ、センサー、サウンドだけにとどまらず、OLEDなどの部品に至るまで多種多様な広がりを見せています。

今回は私が特に注目した、両手両足へ触覚のフィードバックができるコントローラを紹介します!

両手両足に触感が宿るデバイス「Taclim(タクリム)」

f:id:yasa0:20170201171431j:plain 動きによる入力と、触覚のフィードバックに対応したVRデバイスで、両手に持って使用するグローブ部分と、両足に履いて使用するシューズ部分があります。

開発は株式会社Cerevo。Taclim用にカスタマイズした最新鋭のタクタイル・デバイスを合計8つ搭載。VR空間の映像や音声に合わせてシューズとグローブが触感をフィードバックすることで、両手両足にフィールド内の砂や鉄板の踏む触覚や敵を殴った時の衝撃が感じられます。

ファンタジー世界へいざ!

f:id:yasa0:20170201171537j:plain 体験したのは、一人称視点で歩いて迫りくる敵を倒して城の中を進んでいくデモ。両手両足にTaclimを付けてその場で足踏みをします。敵が来るとグローブを持った手でパンチ!コントローラーが振動することで、ちゃんと殴っている感覚がします。 また、砂や鉄板の上を歩いて進むと動きに合わせて、ザッザッという音と感覚がします。その場で足踏みをするため、完全にフィールド内を歩いている感覚とまではいきませんでしたが、感触はまさに歩くときの感覚でした。
手へのフィードバックまでなら、HTC Viveのコントローラと同じで体験したことがある感覚でしたが、足の裏に感じる素材が変わる感覚まで組み合わさると「ちゃんと歩いているんだ」、と実感して筆者は少し感動してしまいました。

デモが終了し、デバイスやヘッドマウントディスプレイを外すとわずかに汗をかいていて、知らず知らずのうちに結構動いていたことに驚きました。

感じた可能性

筆者はこのデバイスは家庭に置くというより、アーケードやジムなどにおいて、ゲームはもちろんフィットネス関係でも活躍しそうだと感じました。なんたって普段3分しないうちに足踏み運動をやめたくなる筆者がそれ以上の時間を夢中で足踏みしてしまったのですから。ジムに置かれているVRデバイスですとICAROSもありますが、ICAROSと同様にぜひとも楽しいフィットネスがTaclimで登場することを、筆者は期待しています。

Taclimの発売は2017年秋頃、価格は10~15万円程度を予定です。

スペック(公式サイトより)

項目 スペック 
グローブ部・本体サイズ 50 ×50 ×147 mm(W×D×H)
シューズ部・本体サイズ 290×100×140 mm(W×D×H)
搭載センサー 9軸(加速度、角速度、地磁気)
タクタイル・デバイス数 8(グローブ部1×2、シューズ部3×2)
ワイヤレス接続 BLE(Bluetooth 4.1)
Sub-GHz(Japan 920MHz/USA 815MHz/Euro 868MHz)
充電方法 Micro USB
利用時間 約2時間(2秒に1回振動した場合)
充電時間 約3時間
開発環境 Unityプラグインを提供予定

※開発中のため製品仕様は変更になる可能性があります

参考: (公式お知らせ)世界初の触感センサー搭載VRシューズ&グローブ「Taclim」開発

Room-Scale Mobile VR の可能性

2017/1/5 ~ 2017/1/8 に米国ラスベガスにて開催された CES 2017。世界最大の家電見本市と言うのは本当で、非常に多くの製品やデバイスの展示がなされていました。その中でも特に気になったもののひとつが Room-Scale Mobile VR として LYRobotix が展示していた NOLO です。

https://scontent.xx.fbcdn.net/v/t31.0-8/15875395_356429498070344_1314860561549668494_o.jpg?oh=089c66c262632114f62da4a5ab39926d&oe=591873FC 画像出展 : NOLO Facebook 公式

特徴

これまでルームスケールで VR を実現するには HTC Vive や Oculus Rift のように専用のトラッキングセンサーが必要で、Daydream や Gear VR のようなモバイル VR では実現されていませんでした。しかし、この NOLO を利用すればモバイル VR を手軽にルームスケール化することができるようになります。

  • 6 軸モーショントラッキングが可能
  • ワイヤレス *1
  • 低レンテンシ *2
  • バッテリー寿命が長い
  • センサー/コントローラーが非常にコンパクト (= 手軽に持ち運びできる)
  • 奥行 6 m くらいまでトラッキングできる *3
  • 希望小売価格 $149 と安価 *4

デモ動画

公開されているデモ動画では PC と有線接続されていますが、これはプレイ画面の出力のためのようで、プレイするだけならワイヤレスで実現可能とのことです。センサーの小ささや着脱の容易さが伺えます。

ブース体験

CES 2017 のブースでは Google の Tilt Brush をプレイすることができました*5。低レイテンシと謳いつつも体感としてはかなり問題がありましたが、説明員によると「会場の Wi-Fi 環境が非常に悪いため」とのことでした。

https://scontent.xx.fbcdn.net/v/t1.0-9/15871717_356881781358449_2791179951162430403_n.jpg?oh=8f71456132efd191845b0bd41f554b97&oe=591B8F04 画像出展 : NOLO Facebook 公式

まとめ

スマホと NOLO だけ持って出かければ、どこでもカジュアルにルームスケール VR 体験ができる。そんな未来が本当にすぐそこまで来ているのだ素直に感じました。どの程度相互干渉があるのかが不明だったりブース体験でのレイテンシ問題などまだ課題はあるかもしれませんが、今後十分注目してよい領域なのではないかと思いますし、NOLO の体験によって今後のモバイル VR への期待感が高まりました。

*1:程度は不明。

*2:程度は不明。

*3:ブースで CEO から受けた説明による。公式サイトでは 13 ft (= 3.9 m) で記載されている。

*4:先着 100 人限定で $89 で販売予定。

*5:Tilt Brush は HTC Vive にしか対応していないはずだが実際にプレイできた。本体 PC の画像を飛ばしているだけなのかは未確認。

CES で出展されていた AR メガネから感じる AR の未来

年始の 2017/1/5 ~ 2017/1/8 に米国ラスベガスで開催された CES 2017 では、前回に引き続き今回も VR / AR が一大トピックとして取り上げられていました。個人的に注目していた AR 分野ではメガネ型デバイスが多くブース展示されていました。その中でも特に良い出来だと感じたものが Osterhout Design Group (以下 ODG) の R-8 / R-9 です。

http://www.osterhoutgroup.com/img/product/r9_side.jpg ※画像出展 : ODG R-9 公式

スペック概要

R-8 / R-9 は透過型のスマートグラスで、仕様上の性能が高く目を見張るものがあります。

  • Qualcomm Snapdragon 805 2.7GHz Quad-Core Processor (CPU)
  • GPS / Glonass (位置検出)
  • 6 軸トラッキング (向き検出)
  • Android ベースの Reticle OS を搭載
  • 通常の Android アプリも必要
  • 明るさの自動調整
  • Bluetooth 5.0 / Wi-Fi 802.11ac / USB Type-C による外部アクセサリ接続
  • 高フレームレートカメラ
  • デュアルマイク
  • 指向性スピーカー
内容 R-8 R-9
視野角 40°以上 50°以上
重さ 約 127g 約 184g
解像度 720px 1080px
記憶容量 64 GB 128 GB
価格 $1,000 以下 約 $1,800
出荷予定時期 2017 年内 2017 Q2

入力はメガネのツルの部分にあるタッチセンサーで行えるほか、スマホアプリやキーボード、マウスなどと無線接続して行うこともできるそうです。

Microsoft HoloLens との比較

以下の動画では ODG AR メガネと HoloLens での見え方の違いを比較していますが、全く引けを取らない美しさやパフォーマンスが見て取れます。ただし、CES 2017 のブース展示で見た範囲では綺麗に表示されているが周辺を暗くし過ぎて (= サングラス具合がキツくて) 回りが見づらいというのは感じました。

ODG AR メガネは HoloLens に比べて小さく軽量という大きなメリットがあります。対して HoloLens は空間認識ができ、モーションによるインタラクティブな操作ができます。また Full Windows 10 が動作するため、Windows エコシステムをそのまま利用できる圧倒的な強みがあります。Reticle OS がどこまでできるのかは見極めていく必要がありますが、Android ベースでかつ外部アクセサリと連携が可能というだけでも期待できるデバイスのひとつなのではないかと思います。

まとめ

ODG AR メガネ、HoloLens などのデバイスはそれ単体でどうこうと言うよりも、外部のデバイス / AI / Bot / Drone などとの連携によってよりパワーを発揮するのだと考えています。そのような連携を支える基盤となり、かつ単体での描画性能も正統に進化してきているというのを CES 2017 で感じることができました。