runc脆弱性に対応するためにうっかりECSからFargateにしました

こんにちは、ソーシャルゲーム事業部ゲーム技研チームの谷脇です。今日は一石n鳥の話、もしくは桶屋が儲かる話をします。

この記事はTech KAYAC Advent Calendar 2019 Migration Trackの2日目の記事です。1日目はMongoDBであるメリットが無くなってしまったのでMySQLに移行したはなしでした。

TL;DR

  • カヤック社内で内製の開発版スマホアプリを配るための配信プラットフォーム「alphawing」を開発・運用してます
  • alphawingはEC2上で動くAmazon ECS(以下ECS)で動いていました
  • コンテナランタイムruncでの脆弱性 CVE-2019-5736 がでてきたのでどうしようどうしようとなりました
  • そのあと様々な検討があり、えいやっとAWS Fargate(以下Fargate)に持っていきました

背景

ゲーム技研チームではソーシャルゲーム事業部内で使われる開発に便利なグッズを作ったり、運用が必要であれば運用を行うことをやっています。

OTAツール、alphawingもその一つです。自動ビルドされる開発版アプリのアップローダとしての機能、スマートフォンから閲覧してダウンロード及びインストールをする機能を持ったWebアプリです。

f:id:mackee_w:20191202120824p:plain

こういった、開発に便利な社内サービスを集積して運用するために、カヤックではAmazon ECSを使っています。細かい生活の知恵グッズを多数運用するときにもこういった本番コンテナサービスは便利です。

runc脆弱性 CVE-2019-5736

以下の記事が詳しいです。

runcの脆弱性情報(Important: CVE-2019-5736) - OSS脆弱性ブログ

要約すると、Dockerなどで使われるコンテナランタイムruncにホスト側rootでコマンド実行出来る脆弱性です。

そこそこ簡単に特権昇格できる脆弱性なのでさっさと対応してしまいたいところです。ゲーム技研チームで対応を検討し始めました。

terraform管理していたはずだがtfstateが失われたので...

alphawing自体は2015年頃から運用されているアプリケーションです。私も2代目ぐらいの開発担当者で、開発当初の状況はあまり知りません。とはいえ、昨年にアプリケーションコードを全面的に書き直して移行も成功したため、大体の構成の把握はできていると自負していました。それが間違いだったのですが。

runc脆弱性対応を行う手法としてまず検討したのは、正攻法ですが、コンテナインスタンスのAMIを脆弱性対応済みのものに入れ替える手法です。そして、社内で動き続けているサービスの性として、できるだけ停止時間を短くする必要もあります。そこで、別のコンテナインスタンスを新しく立ててECSクラスタに入れた上で、新しいインスタンスの方にコンテナを立てた上で、古いインスタンスを停止という方向に持っていく算段でした。

が、しかし...

  • 手で新しいEC2インスタンスを立てようとしたら古いインスタンスのタグにmanaged by terraformという文字列を見つける
  • terraformで使う状態管理ファイル.tfstateを探すがS3のバケットにもGitHubのリポジトリにもない

というわけで、インフラの管理方法は失われてしまっていたようです。

新しいAMIを起動するだけのはずなので、おそらくterraformも簡単に作り直せるはずです。ただ、そっちにいくのではなく...

f:id:mackee_w:20191202121305p:plain

と、SREチームの組長に背中を押されてFargateに切り替えるぞとなりました。

理由として、AWS Fargateがある今、このサービスでEC2を使うECS構成を作るメリットが見出だせなかったことがあります。

ECSとFargateの違い

次のスライドの始めの方に詳しいことが図解されています。

第1回 AWS Fargate かんたんデプロイ選手権 #AWSDevDay

ECSを使う場合は、ECSのコンテナの管理の他にEC2インスタンスの管理も必要です。EC2インスタンスの管理の中には今回のような脆弱性対応も含まれます。

一方、AWS Fargateを使う場合は、EC2インスタンスに当たるような実行環境は使う側からは見えなくなり、AWSの管理に委ねられます。いわゆるフルマネージドサービスと呼ばれるものです。そして、AWSが行う「管理」とは、脆弱性に対応するためのパッチ当ても含まれます。

その違いを、今回の脆弱性に対応する人向けにAWSから出された文章から抜き出してみます。

ソース

Amazon Elastic Container Service (Amazon ECS)

Amazon ECS Optimized AMIs, including the Amazon Linux AMI, the Amazon Linux 2 AMI, and the GPU-Optimized AMI, are available now. As a general security best practice, we recommend that ECS customers update their configurations to launch new container instances from the latest AMI version. Customers should replace existing container instances with the new AMI version to address the issue described above. Instructions to replace existing container instances can be found in the ECS documentation for the Amazon Linux AMI, the Amazon Linux 2 AMI, and the GPU-Optimized AMI.

ECSの場合は使っているAMIを入れ替えてねと書かれています。

AWS Fargate

Customers running Fargate Services should call UpdateService with "--force-new-deployment" enabled to launch all new Tasks on the latest Platform Version 1.3. Customers running standalone tasks should terminate existing tasks, and re-launch using the latest version. Specific instructions can be found in the Fargate update documentation.

AWS Fargateの場合は、aws-cliで--force-new-deploymentをつけてserviceを更新すれば、勝手にアップデートされるよと書かれています。alphawingであればecspressoを使っているので、単にデプロイを行えば同じような効果が得られます。

というわけで、ある程度マネージドなサービス(ECS)と、フルマネージドサービス(Fargate)の違いが実感できます。

今回の脆弱性と同じようなことがあっても、Fargateに移行しておけば簡単に対応できることが期待できます。

移行作業

ecspressoを使用していたので、ecspressoで使う設定用JSON書き換えと、ALBからの向き先入れ替えが主な作業になります。Fargateが動く新しいECSのServiceを作って並列動作させた上で、古いServiceを停止して無停止で移行できました。

  • service definition
    • launchTypeFARGATE に書き換え
  • task definition
    • networkModebridge から awsvpc に書き換え
    • Fargateはawsvpcしかサポートしていないため
  • ALB
    • Fargate用のtarget groupを作ってALBに登録

作業を始めてからだいたい2時間ぐらいで移行が完了しました。

f:id:mackee_w:20191202121510p:plain

まとめ

  • フルマネージドサービスに移行すると脆弱性対応もだいぶ簡単になるぞ
  • フルマネージドサービスになると、場合によってはterraformみたいなので管理する物が減るぞ
    • 結果的にtfstate行方不明みたいなのがなくなる...?
  • フルマネージドサービスでガンガン使うなどし、世間で一般的かつググってでてくる技術でサービス作ったほうが管理コスト安いのではと思う今日このごろです

明日12/3は分析番長マシオカこと池田の「redash v1.0.3 から v8.0.0 までアップグレードするついでにECS化した話」です。バージョン番号が8倍になる話をするそうです。お楽しみに。

低価格スマホでコンピュートシェーダ

この記事はTech KAYAC Advent Calendar 2019の2日目の記事です。

こんにちは。技術部平山です。

この記事では、「低価格帯の機械で状況を良くするためにコンピュートシェーダが使えないか?」 という考えを、簡単なサンプルで検証してみました。

結論

最初に結論を申し上げます。微妙。

まず数字を出しましょう。数字はあるテストでのフレームレート(fps, frames per second)です。

データサイズ CPU ComputeShader
1024 52 55
2048 15 17
4096 4 5

CPUからComputeShaderにすると、非常に負荷が大きい局面では25%くらい速くなるかな?というくらいです。 負荷が低い局面では差が小さくなってしまいます。 測定に用いた機械は、SharpのAndroid One S3、 チップはmsm8937(Snapdragon430)で、 2018年後半以降に登場した、低価格帯(3万円台以下)向けのチップです。

ついでに、たまたまそこにあった高級機種を見てみましょう。Galaxy S9+(SCV39)です。

データサイズ CPU ComputeShader
4096 16.5 60

高い機械は速いなあ、と思いますね。つくづく。 数字はfpsなので60で天井ですから、本当は100出るかもしれませんし、200出るかもしれません。 最低でも、CPUからGPUに持っていけば4倍近くの性能になる、ということがわかります。

さらに、MacBook Pro 13'' 2018でも同じ測定をしてみました。

データサイズ CPU ComputeShader
4096 18 60

これもGPU側は60より大きいかもしれませんが、 最低でも差が3倍近くある、ということです。

それにしても、PCとGalaxy S9+でCPU性能が大差ない、 というのはちょっと疑わしいですね。何か問題があるかもしれませんので、 数字は真に受けないようにおねがいいたします。 今回の実装についての懸念点、改良すべき点については後にまとめます。

測定方法

GitHubにプロジェクトを置いておきました

まず、ある大きさのRGBA32テクスチャにランダムな色を入れておき、 これを32bitの整数とみなして乱数アルゴリズムであるXorShiftで変換します。こんな奴です。

static void XorShift32(ref Color32 c)
{
    uint x = ((uint)c.a << 24) | ((uint)c.r << 16) | ((uint)c.g << 8) | c.r;
    x ^= x << 13;
    x ^= x >> 17;
    x ^= x << 5;
    c.a = (byte)(x >> 24);
    c.r = (byte)((x >> 16) & 0xff);
    c.g = (byte)((x >> 8) & 0xff);
    c.b = (byte)(x & 0xff);
}

RGBA結合して32bit整数をつくり、謎の計算(13,17,5が出てくるところ)をして、 またRGBAに戻しています。32bit整数1チャネルのテクスチャフォーマットを使えば こんな必要はないのですが、どの機械でも動くのかがわからないので、確実に動くRGBA32にしておきました。

1024x1024のテクスチャであれば、100万個ほどの乱数の系列ができ、 これを独立に更新していく、という計算になります。 各要素間は完全に独立なので、GPU向きの計算と言えます。

ちなみに、画面はこんなです。

f:id:hirasho0:20191128160538p:plain

この例だと、解像度は64x64なのでツブツブ模様がよく見えますね。 「GPU」のチェックボックスはoffなのでCPU計算しており、 フレームレート(FPS)は60.4、「Th: 3/4」とあるのは、 CPU計算時のスレッド数が3、Unityが返したプロセッサ数が4、という意味です。 スレッドをいくつ用意した時が一番速いかは、 案外やってみないとわかりません。なのでスライダーを用意しておきました。

ComputeShaderの場合

今回のComputeShaderは、先程のXorShiftの処理をGPUに移植しただけです。 同じ計算をGPUを使って行います。

今回はAndroidしか試しておらず、APIはVulkanのみです。 iOSは試していませんが、たぶんiPhone5S以降であれば動くのでしょう。 実装は後で紹介します。

測定値の詳細

もう一度データを載せます。

データサイズ CPU ComputeShader
1024 52 55
2048 15 17
4096 4 5

毎フレーム、2048x2048の各ピクセルについて、XorShiftで更新をかけると、 CPUではフレームレートが15fps、ComputeShader使用時には17fpsだった、ということです。 4096x4096にすれば計算量は4倍になりますので、性能が1/4に落ちることが想像されますが、そこまでは落ちません。 これは測定方法に問題があることと、フレームごとに発生するそれ以外の処理によるオーバーヘッドがあるからです。

測定方法の問題というのは、VSYNC、つまり垂直同期のことで、 秒間60fps以上は出ず、秒間fpsを保てなくなるとガクンと30fpsまで落ちてしまう、 という特性によります。きちんと測るなら、かつてのKonchiBench で行ったような手続きが必要でしょうが、今回は粗くわかれば良いとして、真面目な測定を作りませんでした。

CPU側の解釈

さて、このデータはsnapdragon430のデータでして、CPUは8コアです。 適切なスレッド数で測らないとCPUの性能を引き出せずGPUが有利になってしまいますから、 調べておきましょう。解像度2048での測定です。

スレッド数 fps
1 8
2 13
3 15
4 15
5 14
6 14

だいたい2スレッドで1スレッドの1.5倍くらいの性能が出て、 もう1スレッド追加すると微妙に速くなり、5を超えると逆に遅くなる、 という結果です。おそらく同時に動いているのは4コアなのでしょう。 忙しい時は速い方の4コアだけが動き、ヒマな時は遅くて電気を食わない4コアが動く、 という設計なのだと思われます。

なお、適正なコア数は計算の内容に左右され、 例えば1024x1024だともう少しスレッドが多い方が速かったりします。 熱や電池のかねあいもあるのでしょう。

ともかく、この測定結果を見る限り、「Nコア使うと性能がN倍」というのは所詮は理想でしかない、 ということが言えるかと思います。 今回はスレッドプールが自作なので、そこの出来が悪いのかもしれませんが、 少なくとも私は「スレッド数を倍にして性能が倍になった」というケースは見たことがありません。 1.5倍でも良い方です。

流行りのJobSystem なら高速なのかもしれませんが、試してみたところ、エディタとStandaloneビルドで 自作スレッドプールに比べて1/4くらいになってしまいました。 今回はGPUが主役ですので、これ以上の検証はしていません。

GPUとCPUの比について考える

今回の結果からは、 「安い機械であっても、GPUでできることはGPUでやる方が速くはなるようだ」 ということが言えます。ただし、その度合いはかなり残念なレベルです。 ComputeShaderの設定を変えたら倍速になる、なんてことはたぶんないでしょう。

さて、ここで根本的なことについて考えておきます。 「CPUでもできることをGPUにやらせることは正義か?」という事です。

描画処理、つまりシェーダの実行はGPUにしかできません。 描画で忙しいのであれば、多少遅くてもCPUでやってGPUを描画に専念させる方が、 全体としては品質が上がるでしょう。 PLAYSTATION3のような「CPU側にすごい性能の謎コアがたくさんあって、 こいつも描画に参加させた方が速い」という変態マシンもありましたが、たぶん例外です。 大抵のゲームでは描画が一番重要な計算になるので、 GPUにはできるだけ描画をさせるべきでしょう。

ただし、ゲームの種類によっては、CPUが忙しくてGPUが余っていることもありえます。 例えば群体シミュレーションが凄まじく重いゲーム、とかですね。 こういう場合にはCPUから仕事を奪って、 積極的にGPUにやらせる方が良い可能性があります。

そして、この天秤は、GPUとCPUの性能比によって変わってくるわけです。

仮にGPUがCPUの100倍速ければ、「よほどのことがない限りCPUではやらない」 というのが正義になります。GPUから少々描画処理をする時間を奪ったとしても、 それによって空くCPU時間が大きいので、追加でいろいろやれるようになります。 全体としては品質が上がるでしょう。

しかし、この比率が下がってくるにつれ、「それくらいならCPUでやればいいかな」 という判断になる率が上がってくるわけです。 古くはスキニング処理が よく話題になりました。

また、新しいGPUには「描画しながら片手間にComputeShaderを実行できる」 という器用な奴もいます(AsyncComputeという奴です)。 こういう機械は「テクスチャアクセスで忙しいけど計算器空いてるから計算しとくわ」 みたいなことができ、性能を有効活用できます。 そういうGPUが一般的になれば、よりComputeShaderを使いたくなってくるわけですね。 ただし、2019年11月現在、 これがUnityから使えるのはPlaystation4だけみたいです

ComputeShaderの実装

さて、実はほぼ初めてComputeShaderを書いたような人間ですので、 偉そうに説明はできませんが、紹介だけはしておきます。

#pragma kernel CSMain

Texture2D<float4> Source;
RWTexture2D<float4> Destination;

[numthreads(8,8,1)]
void CSMain (uint3 threadId : SV_DispatchThreadID)
{
    float4 c = Source[threadId.xy] * 255.0;
    c += 0.5;
    int4 i = (int4)c;
    uint x = (i.w << 24) | (i.x << 16) | (i.y << 8)| i.z;
    x ^= x << 13;
    x ^= x >> 17;
    x ^= x << 5;
    i.x = (x >> 16) & 0xff;
    i.y = (x >> 8) & 0xff;
    i.z = x & 0xff;
    i.w = (x >> 24) & 0xff;
    c = i / 255.0;
    Destination[threadId.xy] = c;
}

シェーダ側はこのようになっています。入力がTexture2DであるSource、 出力がRWTexture2DであるDestinationで、Sourceから読んだ値を加工して Destinationに流すだけのシェーダです。

計算本体

計算の本体はCPU側とほぼ同じですが、 「テクスチャから抜いた値の範囲が0から255でなく、0から1.0」 というのが違います。255を乗じて整数化する必要がありますが、 この時に注意が必要です。

0から255、を、0から1に変換する際には255で割るわけですが、 IEEE754な浮動小数点数は255で割ると誤差が出ます。 例えば255/255が0.99999になれば、ここに255を掛けても254.9999 みたいな感じになり、整数に切り捨てると254になってしまいます。 ですので、ここでは0.5を足してから切り捨てています。

整数のまま出てくるように指定できればこんな面倒はないのですが、 Unityのやり方がわからなかったのでこのようにしました。

インデクスの計算

次に問題になるのがインデクスです。Source[index]とかDestination[index] のように添字でアクセスする時に、何を書けばいいか、ということですね。

結論から言えば簡単で、SV_DispatchThreadIDのセマンティクスでもらった 引数のxyをそのまま渡せば終わりです。 SourceもDestinationも2Dテクスチャですので、添字の型は2次元整数型です。 なので、threadId.xyのように2次元ベクトルを書けます。

あとはSV_DispatchThreadIDって何なの?という話ですが、 結論から言えば、処理しているテクセルの座標になっています。 ちゃんと説明すると面倒くさいのですが、 「全部でテクセルの数と同じだけ、例えば1024x1024回呼ばれるように設定されていれば、 (0,0)から(1023,1023)までの全ての組み合わせが一回づつ出てきます。 だから、Source[index]で値を取って、Destination[index]に入れれば、 全テクセルを処理できるわけです。

ただし、そうなるためにはCPU側も合わせて見ないといけません。次で軽く説明します。

C#側初期化

まず初期化です。

renderTextures = new RenderTexture[2];
for (int i = 0; i < renderTextures.Length; i++)
{
    renderTextures[i] = new RenderTexture(size, size, 1, RenderTextureFormat.ARGB32);
    renderTextures[i].enableRandomWrite = true;
    renderTextures[i].Create();
}
Graphics.Blit(texture2d, renderTextures[1]);

まずシェーダの入出力になるRenderTextureを作ります。 入力と出力を同じにできるかどうかは機種によるので、2枚用意して、 フレームごとに役割を入れ換えます。 最初は0番を入力とし、加工して1番に出力し、 次のフレームでは、1番を入力として、0番に出力するわけです。

コンストラクトしたら、enableRnadomWrite をtrueにしてComputeShaderで書けるようにしてから、 明示的にCreate()します。 Graphics.Blit() したり、RenderTexture.active にセットしたりすれば、中で勝手にCreateが呼ばれるのですが、ComputeShaderの場合は そうできない事情があるようですね。とにかく必要なので呼びます。

なお、最後の行のBlitは、CPUで生成したノイズテクスチャであるtexture2dから、最初の入力になる 1番RenderTextureにコピーするものです。 これ以降、一切CPUにはデータを返さず、2枚のRenderTextureの間でデータの往復が行われるだけになります。

C#側毎フレーム

あとは毎フレームの処理です。

var kernelIndex = computeShader.FindKernel("CSMain");
computeShader.SetTexture(kernelIndex, "Source", renderTextures[1 - writeBufferIndex]);
computeShader.SetTexture(kernelIndex, "Destination", renderTextures[writeBufferIndex]);
uint sizeX, sizeY, sizeZ;
computeShader.GetKernelThreadGroupSizes(
    kernelIndex,
    out sizeX,
    out sizeY,
    out sizeZ);
computeShader.Dispatch(
    kernelIndex,
    size / (int)sizeX,
    size / (int)sizeY,
    1);
image.texture = renderTextures[writeBufferIndex]; // これ同期待たないとダメでしょ感
writeBufferIndex = 1 - writeBufferIndex;

ComputeShader オブジェクトはSerializeField か何かで受け取ります。ここではcomputeShaderという変数です。

次に、CoumputeShader.FindKernel() で使う関数のIDを取ります。

var kernelIndex = computeShader.FindKernel("CSMain");

今回は関数が1個しかないので0固定ですが、 いくつかある場合もあるでしょうから、名前から番号に変換するのが良い作法でしょう。

そうしたら、ComputeShader.SetTexture() で入力と出力のRenderTextureをセットします。

computeShader.SetTexture(kernelIndex, "Source", renderTextures[1 - writeBufferIndex]);
computeShader.SetTexture(kernelIndex, "Destination", renderTextures[writeBufferIndex]);

writeBufferIndexという変数が、 「今どっちが出力か」を覚えていて、0か1が入っています。

次に、ComputeShader.GetKernelThreadGroupSizes() で、先程シェーダの中に書いた[numthreads(8,8,1)] みたいな記述を取ってきます。

uint sizeX, sizeY, sizeZ;
computeShader.GetKernelThreadGroupSizes(
    kernelIndex,
    out sizeX,
    out sizeY,
    out sizeZ);

ここではsizeXに8、sizeYに8、sizeZに1が帰ってきますね。 これが実行の単位で、テクスチャの8x8の64画素分を1単位として実行する、という意味になります。 ComputeShader用語では「1グループあたり8x8=64スレッド」ですね。

そして次にいよいよ実行です。

computeShader.Dispatch(
    kernelIndex,
    size / (int)sizeX,
    size / (int)sizeY,
    1);

さっきの「実行単位」をいくつ実行するかを指定します。 今sizeが1024なら、sizeXは8なので割ると128、 sizeYも8なので128になりますね。 つまり、8x8の実行単位を、128x128回実行しますよ、ということになり、 これで全テクセルをカヴァーできるわけです。 ComputeShader用語では「128x128=16384スレッドグループを実行する」ですね。

こういうふうにやっておけば、シェーダの中ではSV_DispatchThreadID のセマンティクスを持つ変数のxyを添字にしてテクスチャにアクセスすることで、 全テクセルを順番に見ていくことができます。

もっと凝ったことをやりたくなったら、その時はその時ですね。 例えば、縦横1/2に縮小するなら、実行回数は元のテクセル数の1/4です。 シェーダの中では4回Sourceにアクセスして、一回Destinationにアクセスするような コードになるでしょう。SV_DispatchThreadIDからどうやってテクスチャの添字を作るか、 に関してはいろいろ詳しい記事がありますので、探してみてください。 ここでは扱いません。この記事では「そもそも使える速度が出るのか?」 に絞って考え、「何に使うか」は考えないことにしています。

そして最後に、表示用のrawImageで使うTextureを差し換えて、 writeBufferIndexを0から1、あるいは1から0へと切り換えます。

writeBufferIndex = 1 - writeBufferIndex;

余談ですが、「1から引けば0と1が交代する」ということに、 私はこの世界に入って3年以上気づきませんでした。3年経って自分で思いついたわけではなく、 たまたま他人のコードを見て知っただけです。その時の悔しさはひどいものでした。 それまではインクリメントして2になったら0に戻していたのです。 どんなに簡単なことでも、「思いつく」ということは難しいことなのです。 何もかもを自力で作ることはできないのだ、ということを強く意識した出来事でした。

考察もう一度

さて、結果についてもう少し深く考えます。

「だいぶGPUに向いた処理を選んだにも関わらず、期待したほど速くない」 ということを、どう捉えるかです。

私の現状の結論は、「高性能機での品質を重視するか、今後の展望を考えればComputeShaderはアリ。 だが、無理しなくても良さそう」 という感じでしょうか。 4096解像度での低価格機(Sharp Android One S3)と高級機(Galaxy S9+)を比べてみましょう。

機種 CPU ComputeShader
S3 4 5
Galaxy S9+ 16.5 60

CPUは4倍程度、GPUは小さく見積っても12倍は違います。 GPUで差が激しいということは、Galaxy S9+ではComputeShaderを使うことがより有利になる、 ということです。

ただ、「低価格帯の機種であっても、CPUコアを総動員するよりはGPUにやらせた方が速い」 ということは言えますし、 慣れてしまえばCPUで書くよりGPUで書く方が楽、というケースもあります。 今回はスレッドプールを自作して、自力でスレッド数を決めて割り振りまでしていますから、 結構面倒なのです。その点GPUなら割り振りのことは考えずに済みますし、 計算の本体はアセットとして後からロードしたり配信したりできます。

コンピュートシェーダに回せそうな計算は一旦回してみる、 というのも良いかもしれません。その上で、 計算量を低価格機種と高価格機種で変えてあげれば良いのでしょう。 例えばパーティクルの計算に使うのであれば、その数を性能によって変えれば 良いように思います。

それに、いかに進歩が遅い低価格帯であっても、CPUが高速化する速度よりは、GPUが高速化する速度の方が まだマシですから、「今後GPUとCPUの性能差は開いていく。今からComputeShaderに慣れておくべき」 と言うことはできます。

とはいえ、無理して今やらなくてもいいな、という感じはあるわけですね。

Graphics APIについて

「ComputeShader必須」としてしまうと動く機械を狭めてしまう、という問題もあります。

Vulkanが動く機械が必要になり、OpenGLES3.0までの機械はダメです。 Androidの配信ダッシュボード を見ると、今動いている機械のOpenGL/Vulkan対応比率がわかるのですが、 3.0以下の機械がまだ半分くらいいます。 「ComputeShader必須」と言ってしまった瞬間に、世界にあるAndroidの半分が対象外になるわけです。

ちなみに、私が長らくお世話になった京セラS2も、vulkan非対応なので動きません。 今ComputeShaderを使うならば、非対応機種向けのケアは必要でしょう。

まとめ

案外遅かったですね。残念です。

このためにわざと「Computeが動く範囲でできるだけ性能が低そうな」 スマホを買ったのですが、期待した結果にはなりませんでした。

もちろん、実装が悪くて良いデータが出なかった、という可能性はあります。

  • フレームレートを評価値にしてしまったので、オーバーヘッドが大きい上に、60で天井になってしまう
  • 自作スレッドプールの実装が良くない可能性がある
  • ComputeShaderのスレッド/スレッドグループ設定に改良の余地があるかもしれない
  • 整数演算主体なので、GPUによっては不利があるのかもしれない
  • 計算が短いので、オーバーヘッドが多く出て本当の性能が見えにくいかもしれない。

などなど、多数の可能性が考えられます。

しかし、そんなことを吹き飛ばすほど速い、例えば10倍くらいの差が出ることを 期待していましたので、 そんな改造をして多少改善したとしても、どうでも良いことです。 1年は放っておいていいかな、という気分でおります。

おまけ

記事を書いた後に、測定に協力してくださった方が何人かいるので、そのデータも貼っておきます。 高級品が多いので、解像度は全て4096です。 比較用に、Sharp S3の結果も並べておきます。

機種 発売年 チップ CPU ComputeShader
Pixel3 2018 Snapdragon845 14.5 60.5
XPERIA XZ preium(SO-04J) 2017 Snapdragon835 10 56
Essential Phone(PH-1) 2017 Snapdragon835 10.5 47
XPERIA XZ(SO-01J) 2016 Snapdragon820 6-7 20
Nexus 6P 2015 Snapdragon810 0.9 9.5
Sharp S3 2018 Snapdragon430 4 5

測定は非常な誤差が伴うので、参考程度と考えてください。 例えばXPERIA XZで20fpsは低すぎる気がします。 また、Nexus6PのCPU計算が低いのも何か事情がありそうです。

それにしても、皆さん高級品ばかりですね。 安い機械と高い機械のGPU性能の絶望的な差がよくわかります。 やっぱり私くらいは低価格帯の機械使っておかないとダメだなという 思いを新たにいたしました。

...私も自分でスマホ向けのゲーム作ってなかったら 躊躇なく10万払って高級品買うんですよ? 貧乏なわけじゃないのです。