VTuber配信を始めるためのメモ
VTuberというかバ美肉してみようかな、と思ったけど最初は何から始めれば良いかわからなかったので備忘録がてらメモ。随時更新。
準備
アバター/3Dモデル準備
まずはアバター準備。セシル変身アプリがVRMを出力して自由に使えるし、VRChatやClusterでも使えるのお勧め。
VRoidとかも有名だけど「絵を描くように3Dモデルが簡単に描ける!」とある通り、絵を描く程度に難しいので私みたいに絵心無い人とか人生ゲームのキャラクターエディット見たいにパーツを選んでカスタムしたい人向けではないので注意。使いこなせる人にはとても良いツールなんだろうけど。
Vカツ
パーツ選び系のキャラクタエディットツール。高機能かつ多彩な感じでとてもいい。
難点はニコニコ立体にVRMをあげての利用になるので、5000円という値段はともかくVRChatなどVirtualCast以外の環境で使えない。。。 あと、2.0はどうなったんだろう?
セシル変身アプリ
パーツ選び系のキャラクタエディットツール。簡単にかわいいキャラクターが作れてVRMも出力できる神アプリ。
デザインのベースがロリ系に寄せてあるので、それ以外の造形を作ろうとすると結構センスがいるのかも? 継続して開発されていてカスタムできる部分も増えどんどん進化中
ニコニコ立体 & シードオンライン
VirtualCastで利用するためにはVRMはニコニコ立体かシードオンラインにアップロードする必要がある。Seed onlineでは他にもVCIで作られた小物やスタジオが無料ないしは有償で入手できる。
Booth
創作系、とくにVR関連では鉄板の販売サイト。いろんなギミックが仕込まれたアバターも販売してるので、気に入ったアバターを買うのも一般的。カスタム可能なものもある。
スタジオツール
アバターを動かす場所。VRChatのようなVR SNSから中継してる人も居るし、Clusterのようにより勉強会などイベント開催に向いたものもある(今はVR SNSとしても利用可)。
VirtualCast
特にニコニコ生放送と連携したツール。凸機能があるので他の人が配信に参加することもできる(#OFFには普通にできる)。
他にもYoutubeにも対応している。個人的にはVRスタジオ/VR SNSの中では一番UXがこなれていると思し、配信にも便利。リングメニューは素晴らしい。でも自分のスタジオを複数個もったり保存出来るともっと良いのだけど。。。
なお、アバターの追加方法はこちらを参照。
配信ツール
VirtualCast単体ではYoutubeやニコニコ生放送に配信が出来ないので、配信ソフトと組み合わせる必要がある。配信ソフト自体はVRとか特に関係無いのでWindowsやMacで動くソフトを選べばOK。
N-Air
OBSのカスタマイズ。ニコニコ動画に上げるのに便利な機能が付いてるのでニコニコで使うならこっちがお勧め。
OBS
Youtuber/VTuber問わずほとんどの人が使ってる配信ソフトの定番。シーン切り替えや複数のWindowの合成など何でもできる。ゲーム実況とかライブコーディングとか。
ボイスチェンジャー
バ美肉は最近は地声の人も多い(まあ、最古ののじゃロリおじもだが)けど、声を弄りたい時はソフトウェアボイスチェンジャーが便利。恋声とバ美声が有名。あと自分で変声してる両声類という人達もいる。すごい。
うちの構成はこんな感じ。
YAMAHA NETDUETTO
仮想サウンドデバイス。マイクやスピーカーのように振る舞う仮想デバイスでこいつをハブにしてマイク、ボイスチェンジャー、OBSやVirtualCastなどのソフトを繋ぐことが出来る。ボイスチェンジャーはNETDUETTOを出力先に指定することで、OBS/VirtualCastからみたらこいつがマイクなどの入力機器に見える。
バ美声
現在使用中。比較的低遅延なので歌とか歌わない限りは問題無く使えると思う。これより低遅延を求めるとたぶんハードウェアエンコーダー使う必要があると思われる。割と良い感じ。
RTX Voice
フリーで使えるノイズキャンセルツール。バ美声とも組み合わせて使えるのでお勧め。
配信について
配信のやり方やTips。正直自分は精進が全く足りない。。。
スライドの準備
自分の配信はプレゼンスタイルなのでスライドを使います。VirtualCastでスライドを作るには以下のホワイトボードを利用すると簡単に出来ます。
ただ、サーバに画像をアップロードする必要があるので、手前味噌ですがPDFやPPTXをアップロードすれば自動で画像に変換してURLをJOSN形式で返すサービスを作りました。VirtualCastでプレゼンをしたいときには便利だと思います。
配信待ち画面/シーン切り替え
Yotubeやニコニコ生放送では配信ボタンを押すと配信がすぐ開始されます。ただ、VirtualCastとニコニコ動画を接続しようとすると配信の後にバーチャルキャストを起動する必要があります。 その他にも単純に開始してから少し準備をしたい時とか、時間ピッタリに始めるけどある程度参加者が集まってから配信できるように少し早めに配信を開始したい事は良くあります。
そういうときに画面の連携を切っておくことも可能ですが真っ黒な画面を流しておいても味気ないので「シーン切り替え機能」を使って待ち受け画面を作ることが出来ます。
下記はOBSのやり方ですがN-Airでも手順は同じです。
BGM
待受け画面とかあるいは配信中にBGMを鳴らしたいこともあると思います。動画編集ツールで足したり配信ツールで足したりすることも出来ますが、一番簡単なのはシンプルにデスクトップで音楽プレイヤーを鳴らす事です。配信の著作権には要注意。
下記では多くのフリーBGMを公開されています。
また、あえてVR上で完結してみたいという欲求もあると思います。VirtualCastの場合は下記のVCIを使う事でVR上で音楽を再生してそのまま配信に乗せれます。
VRスタジオ内でも音が聞けてめっちゃ便利!
サムネイル
Youtubeとかに動画として挙げる時にはサムネイルがあった方がたぶんよい。作るのは簡単でお勧めは適当なシーンのスクリーンショットをとってそれをパワポに貼り付けて文字を乗せる。DTPとかちゃんとしたソフト使うべきだけど、私のようにセンス無い人はどうせ高度なこと出来ないからこれで十分。センスが欲しい。。。
動画編集
動画の前後の余計な待ち時間をカットしてトリミングしたり、長時間動画の切り抜きを作ったり、そもそもテロップ入れたいとかいろんな欲求も出てくるはず。簡単な処理はYotube上でアップロード後にも出来るけどとてもエンコードに時間がかかる。どうせ、VRする人は良い感じのGPU詰んでるのでPCでやった方が速いのでお勧め。
古き良きAviUtl や、検索したら良く出てくるFilmoraもあるけど簡単な事をしたいのならShotcatが便利だった。エンコードプロファイルに「Yotube」って項目もあるしね。QtベースだからLinuxなイメージがあるかもだけど普通にWindowsでもMacでも問題なく動作するしUIも普通の動画編集ソフトみたいな感じ。kdenliveも良さげだけどとりあえずShotcatを使用中。
テックポエマーの10min IT News! - 2020/03/09 - 「コロナウイルスと電子書籍、パスワードを竜破斬に」 を公開しました
今回からWebページのスクショを直接じゃなくて、一度パワポに張り付けて画像に変換してみました。サイズが統一されて見やすくなったはず。
日本もアメリカもコロナウイルスの影響で大混乱というのが今週の傾向かな。動画では言い忘れたけどAmaoznやMSのシアトルオフィスも全面リモートワークになったりと動きが加速してる感じ。
それにしても電子書籍が紙のコミックを売り上げベースでついに超えたのは嬉しいですね! 合算したコミック全体の売上も増えてるし。
今週のサマリ
- QuarkusいっきにJavaのメジャーフレームワークへ。あれHelidonは?
- コロナウィルスで様々なイベントが中止。一方、Zoomなどのリモートワークは進む
- 電子書籍のコミック売上がついに紙のコミックを超える
- パスワードを竜破斬のカオスワーズにするとセキュア
動画中の引用記事
BeyondCorp? ゼロトラスト? VPNを超えていけ!
- TL;DR
- はじめに
- BeyondCorp << ゼロトラスト
- ゼロトラストセキュリティ
- リモート接続のセキュリティ3要素
- VPNの問題点
- ゼロトラストによるセキュアな通信
- ゼロトラストの企業ネットワーク
- コンテキスト認識アクセスコントロール
- ゼロトラストによるVPNの問題点の解決
- ゼロトラストの課題と展望
- まとめ
- 参考
TL;DR
- BeyondCorpはゼロトラストの実装の一つ。ゼロトラストは超乱暴に言えばポストVPN
- 社内も信用出来ないとして社外と同じ方式でNWを組む
- HTTPSによる暗号化と次世代認証基盤を使ったCAACによるアクセスコントロール
- FWによる境界からIDによる境界へのパラダイムシフト
はじめに
ニコ生でゼロトラストについて話したので、こちらは解説記事となります。
動画で利用したスライドはこちら。SpeakerDeck
コロナウイルスによりまさかのパンデミックによるBCPが発動されている今日この頃。皆様はいかがお過ごしでしょうか?
出社してる人、自宅からリモートワークをしている人、色々だと思います。
リモートワークと聞いてまず最初に思い浮かぶのがVPNですね。VPNで社内ネットワークに接続して社内と同様にアクセスできる。リモート接続の鉄板ですね。
ですが、VPNにもそれなりに問題があり、最近はゼロトラストセキュリティという言葉も聞くようになりました。あるいはGoogleはVPNを辞めてBeyondCorpを採用する事で、VPN詰まりも無く在宅勤務できる仕組みがあるとか。
私も初めて「Google VPN辞めたってよ」的な話を聞いた時には「????」という感じだったので、今回はゼロトラストセキュリティあるいはBeyondCorpに関して説明していこうと思います。
BeyondCorp << ゼロトラスト
現時点での私の理解では、という前置きは付いてしまいますがBeyondCorpとゼロトラストは言っている事は同じです。
もう少し言い換えるとGoogleでのゼロトラストの実践がBeyondCorpです。このあたりはDevOpsの実践の一つがSREなのと同じですね。
歴史的にゼロトラストが流行る前からGoogleが対応していた名残だと思います。なのでGCPでBeyondCorpを見かけたらゼロトラストの事と思えば良いですし、GCPな方はゼロトラストをBeyondCorpと理解すればだいたいあってます。
ゼロトラストセキュリティ
Zero Trust SecurityとZero Trust Networkの差は前者の方が少し範囲が広く後者は企業ネットワーク基盤の話をしている認識です。とはいえ大きな差は無いでしょう。
スライドにも書いている通り、トラディショナルな企業ネットワークは「信頼されたネットワーク」の存在を前提に成立しています。
たとえば、DCの内と外はFirewallでがっちりと守ります。しかし、その中はどうですか? DC内の通信にFirewallやIDS/IPSはどこまで適用されるでしょうか?
DC内は安全という前提でVPNでDC内に繋ぐ事で社内システムにアクセスができます。多くの社内システムは外部にさらされることを想定していないので認証が弱かったりDDOS攻撃のような基本的な対策もされていません。
このようにネットワークを内と外で分けるのがトラディショナルなネットワークの基本的な考え方です。しかし、これは現代では十分ではありません。
ゼロデイ攻撃、巧妙なウイルス、ターゲッティング攻撃、悪意のある利用者と様々な理由により境界となるFWを超えた攻撃がなされるのは常識です。
そのため、内部のアクセスであっても常に疑い認証をしっかりとする事が重要です。結果として「社内と社外を区別せず常に如何なる場所からのアクセスであっても検証する」という同じコントロールが実現できます。
リモート接続のセキュリティ3要素
リモート接続には様々な方式が存在しています。TELENT, VNC, SSH, そしてVPNです。
かつてはSSHの利用も多かったとは思いますが、今はVPNが主流だと思います。VPNにはDC間を繋ぐSite To Ste(S2S)VPNとClient to Site(C2S)がありますが、リモート接続に使うのはC2Sですね。
一言でVPNと言ってもその特性や実装はプロトコルや製品で大きく異なりますが、企業向けのリモート接続でC2SのVPNに要求される機能は以下でしょう。
Authentication/認証
VPNクライアントを立ち上げると社内アカウントのIDとパスワードを入力します。場合によってはMFAで生体認証やスマートカードを使うケースもあります。いずれにしても利用者のIdentityはVPNにとっても非常に重要な要素です。
Device Trust/機器認証
これは厳密には認証の一部ですが分かりやすいのであえて切り出しました。BYODとしてどんなPCからもリモート接続可能というケースもありますが会社から提供されたPCのみから使えるケースも多いと思います。これを実現するためにSSL証明書など端末にインストールする方式をとります。。これによって安全なPCかどうかを判定するわけです。
Encryption/暗号化
VPNの最も基本的な機能と言っても良いでしょう。Virtual Private Networkというだけあってあらゆる通信パケットをラッピングして暗号化するのがVPNの本業です。これによりインターネットを経由したアクセスであってもセキュアな通信が実現できます。
セキュアな通信としてはインターネットVPN、広域IP網を利用したIP-VPN、そしてVPNじゃないけど専用線があり良く比較に上がると思いますが「セキュリティ観点では」実質これらは同等です。
IP-VPNや専用線を使うのは単に通信品質の保証やレイテンシなどパフォーマンスのためです。セキュリティ強度としてはSSLを利用したインターネットVPNで問題なく、そこは製品選定の時には誤解しないように注意が必要です。価格は圧倒的に違いますしw
リモートアクセスとしてのVPNは上記の要素によりセキュアな通信を実現しています。逆に言えば上記の3つを満たせばVPNでなくてもセキュアな分けです。
VPNの問題点
つづいてVPNの問題点について考えていきましょう。大きく以下の3つがあります。
キャパシティ/パフォーマンス
まずはキャパシティです。VPNは基本的に社内のDCに接続しに行くのでVPN GWが必要になります。
これはリモート専用の設備なのでリモート利用者向けにキャパシティプランを組み立てますが、そこがネックです。1万人の会社で1万人がリモートアクセスするケースはまれでしょう。せいぜい1000人分のキャパプラをします。
これが災害時には5000人とかに跳ね上がり混雑を引き起こします。BCPを考慮した予算確保は現実的には結構大変なので現実的な課題です。
またパフォーマンス観点でもVPNは暗号化された通信なので原理的に遅いというのもありますが、そもそもDCを経由した通信を実現する技術です。DC内にアクセスする場合なら良いのですがSaaSの場合は不必要な経路を通ることになります。地理分散してない場合は海外からの接続では大きくレイテンシーが劣化してしまう懸念もあります。
特にVDIを利用している場合は通信がTCPに固定されてしまったりVDI側の最適化方法が上手く使えない場合もあるので通信が劣化しがちです。
このようにVPNにはいくつかのオーバーヘッドがあり、キャパシティプランも個別に必要なものとなっています。
適切な制御の難しさ
VPNは基本的に社内に中継ポイントを置いてそこからアクセスする方式なので追加のACLを入れない限り社内と同様のアクセスポリシーが適用されてしまいます。
つまり良くも悪くも「リモートからもVPNを繋げば社内と同じ完全に同じことができる」ということです。
例えば「リモートからメールなどのシステムにはアクセスしたいけど、顧客管理システムにはアクセスさせたくない」というケースがあったとします。
この場合、VPNで振られるIPで顧客管理システムの手前にあるFirewallで止めるしかありません。さらに「基本的には禁止なんだけどAさんだけは顧客管理システムにアクセスさせたい」という要件もあり得ます。この場合は、AさんのIPに対してさらに例外的な設定をする必要があります。
VDIを利用しているとより複雑で、社内と社外で同じVDIを使うとリモート接続かどうかがそもそも判定できません。そのため社外用のVDIを別に用意するケースも考えられます。
また、これらの制御はあくまでACLなので、通常のアクセス権を管理しているフロー(アカウント管理とか権限管理とか)とは異なります。異なる作業を組み合わせるためオペレーションや業務フローが煩雑になりがちです。
これらの課題は既存のDCの設計次第ですし、実現できなくはないのですがコントロールのコストが高すぎるためあまり細かな権限管理は行われていないのが実際だと思います。
一貫したセキュリティポリシーの適用が困難
VPNの最大の問題として「VPNに繋がないと社内セキュリティが適用できない」という事が言えます。
これは簡単に言えば社内ProxyやPaloAltoなどの次世代FW、あるいはEDRやデバイス制御の適用外になるという事です。VPNを使わない場合は通常のインターネット経路の接続になってしまい、トラフィックを管理することが出来ませんし、DC内にあるセキュリティツールからリモート管理することもできません。
このようにオンプレミスのセキュリティと相性が極めて悪いのです。そのためリモート接続用のPCのセキュリティが下がってしまいます。
これに対応するためにリモート接続用のPC向けに別の製品を入れるなどでセキュリティを担保する事もありますが、これもコントロールがケースバイケースになる事で複雑化してしまい望ましくはありません。
他にも接続先が社内システムなのかSaaSなのかで適用するセキュリティ製品が異なる場合もあります。たとえば、社内システムにはVPN必須だけど社内でも利用しているSaaSはVPN無しでも行けてしまうとか適切に設定をしていないと脆弱性になりますし、社内のセキュリティルールが複雑化して矛盾が発生してしまう元です。
このようにVPNでは「オフィス内とリモート接続」「DC内とSaaS」といった複雑な環境に対して一貫したセキュリティモデルを適用することが困難です。
ゼロトラストによるセキュアな通信
それではゼロトラストでどのようにセキュアな通信を実現するかを見ていきたいと思います。
ゼロトラストは既に述べているとおりFWではなく認証システムを基盤としたセキュリティモデルです。そのため、認証という点では問題なく実施できます。
例えばAzureADを使った場合であればセキュリティキーやスマホの認証アプリ、あるいは指紋認証などのFIDO対応のMFAを適用しパスワードレスな運用もできます。デバイス認証に関してもIntuneなどの組み合わせて実現できます。
大きく異なるのは通信の暗号化です。VPNでは全ての通信を暗号化していました。これは当時としては必要だったのですが現在はWebアプリケーション化も進んでおりHTTPSも当然利用されています。VDIのようなアプリケーションの場合でもSSL/TLSには対応してます。その為、暗号化自体は個々のアプリケーション側に任せるというのがゼロトラストの考え方です。
時代が、暗号化を常識とするようになったので、結果としてリモートアクセス基盤としてはVPNのように「全てを暗号化する」という責務から解放されたわけです。
ゼロトラストの企業ネットワーク
ゼロトラストにおける企業ネットワーきうの形を見ていきましょう。
「トラディショナルな企業ネットワーク」と異なり信頼できるネットワークは無くなり、認証基盤により各ユーザ/各アプリ毎にチェックを行います。
ネットワークとFWがセキュリティ境界だったトラディショナルなモデルから、IDを境界にセキュリティを担保するようになりました。
後述するCAACにより社内なのか社外なのかというのはあくまで多要素の一つとして利用はされますが、根本的にはどこからアクセスするかとかどこにアクセスするかは関係なくなります。
これにより社内も自宅もDCもSaaSも関係なく同一のシステムでアクセスコントロールが掛けれるようになります。
コンテキスト認識アクセスコントロール
引用: [Japan Tech summit 2017] SEC 010
Context-Aware Access Control (CAAC) ではコンテキスト----すなわち「誰が」「どこから」「どのデバイスで」「どのアプリに」アクセスしようとしているかを判定して、それらのリスクを計算した上でアクセス可否を判定します。
これにより同一の人が自宅からアクセスするときと社内からアクセスする時で認証の制御を分けることが出来ます。VPNが苦手だったロケーションによる複雑な制御がゼロトラストでは容易になります。
このようなリスクを動的に計算する仕組みはルールベースの認証だけではなく、機械学習などにより「いつもと違うアクセスに対して追加の認証を要求する」と言ったリスクベース認証も可能です。
たとえば、出張等で海外に行っているときや不正アクセスが多くリスクが高いと判定された時は追加の認証が自動で入ったりする、などが例としてイメージしやすいかと思います。従来のRBAC + Network ACLではこのような柔軟な制御は苦手なのでCAACになって実現しやすくなったことの一つです。
ゼロトラストによるVPNの問題点の解決
ゼロトラストでは。VPNと違いリモート専用の仕組みという分けではなく社内も社外も同じ仕組みです。その為社外向けの特別なキャパシティプランは不要です。つまり社内向けに1万人の設計をしたら1万人分のリモートアクセスのキャパシティは持っているわけです。
また、CAACを基本とした次世代認証基盤により社内も自宅もDCもSaaSも一貫した仕組みでアクセスコントロールが出来ます。もう、リモートの場合は特定のIPセグメントを振って、それに対してACLを入れて。。。と言ったことは不要です。
アクセスコントロールだけではなくエンドポイントセキュリティに対しても一貫したコントロールが可能になります。
CAACの機能として「どこから」を判定する必要があるため基本的にこの認証基盤はインターネットからアクセスされる場所にあります。つまり、社外からの門番として存在していたVPN-GWの代わりに認証基盤自体がGWになってるわけです。
そのため「VPNに繋ぐ」というクッション無しにダイレクトにSaaSへの認証やアクセスを制御できます。これによりEDRやEgress Filterでもクラウドベースのセキュリティ製品を利用しやすくなります。これらを使いVPNを経由せずインターネットから直接端末を保護することで、社内にいる時と同様に社外のセキュリティをコントロールできます。
このようなデバイスレベルでのセキュリティ、End to Endのセキュリティがゼロトラストの特徴です。
半面、認証基盤に要求されことは多くAzureADやCloudIdentityあるいは各種セキュリティ大手ベンダーやベンチャーが出しているIDaaSを使うのが無難でしょう。
ゼロトラストの課題と展望
セキュリティと言えば「めんどくさい」「生産性が下がる」というのがイメージだと思いますが、ゼロトラストは珍しく生産性とセキュリティの両方を向上させる概念です。
ゼロトラストで社内と社外を区別しないというのは結構大きくて、これは「スタバのようなカフェ」と「会社の社内ネットワーク」を同じセキュリティレベルとしている事です。逆に言えば会社のネットワークセキュリティを普通のWiFiレベルまで下げれます。
ただ、万能ではなく課題というか注意点もあります。
たとえば、ネットワーク的には同一とはいえ社内と社外では監視カメラや入退室のセキュリティーカードといった物理セキュリティが異なるので通常は区分する必要があることも多いでしょう。
また、WebアプリケーションならHTTPSやIDaaS対応は容易ですが、C/Sベースのレガシーな仕組みだと難しい部分もあります。
特にサーバとの通信が平文の謎プロトコルだとクライアント側に結構手を加えないと実現は難しいでしょう。新規に作るものやHTTPS/SAMLに対応したデスクトップアプリなら問題はありませんが。
そういったこともあって、まだまだVPNを利用される事は多いと思います。しかしSaaSが増えてきた現在ではゼロトラストの観点でセキュリティを組んでいくしかありません。基本はWebアプリケーションにしてC/S型のデスクトップだけVDIやアプリケーションストリーミングを利用して対応する必要があるかと思います。
レガシー対応としては、既存のWebアプリケーションをゼロトラストに対応させるGoogleの「BeyondCorp Remote Access」やMicrosoftの「Azure Application Proxy」が有効です。C/SにはCitrixの 「アプリケーション仮想化」などが使えるかと思います。
まとめ
今回はBeyondCorpを皮切りにゼロトラストに関して説明してみました。改めてまとめるとポイントは下記です。
- BeyondCorpはゼロトラストの実装の一つ。ゼロトラストは超乱暴に言えばポストVPN
- 社内も信用出来ないとして社外と同じ方式でNWを組む
- HTTPSによる暗号化と次世代認証基盤を使ったCAACによるアクセスコントロール
- FWによる境界からIDによる境界へのパラダイムシフト
それにしても震災のタイミングではじめてDRという考えが「現実的なシナリオとして」定着しましたが、今回のパンデミックでBCP観点でのリモートワークも現実的な検討になるんでしょうか?
災害は喜ぶことでは決してないですが、雨降って地固まるというか塞翁が馬というか少しでも困難を糧にできれば良いなぁ、と思います。
あと、余談ですがAzureADはADのマネージドクラウド版ではなく、まったく別の認証基盤でむしろADFSに認証機能が内蔵されて進化したものととらえた方が良いので名前に惑わされないように注意しましょう。
それでは、Happy Hacking!
参考
プログラマは非効率的なコードを書くべきである
「プログラマは非効率的なコードを書くべきである。」
メモリやCPUリソースを普段から意識してはいけない! こう書くと山盛りの反論が来そうです。ですが性能の良いコードを普段から書く必要あるのでしょうか?
もちろんプログラムの実装による性能改善は重要です。
パフォーマンスチューニングをミドルウェアで頑張ったり、金の弾丸でサーバーを増強したり増やしたりするよりも圧倒的な効率で性能改善が出来ることはザラです。
それと同時に「こっちの方が効率が良いと思って...」という気持ちでちょっぴり複雑なコードを作ってしまう事も良くあります。
さて、性能とは可読性より優先するものでしょうか??? Stringのプラス演算子のコストが高いから常にStringBuilderを使うべき、Lambdaは遅いからfor文を使え。リフレクションは遅い。色々あります。ええ、確かに理論上遅いはずです。
「その幻想をぶち壊す!」
「確かに効率が悪いのは確かさ。でもその効率って奴は何かを...そう可読性とかを犠牲にしてまで守らなきゃならないもんなのか!?(上条さん風)」
というわけで、計測大事だよねって観点で実際最近の言語/マシンではどのくらいなもんかをマイクロベンチして見ます。 ちなみに特性は当然言語や処理系によって変わりますが今回はJDK11を利用しています。
文字列結合編
Javaの文字列結合でStringのプラス演算子ではなくStringBuilderを使うべし! ってのは有名ですよね。 これは事実だけどStringBuilderが必須な文字列結合のケースは結構検定的です。
コンパイラ編
まずは実測の前にコンパイラさんの仕事を見てみましょう
public static void checkJavacOptimizationPlus() { String hello = "hello(v)"; String world = "world(v)"; final String HELLO = "hello(c)"; final String WOLRD = "world(c)"; String mixedStr2 = "hello(l)" + "world(l)" + HELLO + WOLRD + hello + world; } public static void checkJavacOptimizationSB() { String hello = "hello(v)"; String world = "world(v)"; final String HELLO = "hello(c)"; final String WOLRD = "world(c)"; StringBuilder sb = new StringBuilder(); sb.append("hello(l)"); sb.append("world(l)"); sb.append(HELLO); sb.append(WOLRD); sb.append(hello); sb.append(world); String sbStr = sb.toString(); }
リテラル、定数(final)、変数をJIT前のコンパイラがどう変換したかをjavapの結果を見てみます。
public static void checkJavacOptimizationPlus(); Code: 0: ldc #22 // String hello(v) 2: astore_0 3: ldc #23 // String world(v) 5: astore_1 6: ldc #24 // String hello(c) 8: astore_2 9: ldc #25 // String world(c) 11: astore_3 12: aload_0 13: aload_1 14: invokedynamic #29, 0 // InvokeDynamic #6:makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; 19: astore 4 21: return
public static void checkJavacOptimizationSB(); Code: 0: ldc #23 // String hello(v) 2: astore_0 3: ldc #24 // String world(v) 5: astore_1 6: ldc #25 // String hello(c) 8: astore_2 9: ldc #26 // String world(c) 11: astore_3 12: new #6 // class java/lang/StringBuilder 15: dup 16: invokespecial #7 // Method java/lang/StringBuilder."<init>":()V 19: astore 4 21: aload 4 23: ldc #30 // String hello(l) 25: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 28: pop 29: aload 4 31: ldc #31 // String world(l) 33: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 36: pop 37: aload 4 39: ldc #25 // String hello(c) 41: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 44: pop 45: aload 4 47: ldc #26 // String world(c) 49: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 52: pop 53: aload 4 55: aload_0 56: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 59: pop 60: aload 4 62: aload_1 63: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 66: pop 67: aload 4 69: invokevirtual #11 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 72: astore 5 74: return
なんと驚きプラス演算子のがコードがガツっと短いのです。Java8だと短いながらももうちょっとStringBuilderと近い感じだったのですが。 察するに用途が明確な分、StringBuilderよりもコンパイル時の最適化がしやすいのかもですね。 ちなもにこの例では判りづらいですがリテラルと定数の結合はjavac時点で対応されるのでゼロコストです。
実行時編
JITの効くJavaでコンパイル時のの結果だけ見てもさほど意味はない気がしますのでちゃんと測ります。 今回マイクロベンチは下記の手法で行なっています。
文字列結合を大量に繰り返すケース
これはSQLを組立実行とか同じ結合処理をループの中で何度も繰り返すケースですね。 文字列結合をするパターンとしては一番多いのでは無いでしょうか?
public static String concatStringByPlus(int count) { String x = "hello"; String y = " "; String z = "world"; String s = ""; for (int i = 0; i < count; i++) { s = x + y + z; } return s; } public static String concatStringBySB(int count) { String x = "hello"; String y = " "; String z = "world"; String s = ""; for (int i = 0; i < count; i++) { StringBuilder sb = new StringBuilder(); sb.append(x); sb.append(y); sb.append(z); s = sb.toString(); } return s; }
実行結果は以下の通り。
## 1回 testcase:Concat string by 'plus' loop:0 arguments:[1000000] response(ms):190 gc_count:0 gc_total(ms):0 gc_max(ms):0 gc_avg(ms):0.000000 testcase:Concat string by 'StringBuilder' loop:0 arguments:[1000000] response(ms):89 gc_count:0 gc_total(ms):0 gc_max(ms):0 gc_avg(ms):0.000000 testcase:Concat string by 'plus' loop:0 arguments:[1000000000] response(ms):31890 gc_count:327 gc_total(ms):49 gc_max(ms):6 gc_avg(ms):0.149847 testcase:Concat string by 'StringBuilder' loop:0 arguments:[1000000000] response(ms):41450 gc_count:510 gc_total(ms):14 gc_max(ms):2 gc_avg(ms):0.027451 testcase:Concat string by 'plus' loop:0 arguments:[2000000000] response(ms):21135 gc_count:559 gc_total(ms):5 gc_max(ms):1 gc_avg(ms):0.008945 testcase:Concat string by 'StringBuilder' loop:0 arguments:[2000000000] response(ms):29857 gc_count:825 gc_total(ms):16 gc_max(ms):1 gc_avg(ms):0.019394 ## 2回 testcase:Concat string by 'plus' loop:1 arguments:[1000000] response(ms):11 gc_count:0 gc_total(ms):0 gc_max(ms):0 gc_avg(ms):0.000000 testcase:Concat string by 'StringBuilder' loop:1 arguments:[1000000] response(ms):25 gc_count:0 gc_total(ms):0 gc_max(ms):0 gc_avg(ms):0.000000 testcase:Concat string by 'plus' loop:1 arguments:[1000000000] response(ms):10667 gc_count:261 gc_total(ms):16 gc_max(ms):11 gc_avg(ms):0.061303 testcase:Concat string by 'StringBuilder' loop:1 arguments:[1000000000] response(ms):15008 gc_count:411 gc_total(ms):8 gc_max(ms):1 gc_avg(ms):0.019465 testcase:Concat string by 'plus' loop:1 arguments:[2000000000] response(ms):21203 gc_count:526 gc_total(ms):11 gc_max(ms):2 gc_avg(ms):0.020913 testcase:Concat string by 'StringBuilder' loop:1 arguments:[2000000000] response(ms):29767 gc_count:857 gc_total(ms):19 gc_max(ms):1 gc_avg(ms):0.022170 ## 3回 testcase:Concat string by 'plus' loop:2 arguments:[1000000] response(ms):13 gc_count:0 gc_total(ms):0 gc_max(ms):0 gc_avg(ms):0.000000 testcase:Concat string by 'StringBuilder' loop:2 arguments:[1000000] response(ms):16 gc_count:0 gc_total(ms):0 gc_max(ms):0 gc_avg(ms):0.000000 testcase:Concat string by 'plus' loop:2 arguments:[1000000000] response(ms):10555 gc_count:264 gc_total(ms):2 gc_max(ms):1 gc_avg(ms):0.007576 testcase:Concat string by 'StringBuilder' loop:2 arguments:[1000000000] response(ms):14941 gc_count:412 gc_total(ms):10 gc_max(ms):2 gc_avg(ms):0.024272 testcase:Concat string by 'plus' loop:2 arguments:[2000000000] response(ms):21221 gc_count:525 gc_total(ms):11 gc_max(ms):1 gc_avg(ms):0.020952 testcase:Concat string by 'StringBuilder' loop:2 arguments:[2000000000] response(ms):29832 gc_count:855 gc_total(ms):24 gc_max(ms):1 gc_avg(ms):0.028070
1回目はwarm upなので無視するとしてですが2回目いこうに注目しても実は+演算子の方が高速です。 StringBuilderはclearとか無いので毎回初期化するでしょうしそのコストもあるでしょうね。 何れにしても100万回のループで3ms、20億回のループで8秒程度なのでよほど変なバッチを組んだ時以外は気にする事はないでしょう。 リアルタイムなら秒レベルの差は気になりますがで億回ループ処理で数秒なら誤差です。
大量の文字列を一つに連結するケース
先ほどの例はStringBuildernの使い勝手としてはフェアじゃないので違う例も試します。
大量の文字列を1つの文字列に結合するパターンです。
public static String concatAllStringByPlus(int count) { String x = "hello"; String y = " "; String z = "world"; String s = ""; for (int i = 0; i < count; i++) { s += x + y + z; } return s; } public static String concatAllStringBySB(int count) { String x = "hello"; String y = " "; String z = "world"; StringBuilder sb = new StringBuilder(); for (int i = 0; i < count; i++) { sb.append(x); sb.append(y); sb.append(z); } return sb.toString(); }
実行結果
## 1回目 testcase:Concat all string by 'plus' loop:0 arguments:[10000] response(ms):716 gc_count:0 gc_total(ms):0 gc_max(ms):0 gc_avg(ms):0.000000 testcase:Concat all string by 'StringBuilder' loop:0 arguments:[10000] response(ms):1 gc_count:0 gc_total(ms):0 gc_max(ms):0 gc_avg(ms):0.000000 testcase:Concat all string by 'plus' loop:0 arguments:[100000] response(ms):44470 gc_count:1164 gc_total(ms):205 gc_max(ms):44 gc_avg(ms):0.176117 testcase:Concat all string by 'StringBuilder' loop:0 arguments:[100000] response(ms):9 gc_count:0 gc_total(ms):0 gc_max(ms):0 gc_avg(ms):0.000000 testcase:Concat all string by 'plus' loop:0 arguments:[200000] response(ms):60044 gc_count:1263 gc_total(ms):119 gc_max(ms):10 gc_avg(ms):0.094220 testcase:Concat all string by 'StringBuilder' loop:0 arguments:[200000] response(ms):8 gc_count:0 gc_total(ms):0 gc_max(ms):0 gc_avg(ms):0.000000 ## 2回目 testcase:Concat all string by 'plus' loop:1 arguments:[10000] response(ms):93 gc_count:0 gc_total(ms):0 gc_max(ms):0 gc_avg(ms):0.000000 testcase:Concat all string by 'StringBuilder' loop:1 arguments:[10000] response(ms):0 gc_count:0 gc_total(ms):0 gc_max(ms):0 gc_avg(ms):0.000000 testcase:Concat all string by 'plus' loop:1 arguments:[100000] response(ms):12685 gc_count:262 gc_total(ms):31 gc_max(ms):8 gc_avg(ms):0.118321 testcase:Concat all string by 'StringBuilder' loop:1 arguments:[100000] response(ms):2 gc_count:0 gc_total(ms):0 gc_max(ms):0 gc_avg(ms):0.000000 testcase:Concat all string by 'plus' loop:1 arguments:[200000] response(ms):61873 gc_count:1249 gc_total(ms):108 gc_max(ms):11 gc_avg(ms):0.086469 testcase:Concat all string by 'StringBuilder' loop:1 arguments:[200000] response(ms):3 gc_count:0 gc_total(ms):0 gc_max(ms):0 gc_avg(ms):0.000000 ## 3回目 testcase:Concat all string by 'plus' loop:2 arguments:[10000] response(ms):111 gc_count:0 gc_total(ms):0 gc_max(ms):0 gc_avg(ms):0.000000 testcase:Concat all string by 'StringBuilder' loop:2 arguments:[10000] response(ms):0 gc_count:0 gc_total(ms):0 gc_max(ms):0 gc_avg(ms):0.000000 testcase:Concat all string by 'plus' loop:2 arguments:[100000] response(ms):12227 gc_count:269 gc_total(ms):13 gc_max(ms):2 gc_avg(ms):0.048327 testcase:Concat all string by 'StringBuilder' loop:2 arguments:[100000] response(ms):1 gc_count:0 gc_total(ms):0 gc_max(ms):0 gc_avg(ms):0.000000 testcase:Concat all string by 'plus' loop:2 arguments:[200000] response(ms):56216 gc_count:1262 gc_total(ms):29 gc_max(ms):1 gc_avg(ms):0.022979 testcase:Concat all string by 'StringBuilder' loop:2 arguments:[200000] response(ms):2 gc_count:0 gc_total(ms):0 gc_max(ms):0 gc_avg(ms):0.000000
圧倒的速度差!
プラス演算子だと1万件でも100ms程度でそこからどんどん遅くなりますが、StringBuilderは速度が安定しています。
なので「1万個以上のStringを結合するときはStringBuilderが確実に良い」です。
でも、「そんな処理普段書きますか? 1万個の文字列結合ですよ???」
少なくとも100件とか1000件のループで気にする必要はありません。文字列結合が大量に発生するケースも先のSQLのようにトータル件数はともかく個別の変数単位の件数はしれてるものです。
まとめ
早過ぎる最適化は技術的負債の一つです。
もちろん、LinedListをインデックスアクセスするとか致命的に向いてない処理もありますが、基本的にはインターフェースに対して抽象度の高い操作をすれば、ライブラリやコンパイラで弄る余地があるので最適化されます。
これはJavaに限らず多くの言語の基本的な特性なので、マイクロベンチマークに拘った過度な最適化は基本的にはせず、遅くなったらプロファイルを適切にとったうえでピンポイントに修正すればいいのです。
若手のうちはつい最適化に日ごろから気を使いすぎてしまうので「統的な最適化テクニックもJITの前には無力だ。人間が頑張るのは最後で良い」という事を伝えたくてこの記事を書いてみました。
それではHappy Hacking!
テックポエマーの10min IT News! - 2020/02/18 - 「未踏/デブサミ/新リリース」 を公開しました
広告の代わりにサイトを閲覧するだけでコンテンツ利用料を払える「マインペイ」を作ってみた
はじめに
皆さん、Web広告は好きですか?
私はあまり好きではありません。Web広告から買いに行ったことは基本無いのでCPUとパケットと画面を消費してるだけの印象が強いです。
とはいえ、Web広告があるから色々なサービスが無料で利用できるのも事実。Googleなんてあの巨額の売上のほぼ全ては広告ですしね。
という分けで、別の仕組みが作れないかという事で「マインペイ」というサービスを取り急ぎ超プロトタイプですが作ってみました。
これは地球外知的生命体を探索する「SETI@home」や小児がん治療薬の探索プロジェクトの「Smash Childhood Cancer 」のようなグリッドコンピューティングのようにパソコンの空リソースを使う仕組みです。 つまり閲覧中のパソコンやスマホの空いてるリソースを別の計算に充てます。
実際に予め負荷のかかり過ぎないように決められたリソースが仮想通貨の発掘に使われこれが広告収入の代わりになるという仕組みです。
現在はまだα版なので実際の発掘はしていませんが、広告の代わりに下記の計算を行っている事を表示するバナーを表示します。これで他の広告が無くなるなら画面は大分すっきりしますね!
以下にサンプルページがあるのでアクセスしてみてください。
https://storage.googleapis.com/mine-pay-121y4672/index.html
CPUをただ閲覧しているよりは使いますが、広告と違ってパケットをほとんど消費することが無く、CPUを利用する事による電力消費も邪魔にならない程度に抑えていく予定です。
使い方 - 閲覧者編
閲覧者は特に何もする必要はありません。単に閲覧されるだけで自動的にサイト運営料としてマイニングが行われます。
JSで動作しているので見てるページから移動するまたはタブを閉じればそれ以上発掘される事はありません。また、JSをオフにする事で実行自体を止めることもできます。
使い方 - サイト運営者編
将来的にはSaaSっぽい仕組みになると思いますが、現状はとりあえず下記のソースコードをcloneして適当なところに配置してください。
設置に必要なコードは下記だけなので、これを任意のHTMLの箇所に埋め込めば大丈夫です。
<img id="minepay" src="https://自分のドメイン/mine-pay.png" /> <script src="https://自分のドメイン/minepay.js"></script>
マインペイ利用のメリット
- 広告と違ってパケットをあまり使用しない。画面を占有もしない。不快な広告を見る事も減る
- 長時間の滞在が運営者の直接的なベネフィットになるので良質なコンテンツが増える
- 収益化方法の選択肢が増える
TODO
とりあえず雰囲気だけ作ったα版というかほぼモックなので以下の事をしていきたいです。
- ちゃんと仮想通貨を発掘する
- センスのあるバナー
- 負荷の制御
- CPU負荷状況をバナー等にアニメーション表示
- 実行を止める機能
- 過剰な負荷を不正に与えないようにする仕組
まとめ
目指してるところはCoinhiveと概ね同じものですが、広告料の代わりという事を強調出来るよう投げ銭的なイメージの○○ペイという名前にしてみました。
また、利用していることを明示せずにウイルス扱いされてしまってはいけないので、利用していることを明示できるようにバナーを付けるのを基本にして、将来的にはバナー上に利用してるリソース状況とかも出たり、広告のオフみたいに無効にする機能が付いてれば良いんじゃないかと考えています。
実際にWeb広告の代替になる程の収益性がそもそもあるのかはかなり疑問があるのですが、広告やサブスクリプションとは違う収益も出るがあっても良いと思うのでとりあえず開発してみたいと思います。
それではHappy Hacking!
参考
テックポエマーの10min IT News! - 2020/02/09 - 「 月を穿つ、不老、Coinhive問題、ローカル5G、Dockerその他」 を公開しました
10分と言ったな? あれは、嘘だ。
と言わんばかりに今週も楽勝でオーバーしてしまいました。うーん、もう少しコンテンツを減らすべきか。。。 あと、タイトルをもうちょっとRebuild.fmみたいに一言ワードにまとめてたいんだけど、全編を通しての良いフレーズが思いつかない。