syghの新フラグメント置き場

プログラミングTipsやコード断片の保管場所です。お絵描きもときどき載せます。

VulkanシェーダーでSub-group命令を使う

NVIDIAはKepler (Compute Capability 3.0) 世代のハードウェアにおいて、Warp Shuffle命令を実装しました。WarpシャッフルはCUDAから組み込み関数 (intrinsic function) の形で利用できるSIMD命令の一種で、Warpと呼ばれるスレッドグループ内での並列データ交換を実現する機能です。NVIDIAGPUでは、ひとつのWarpは32個のハードウェアスレッドからなり、またWarp内の各スレッドはすべて同じ命令を実行します*1。つまり、ひとつのWarp内のスレッド群はすべて同期して並列動作する*2ため、Warpシャッフルを使うことで、データ列の総和を求めるリダクション演算などを、共有メモリを使うよりも高速に並列実行することができます。

Warpシャッフルに関しては、以前「CUDA Warpシャッフル命令のエミュレーション」という記事を書きましたが、CUDAの共有メモリに関する知識があれば、Warpシャッフルがいかに便利で簡潔な機能であるかを理解できると思います。

WarpシャッフルのようなSIMD命令は、汎用的な計算(GPGPU)だけでなく、グラフィックスのポストエフェクト処理などの高速化にも非常に有用なのですが、APIの停滞のせいであまり標準化が進んでいませんでした。NVIDIAWarpシャッフル命令をHLSL/GLSLから利用できるベンダー拡張を提供していますが、当然NVIDIA環境でしか使えないという制約があります。

Warpシャッフル相当の機能はまた、OpenCL 2.0の拡張機能cl_khr_subgroupsおよびOpenCL 2.1のコア機能として策定されていますが、2018年3月現在、OpenCLの実装はどのベンダーも停滞しており、OpenCLには期待できない状況です。一方、OpenGLにもARB拡張GL_ARB_shader_ballotとして存在しており、OpenCL同様にサブグループ (Sub-groupあるいはSubgroup) と呼ばれています。GL_ARB_shader_ballotはCUDAのWarpシャッフルのサブセットであるものの、GLSLを使えばOpenGLだけでなくVulkanからも基本的なサブグループの機能を利用できます。

Vulkanの学習を兼ねて、コンピュートシェーダーにてサブグループ命令を利用するサンプルコードを書いてみました。実行にはVulkan 1.0とVK_EXT_shader_subgroup_ballot拡張をサポートする環境が必要となります。NVIDIAハードウェアの場合、Kepler世代以降であれば拡張をサポートしているはずです。サンプルはWindows環境向けですが、ウィンドウシステムへのアクセスを必要としないオフスクリーンのコンピュート機能だけを使っているため、他の環境に移植するのは比較的簡単だと思います*3

以前書いた記事「Vulkan SDK付属のGLSLコンパイラー」では、Vulkan SDK付属のシェーダーコンパイラーがあまりに未完成なことを嘆いていましたが、その後着々と進化を続け、ほぼ使えるレベルに達したようです。
なお、今回はホストコードの記述量を減らすため、C++向けのラッパー (vulkan.hpp) を使っています。もともとこのラッパーはNVIDIAから寄贈されたものをベースに公式化されたらしいのですが、設計不足な点が散見されたり、一部の関数テンプレートのコンパイルが通らないというひどいバグが混入していたりと、まるでテストされていないことがありありとうかがえます。はっきり言ってプロダクションコードに使うには厳しい印象です。本気でVulkanの導入を検討する場合は、ぶっちゃけこのラッパーは使わないほうがいいでしょう。

なお、AMDAnvil*4という上位レベルのVulkanベースフレームワークを公開していますが、いつも仕事が中途半端なAMDが、この先ちゃんと継続的にメンテしてくれるのかどうか不安です。AMD APP SDKも、2015年8月にリリースされたv3.0で放置されたままという体たらくなので、正直期待できません。

Vulkan 1.1とSub-group命令

先日Vulkanのアップデートとしてv1.1が発表されましたが、Direct3D 12が先行していた、マルチGPUの活用やシェーダーモデル (SM) 6.0に対応する形で、重要な新機能が追加されているようです。サブグループ演算 (subgroup operation) に関しても、既存のOpenGL互換のARB拡張とは別のKHR拡張 (GL_KHR_shader_subgroup) およびホストAPIの正式仕様が策定され、CUDAと同等あるいはそれ以上の機能を獲得したようです。

なお、DirectXにおけるSM 6.0の目玉とも言えるのが、Sub-groupに相当するWave命令セットです。Waveは機能レベル12_0以上でないと使えないとか、そもそもDirect3D 12自体がWindows 10でないと使えないとかいう制約があり、個人的にはプラットフォーム制約の少ないVulkanのほうが有望だと思うのですが、いずれにせよこれでようやくCUDA以外でも高速なSIMD命令が標準的に利用できる土台が整い始めました。
とはいえ、HLSLもGLSLも、CUDA C++に比べるとプログラマビリティは遥かに劣ります。個人的にはC++のテンプレートが使えないのが一番痛いです。OpenCLは2.2でようやくカーネル記述言語にC++を採用しましたが、PCプラットフォームではまだどのベンダーもOpenCL 2.2をサポートしていません。純粋なGPGPU用途であれば、結局CUDAの地位は安泰であり、ただ単にNVIDIAハードウェアの能力を最大限活用できるAPIというだけでなく、地球上でもっとも生産性の高いGPGPU開発基盤であることは疑いの余地がありません。VulkanはせっかくSPIR-Vという強力なシェーダー中間言語を持って生まれたのだから、今後はシェーダーコンパイラーのさらなる飛躍と、Metalシェーディング言語のような新しいC++ベースのフロントエンドのサポートに期待したいところです。

*1:AMD GPUにおいてもWarp同様の概念としてWavefrontが存在します。Wavefrontは64個のハードウェアスレッドからなります。

*2:GPUコアの1サイクルでWarp内のすべてのスレッドが同時に駆動するとは限りませんが、論理的には必ず同期するようになっています。

*3:OpenGLでは仮にコンピュートを利用するだけであっても、GLコンテキストの作成にウィンドウハンドルを必要とし、しかも実行デバイスを明示的に選択する機能すら標準で提供されていませんでしたが、Vulkanは遥かに洗練されており、OpenCLやDirectComputeと似たような使い方ができるようになっています。

*4:Anvilとは「金床」(かなとこ)という意味ですが、UbisoftのAnvil Engineとカブっているので、できれば別の名称にして欲しかったです。

C/C++の#warning

コンパイルエラーを意図的に発生させる#errorプリプロセッサディレクティブに関しては、お馴染みの#includeや#defineなどと同様に言語仕様として標準化されているようで、おそらくすべてのC/C++処理系でサポートされています。

#ifdef __cplusplus
#error This code does not support C++!!
#endif

問題は警告のほうです。C/C++でユーザー定義の警告を発生させる方法は標準化されていません。
gccには#warningディレクティブが存在するのですが、Visual C++には存在しません。Clangにも存在しないようです。
#warningの実装は過去に提案されていたりするのですが、実現には至っていないようです。

#pragma messageはたいていの処理系でサポートされているのですが、単にメッセージを出力するだけで、警告にはならず、強制力がありません。

#ifdef NDEBUG
#pragma message("This code is compiled when release mode.")
#endif

また、#pragma messageでファイル名と行番号を出力しようとすると、__FILE__マクロと__LINE__マクロおよびトークンの文字列化と結合を駆使せねばならず、移植性のあるコードを記述するのが大変になります。

C#

一方、C#では#warningがちゃんと標準化されています。当然#errorもあります。

やはり本来は処理系ごとにどうにかするべき問題ではなく、C#のように言語仕様として標準化してほしい機能です。


え、Javaですか? そんな貧弱な言語は知りませんね。窓から投げ捨てましょう。

※2023-02-19追記:
C23およびC++23で、ついに#warningが標準化されるようです。

C#の登場から20年以上の歳月を費やし、ようやくその必要性を認めたようです。相変わらず遅すぎる。

Windowsの画面キャプチャ取得方法

Windowsにおいて、画面のキャプチャ(スクリーンショット)を取得する方法はいくつかあるのですが、下記の標準機能は遥か昔(たぶんWindows 95あたり)から搭載されています。いずれもWindowsユーザーであれば誰もが知っていないとおかしいレベルの、初歩中の初歩です。

PrintScreenキー デスクトップ全体を1つの画像として取得します。マルチディスプレイ(マルチモニター)環境の場合、合成された1つの画像となります。
Alt+PrintScreenキー アクティブなウィンドウのみのスクリーンショットを取得します。ただしMDI子ウィンドウのみのキャプチャには対応していません。アクティブでない別のウィンドウがかぶさっている場合、そのウィンドウも映り込みます。

PrintScreenキーで取得したデータは、不可視のクリップボード(システム全体で共有するグローバルメモリ領域)にビットマップ画像データとして保存されます。オンメモリなので、当然Windowsをシャットダウン/再起動すると消失する揮発性のデータです。クリップボードに保存されたビットマップ画像データをファイルとして保存する場合は、お好みのペイントツールにてキャンバスに貼り付けて、所望のフォーマットで保存します。Windowsに標準搭載されているMS Paint(ペイント)を使う場合は、あらかじめキャンバスサイズを小さくしておけば、ペースト時に画像サイズに合わせてキャンバスを自動拡張してくれます。MS Word/Excel/PowerPointなど、Office系のソフトも直接クリップボード経由でドキュメント内に画像を貼り付けることができますが、設定によってはファイル埋め込みの際に、勝手に画像の解像度を落としてしまうこともあるので注意が必要です。サードパーティ製のアプリケーションを自由にインストールできる自宅のプライベート端末では、起動と動作が軽快なIrfanViewを使ってクリップボードデータをファイル保存することが多いです。

ちなみにスクリーンショットを画像ファイルとして保存するときは、通例可逆圧縮PNGフォーマットを使います。非可逆圧縮JPEGは主に写真向けの圧縮フォーマットであり、画面スクリーンショットの保存には向いていません(色変化の激しい部分でモスキートノイズが目立ったり、PNGよりもファイルサイズが増加したりします)。

なお、スクリーンショット画像にはマウスなどポインティングデバイスのカーソルが含まれません。カーソル合成前の画像となります。特に困ることはないと思いますが、もしマニュアル作成用途などで画像にカーソルをどうしても含めたい場合は、スクリーンショットにカーソルのアイコンをオーバーレイ描画した合成画像を自動生成できるツールがあるので、そういったものを利用する方法もあります。

Windows Vista以降

Windows Vistaではキャプチャ用のSnipping Toolが搭載されました。デスクトップ全体・指定ウィンドウ以外に、選択領域のキャプチャもできます。しかし、個人的には上記のPrintScreenキーとペイントツールを使った方法よりもかえってめんどくさいので使っていません。また、PrintScreenキーを使った方法であれば、コンボボックスのドロップダウンリストを展開表示した状態や、特定のUI要素をマウスオーバーした状態でスクリーンショットを取得することができますが、Snipping Toolではそういったことができません。

Windows 8以降

Windows 8において、Windowsキー+PrintScreenキーを押すと、"%UserProfile%/Pictures/Screenshots" にデスクトップ全体のスクリーンショットPNG形式で自動保存されるようになりました。これはわりと有用な機能だと思いますが、Windows 7以前では使えないので、知っている人は少ないかもしれません。

ただし、Alt+PrintScreenのようにアクティブウィンドウのみをキャプチャして直接ファイル保存する機能がありません。この点がかなり不満です。

Windows 10

Windows 10ではゲーム録画 (Game DVR) の機能が追加され、Windows+Alt+PrintScreenでアプリのスクリーンショットを取得・保存できるようになりました。保存場所は"%UserProfile%/Videos/Captures"だそうです。「ゲーム録画」と銘打っていますが、ゲームアプリ以外でも使えます。しかし、利用にはXboxアプリへのMicrosoftアカウントを利用したサインイン(ネットワーク接続)が必要です。ローカルアカウント中心で使っているユーザーや、企業ユーザーの場合は使えない手段でしょう。Game DVRは当初既定で無効化されていたものの、Anniversary Update (1607) にて既定で有効化されたようです。以降は無効化する場合にもXboxアプリへのサインインが必要だそうで、もう意味不明ですね。Game DVRは個人情報が勝手にぶっこ抜かれる可能性があるので、自分は使っていません。

なお、Creators Update (1703) では余計な機能満載のPaint 3Dが標準インストールされるようになったのですが、さらにFall Creators Update (1709) では、従来の標準ペイントツールMS Paintが廃止・非標準になりました。完全に廃止されたわけではなく、Windowsストア(Microsoftストア)から明示的にインストールすれば使えるらしいのですが、ストアへのアクセスにはやはりサインイン(ネットワーク接続)が必要となります。電卓アプリや画像ビューアーの劣化と同じような道をたどるようです。ぶっちゃけ「Creators Update」とかいいつつ、誰も望んでいない、使えないゴミアプリを搭載しただけのアップデートです。そもそもCreatorだったらAdobe/Autodeskなどのデファクトスタンダードサードパーティ製ツールを使うのでPaint 3Dなんぞ要らないですね。

MS Paint自体はAdobe Photoshopなどと比べると大した機能を持っていませんが、これまでは標準インストールされていたことが最大の強みでした。特に仕事で、好きなペイントツールが使えない(インストールされていない/できない)端末において、取得したスクリーンショットをファイル保存するときだけはMS Paintをよく使っていました。また、機能が少ないぶんシンプルで、操作体系も昔からほとんど変わっていないため、初心者でも使いやすいというメリットがありました。しかし、今後は仕事で他の人にスクリーンショットの取得を指示する場合、まずはPaint 3DもしくはSnipping Toolに慣れてもらう必要がありそうです。

Steam

Valveが提供・運営するSteamプラットフォーム向けのゲームでは、共通してF12キーでスクリーンショット画像をファイル保存できます。ウィンドウモードでもクライアント領域のみが保存されるので、Steamの場合はこちらのほうが便利でしょう。昔はJPEGフォーマットのみでしたが、現在は劣化の無いPNGでの保存もサポートされています。
保存場所は "%ProgramFiles(x86)%/Steam/userdata/<ユーザーID>/760/remote/<作品ID>/screenshots" です。階層が深くて覚えにくいので、途中の "remote" フォルダーへのショートカットを作成しておくといいかもしれません。
Steamクライアントの「ライブラリ」から各ゲームのページを表示して、「スクリーンショット」欄の「全スクリーンショットを表示」ボタンをクリックすることで、「スクリーンショットアップローダ」ダイアログが表示され、このダイアログ上の「フォルダを表示」ボタンを押すことでもアクセスできます。ダイアログ上で各スクリーンショット画像(とサムネイルのペア)を削除したり、オンラインコミュニティにアップロードしたりすることができます。

余談

XPやVista/7はLunaやAeroといったVisualテーマを適用すると、ウィンドウの四隅が丸くなります。キャプチャすると四隅の部分は白色となりますが、個人的にはこの丸まった四隅がダサくて大嫌いでした。
また、過去のWindowsではタイトルバーにグラデーションが使われていたり、Windows Aero (Aero Glass) の機能でウィンドウが透過していたりしたのですが、これによりPNGキャプチャ画像は無駄にファイルサイズが大きくなりがちでした。なめらかに色変化する複雑な画像はPNGだと圧縮しづらいからです。Windows 8.xではデザイン方針としてModern UIを採用することでタイトルバーがシンプルな単色になり、PNGキャプチャ画像のファイルサイズも削減されました。しかし、Windows 10 1709では、Fluent Design Systemという、これまたMSの自己満足的デザイン方針が採用され、AppleiOSで採用されているような半透明のすりガラス(アクリル)効果が多用されることになったため、再びPNGキャプチャ画像のファイルサイズが無駄に増えそうです。「設定」→「個人用設定」→「色」にて「透明効果」をOFFにすることで無効化できるので、気になる人は設定変更しておいたほうがいいと思います。

2018-07-31追記:
Windows 10の次期バージョン(RS5)では、Snipping Toolが廃止予定となったようです。代わりに「Screen Sketch」とやらが導入される予定だそうですが、どうも微妙な感じのツールですね。どうせ使わない気がします。短命に終わる要らないアプリを開発してるヒマがあったら、バグのひとつでも取り除いてOS自体の品質を高めて欲しいです。

GeForceドライバー380系列のDirect3D 11バグ

3DグラフィックスとC++の研究目的で、DirectX 11 (Direct3D 11) を使った自前FBXビューアーを開発しているのですが、とある自作FBXファイル(約18,000ポリゴン程度)を開いて、カメラを回転させながら描画すると、レンダリングが停止する現象に遭遇しました。デバッグ レイヤーからは以下のようなエラーメッセージが出ます。いわゆるTDRハングアップです。

D3D11: Removing Device.
D3D11 ERROR: ID3D11Device::RemoveDevice: Device removal has been triggered for the following reason (DXGI_ERROR_DEVICE_HUNG: The Device took an unreasonable amount of time to execute its commands, or the hardware crashed/hung. As a result, the TDR (Timeout Detection and Recovery) mechanism has been triggered. The current Device Context was executing commands when the hang occurred. The application may want to respawn and fallback to less aggressive use of the display hardware). [ EXECUTION ERROR #378: DEVICE_REMOVAL_PROCESS_AT_FAULT]

具体的にどのメソッドコールやシェーダーがタイムアウトのトリガーになっているのかまでは調べ切れていないのですが、以前は同じFBXファイルをまったく問題なく表示できていました。検証した組み合わせ環境は以下の通りですが、どうやらNVIDIA GeForceドライバーを384.94に更新したことが原因のようです。Windows 7 (SP1 Platform Update) でもWindows 10でも発生します。

  • Win10 (1703) x64 + GeForce GTX 760 4GB + 384.94: hang
  • Win10 (1703) x64 + Quadro M4000 + 382.48: OK
  • Win10 (1703) x64 + Quadro M4000 + 386.01: OK

少なくともQuadro M4000では比較的新しいドライバーを適用しても問題が発生しないことを確認できています。Quadroのドライバーは基本的にGeForceよりも厳密度や品質・安定性が重視されているので当然と言えば当然かもしれません。Kepler固有の現象なのか、それともGeForceであればMaxwellPascalでも発生するのかは不明です。

Direct3D 11は現在のハイエンド3Dゲーム開発における中核ともいえるAPIで、すでに次世代ローレベルAPIであるDirect3D 12やVulkanが正式リリースされて2年ほど経つものの、アプリケーション開発のしやすさの点から言えば、いまだDirect3D 11の地位は揺るぎません。今回発見したバグは、現役のAPIに関する基本的なリグレッションなので、すでにゲーム開発者やゲーマーからNVIDIAにバグ報告が寄せられて修正されていてもいいような気がしますが、2017年12月にリリースされたドライバーでも修正されていないようです。

逆にある程度古いドライバーだとタイムアウト現象は確かに出ないのですが、古いドライバーにはセキュリティ脆弱性が潜んでいることがあるので、古いドライバーを使い続けるのも得策ではありません。もともと384.94は、重大なセキュリティ脆弱性が修正されたとかいう話だったので急遽インストールしたものです。

なお、年明け早々に大々的に報じられたCPU脆弱性Spectre/Meltdownのうち、Spectreの修正に対応した390系列のドライバーが先日公開されました。とはいってもGPU側の予測分岐機能などがシステムのセキュリティに影響を及ぼすわけではなく、たとえばWebブラウザ上で実行するJavaScriptのように、悪意のあるコードの踏み台となりえるソフトウェアプログラムが含まれていたせいか、もしくはパッチ適用によるシステム低速化を緩和させる目的で、おそらく今回の修正対象となったものと思われます。通例ドライバーはシステムメモリにカーネルモードでアクセスできるため、対策も必要となるのでしょう。近いうちに390系列も試す予定です(ただし人柱になるのは御免こうむりたいので、しばらくは様子見)。

余談:ドライバー更新時の問題

バージョン番号の新しいNVIDIAドライバーをインストール(バージョンアップ)する場合は通例上書きインストールできるものなのですが、検証などのためにロールバック(バージョンダウン)しなければならない場合、まず現在のドライバーをアンインストールするのが常套手段です。
しかし、NVIDIAのグラフィックスドライバーおよびHDオーディオドライバー*1をコントロールパネルからアンインストールした後、Windowsを再起動すると、Windowsによって勝手にハードウェアの認識と古いデフォルトドライバーのインストールが始まってしまうのが厄介です(しかも結構時間がかかる)。マウスやキーボード、フラッシュメモリなどのUSBデバイスに関しては、こういったプラグ&プレイによる自動認識動作は非常にありがたいのですが、グラフィックスやオーディオまで勝手にデフォルトドライバーをインストールされると非常に困ります。
なお、XPまではグラフィックスとオーディオのドライバーは自動インストールされず、OSインストール直後はハードウェアアクセラレーションが使えない状態となるのがデフォルト動作になっていました。このグラフィックスとオーディオのデフォルトドライバー自動インストール動作はWindows Vista以降で実装されたものです。ドライバーのインストール作業の方法が分からないビギナーにとってはありがたいものなのかもしれませんが、ドライバーバージョンを完全に自分でコントロールしたいパワーユーザーにとっては単なるおせっかいでしかない迷惑機能です。「デバイスのインストール設定」でWindows Update経由のドライバーインストールを無効化していても抑制できない模様で、またローカルのどこかにデフォルトドライバーのインストーラーパッケージを隠し持っているためか、ネットワークを切断していても抑制できません。どうやら抑制するにはレジストリ操作が必要らしく、面倒なので今回はあきらめることにしました。

*1:HDMIなど、グラフィックス以外にオーディオ伝送も可能とするインターフェイスが存在します。そういったものをグラフィックスデバイス経由でサポートする目的で、NVIDIAのドライバースイートにはオーディオドライバーも含まれています。

OpenCL/OpenGL/OpenCVのバイナリキャッシュ機能は使ってはいけない

OpenCL/OpenGLには当初、カーネルおよびシェーダープログラムに関してSPIR/SPIR-Vのような中間表現(バイトコード)規格が用意されておらず、それゆえオフラインコンパイルがサポートされていませんでしたが、コンパイル済みバイナリ(ベンダー依存)のキャッシュ機能はありました。

OpenCL 1.0:

OpenGL 4.1 or GL_ARB_get_program_binary:

また、OpenCV 2.xには、oclモジュールにおいて使用されるOpenCLカーネルのバイナリを、アプリケーションで使用する画像処理関数のカーネルごとに、初回呼び出し時に指定ディレクトリにファイル保存させることのできるキャッシュ機能が備わっていました。なお、保存されるファイル名にはOpenCLプラットフォーム名とデバイス名が含まれ、.clbの拡張子が付けられます。ただしワイド文字列のサポートはなく、したがってWindows上ではUnicodeがサポートされません*1

キャッシュ機能はOpenCV 3.0でいったんサポート外となった後、3.4にてOpenCL APIの薄いラッパーとして形を変えて復活したようです。

問題点

NVIDIA GeForceドライバーのとあるDirect3D 11リグレッション検証のために、グラフィックスドライバーのロールバック作業をしていたんですが、ついでにOpenCV 2.4.13を使ってOpenCV-CLの動作確認テストも実施したところ、OpenCLバイナリキャッシュの前方互換性がないことに気付きました。
具体的に言うと、例えば新しい388.71で出力したOpenCLカーネルのバイナリ*2を、古い353.90や364.72で読み込もうとすると、CL_INVALID_BINARYのエラーが発生します。OpenCV 2.4.13だと以下のようなエラーメッセージとなります。

XXX\sources\modules\ocl\src\cl_programcache.cpp:445: error: (-217) CL_INVALID_BINARY in function cv::ocl::ProgramFileCache::getOrBuildProgram

一応、古いカーネルを新しいドライバーで読み込むことはできる(後方互換性はある)ようですが、それが確実に保証されるのかどうか不明です。たぶん保証はされないでしょう。少なくとも前方互換性に関しては確保されていないことは確かです。おそらくOpenGLのシェーダープログラムバイナリに関しても似たような状況となっていることが予想されます。

これのどこが問題かというと、もしアプリケーションがカーネルバイナリをファイルとしてストレージにキャッシュ(保存)した後で、ユーザーがデバイスドライバーを変更すると、互換性がなくなって次回起動時にそのキャッシュが読み込めなくなり、アプリケーションが動作しなくなる、ということが懸念されるからです。
もしどうしてもバイナリキャッシュ機能を使いたい場合は、ドライバーの実装(バージョン番号)に紐づけた管理をするべきですが、少なくともOpenCV 2.xの実装はそうなっていません*3

なお、ドライバー変更以外でも、OpenCVのバージョンを変更すると、画像処理関数内部のカーネルが変わってOpenCLバイナリに互換性がなくなる、というケースがありえます。いっそOpenCV-CLのキャッシュ機能は使わないほうがよいでしょう。
少なくともPCにおけるAMD/NVIDIA環境に関しては、PCをシャットダウンするまで有効なオンメモリの組み込みキャッシュ機能がOpenGL/OpenCLドライバー側に備わっているはず*4なので、そちらに期待したほうがよいと思います。PC起動後の初回のコンパイル時間を我慢できれば、わざわざアプリケーション側でファイルとしてキャッシュする必要はありません。

ちなみにNVIDIA GeForceの場合、x86とx64とでOpenCLカーネルのバイナリ互換性がないらしいです。x86プロセスにてコンパイルしたバイナリをx64プロセスのドライバーに食わせようとすると、ドライバーがやはりCL_INVALID_BINARYのエラーを吐き、最悪の場合TDR後にドライバーがクラッシュすることもあるようです。普通に考えれば、ホストCPUアーキテクチャに依らずGPUコード側は同じはずなんですが、CUDA Unified Memory機能関連などでインターフェイス仕様の差異があるのかもしれません。Quadroは不明。
AMD FireProの場合は、少なくともCatalyst 15時点でx86/x64間の互換性がある模様です。Radeonは不明。

余談

ドライバー353.06だと、OpenCV 2.4.13のCV-CLで下記のエラーが発生します。別のバージョンのドライバーだと正常に動作するので、353.06のOpenCLドライバーにバグがある模様。

XXX\sources\modules\ocl\src\cl_operations.cpp:219: error: (-217) CL_MEM_OBJECT_ALLOCATION_FAILURE in function cv::ocl::openCLMemcpy2D

古いNVIDIAドライバーにはAPIが正常に動作しないバグがあるだけでなく、セキュリティ上の脆弱性も多々あるので、できるかぎり古いドライバーは使わないほうがよいです。
ところで先日、32bit版OS向けNVIDIAドライバーのサポートが打ち切られることが発表されましたが、現時点でWindows 7/8.1/10の32bit版は64bit版同様まだサポート期限が切れていないのに、今後32bit版において新しいハードウェアの対応がなされないのは企業ユーザーにとってかなり痛手になると思われます。32bit OS向けのセキュリティ対策は2019年1月まで続けられるそうですが、余命宣告を受けたも同然です。いずれにせよ、個人的にはどうでもいいんですけどね。

結論

やはりNVIDIA GeForceOpenCL実装は貧弱極まりないですね。また、それ以上にOpenCL/OpenGL仕様のいい加減さにあきれます。だからバイトコード規格を先に定義しろと(以下略)

*1:設計思想の古いOpenCVにとっては平常運転です。

*2:NVIDIAの場合、内部的にはCUDAで使われるPTXフォーマットになる模様です。

*3:クロスプラットフォームかつベンダー非依存な方法でドライバーのバージョン情報を取得する手段が標準化されていないこともあり、これはある意味仕方ないと思われます。

*4:OpenCL CもしくはGLSLのソースコード文字列に変更がなければ、一度コンパイルしたカーネルやシェーダープログラムのキャッシュが次回以降も使われるようです。

volatileに関してそろそろ一言いっておくか

今更言うまでもありませんが、C/C++Java/C#ではキーワードvolatileの意味が若干異なります。

C/C++

C/C++言語のvolatile修飾子は、コンパイラに副作用を示唆し、メモリアクセスの最適化を抑制するために存在します。volatileは典型的な処理系依存機能のうちのひとつであり、解釈は各コンパイラの実装に委ねられています。

C/C++volatileは、ときどきマルチスレッド間の簡易的な同期・通信用として使われていることがあります。たとえばサブスレッドでの処理完了を待機するために、グローバル変数などを用いて定義した処理完了フラグをメインスレッドにて監視・ポーリングする、といった状況です。

#include <cstdio>
#include <windows.h>
#include <process.h>
#include <conio.h>

volatile bool g_completed;

UINT CALLBACK MyThreadFunction(void*) {
    printf("Begin of sub-thread.\n");
    // ヘビーな処理を実行。
    for (int i = 0; i < 3; ++i) {
        printf("Executing task #[%d]...\n", i);
        ::Sleep(1000);
    }
    g_completed = true;
    printf("End of sub-thread.\n");
    return 0;
}

int main() {
    printf("Main thread waiting for completion of sub-thread...\n");
    auto hThread = reinterpret_cast<HANDLE>(_beginthreadex(nullptr, 0, MyThreadFunction, nullptr, 0, nullptr));
    while (!g_completed) {
        // メッセージ処理などを行ないながら待機。
        ::Sleep(1);
    }
    ::WaitForSingleObject(hThread, INFINITE);
    ::CloseHandle(hThread);
    hThread = nullptr;
    puts("Press any...");
    _getch();
}

※本来はC++11で標準化されたstd::threadを使ってもよいのですが、VC10.0 (VC++2010) などの古い処理系でもコンパイルできるよう、あえて古典的なCRTを使ってみました。

#include <cstdio>
#include <thread>
#include <chrono>

volatile bool g_completed;

void Sleep(int ms) {
    std::this_thread::sleep_for(std::chrono::milliseconds(ms));
}

void MyThreadFunction() {
    printf("Begin of sub-thread.\n");
    // ヘビーな処理を実行。
    for (int i = 0; i < 3; ++i) {
        printf("Executing task #[%d]...\n", i);
        ::Sleep(1000);
    }
    g_completed = true;
    printf("End of sub-thread.\n");
}

int main() {
    printf("Main thread waiting for completion of sub-thread...\n");
    std::thread thread(MyThreadFunction);
    while (!g_completed) {
        // メッセージ処理などを行ないながら待機。
        ::Sleep(1);
    }
    thread.join();
    puts("Finished.");
}

処理完了フラグはサブスレッドで書き換えますが、上記のようなケースにおいてフラグ変数をvolatileで修飾しない場合、コンパイル時の最適化により、フラグ変数へのアクセスを(メインスレッド-サブスレッド間で共有する)メインメモリではなく、(スレッドローカルな)レジスタにて実行するようなコードを出力してしまうことがあります。そうなると上記のポーリング用whileループの継続条件式は常に真となってしまい、メインスレッドはループを永遠に脱出できなくなります(無限ループ)。

ただし、こういったスレッド間の同期・通信用途はC/C++本来のvolatileの守備範囲ではなく、避けるべきです。そもそも、C++03規格およびそれ以前では、スレッドという概念そのものが標準化されていません。このようにvolatileがマルチスレッド同期に乱用されるようになった背景として、MSVCにおける独自の勝手な言語拡張*1があります。

VC++volatile拡張仕様では、なんとメモリバリアまで張ってくれるらしいです。つまり読み書き操作が暗黙的にアトミック処理になります。ただしx86/x64アーキテクチャARMアーキテクチャとでは既定の動作(コンパイラオプション)が異なるらしいので注意が必要です。移植性を考えると、VC++volatile拡張仕様に依存するべきではなく、メモリバリアはスレッドライブラリに用意されている同期オブジェクトを使って明示的に実装するべきです。

なお、一般的なアトミック操作の実現手段としては、C++11規格以降は基本的に標準ライブラリのstd::atomicを使うべきです。ただし、C++/CLI (/clr) では残念ながらstd::atomicが使えないそうで、代わりにWin32 Interlocked APIを直接使用するか、.NET標準クラスライブラリで用意されているアトミック処理用のSystem.Threading.Interlockedクラスなどを使います。C++/CLIにはC#lockステートメントに相当する組み込み構文は存在せず、またstd::mutexも使えないので、複雑な排他制御にはCRITICAL_SECTIONのようなWin32の同期オブジェクトを直接使用するか、System.Threading.MonitorSystem.Threading.SemaphoreSlimといった.NETの同期オブジェクトを直接使用するか、あるいはラッパークラスmsclr::lockを使用します*2処理系依存volatileを使うのは最後の手段にしましょう。

ちなみに前述の例は、WaitForSingleObject()の第2引数に1を指定して(タイムアウト時間1ミリ秒の待機とする)、ポーリングループ内で呼び出して戻り値をチェックする方法に変更すれば、volatileグローバルフラグ変数を取り除くことができます。

  while (::WaitForSingleObject(hThread, 1) == WAIT_TIMEOUT) {
    // メッセージ処理などを行ないながら待機。
  }

Java/C#

Java/C#volatileは、VC++拡張仕様のようなメモリバリアまではなされないものの、最適化を抑制する効果があります。C/C++と違い、処理系依存ではなくれっきとした言語仕様となっています。C#/Javaは当初からマルチスレッドを考慮した言語設計がなされており、volatile仕様に関してもマルチスレッドが考慮されているため、限定的ながらvolatileをスレッド間の同期・通信に使うこともできます。

Javavolatile変数に対するread/write自体はアトミックで、longdoubleにも指定することができます(ただし64bitのデータ型に対してアトミック命令が使われるとは限らない)。
C#でもvolatile変数に対するread/write自体はアトミックですが、longdoubleに指定することはできません(64bitのデータ型に対して、32bit環境ではアトミック命令が使えるとは限らないため)。

Java/C#volatileフィールドに対するインクリメント・デクリメントなどはアトミック操作にならないので、当然そういった用途にはjava.util.concurrent.atomicパッケージやSystem.Threading.Interlockedクラスなどを使うべきですが、一度だけ発生するフラグの単純な代入 (write) と参照 (read) による疑似的なシグナル用途など、volatileでもOKな場面もあります。運用制限を設けて賢く使いましょう。

*1:うわさによると、VC7.1 (VC++ .NET 2003) では、volatile関連で /Oa というさらにぶっ飛んだ最適化オプションがあったそうですが、このオプションはVC8.0 (VC++2005) で削除されたそうです。Visual Studio 2003 から 2015 の Visual C++ の新機能 | Microsoft Learn

*2:Windowsでの開発経験が浅く、Pthreadsや標準C++のスレッドライブラリしか使ったことがない人は勘違いしていることが非常に多いのですが、Win32のミューテックスオブジェクトや.NETのSystem.Threading.Mutexはプロセス間の排他制御に使います。スレッド間の排他制御に使うこともできますが、オーバーヘッドが大きいので基本的に使いません。

Visual StudioのビルドイベントでPowerShellを踏み台にしてC#を使う

Visual Studioでプロジェクトをビルドする際に、複雑な前処理・後処理を記述する場合、通例バッチコマンドによるカスタマイズをします。ただ、Windowsのコマンドは貧弱で、Unix/Linux環境のシェルなどとは比べ物になりません。
従来のバッチコマンドの代わりにPowerShellを使うのが近代的なWindowsプログラマーですが、個人的にはPowerShellの文法が好きではありません。言語機能も従来のバッチコマンドと比べると遥かに柔軟かつ高機能ですが、我々の大好きなC#言語と比べるとかなり書きづらいです。できればPowerShellよりもF#スクリプトC#スクリプトを使いたいのですが、これらはOS機能として統合・標準化されていないのが難点です。

そこで、PowerShellからC#コンパイラを使い、C#ソースコード文字列を渡してコンパイルし、C#で書かれたクラスをPowerShellから利用するという方法をとってみます。

param($rootDir)

$assemblies = (
#"System" # 不要。
"Microsoft.CSharp" # dynamic 型を使用するために必要。
)

$source = @"
using System;
using System.Runtime.CompilerServices;
public static class Test
{
  public static void CheckLambdaSpec()
  {
    var data = new[] { 1, 2, 3, 4, 5 };
    Action a = null;
    foreach (var x in data)
    {
      a += () => Console.WriteLine(x);
    }
    a();
  }

  public static void DoCallerInfoTestImpl([CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0)
  {
    Console.WriteLine(string.Format("Member name = \"{0}\"", memberName));
    Console.WriteLine(string.Format("Source file path = \"{0}\"", sourceFilePath));
    Console.WriteLine(string.Format("Source line number = {0}", sourceLineNumber));
  }

  public static void DoCallerInfoTest()
  {
    DoCallerInfoTestImpl();
  }

  public static void DoTest(string dir)
  {
    //string path = System.IO.Path.Combine(System.IO.Path.Combine(dir, @"Properties"), @"AssemblyInfo.cs");
    //using (System.IO.StreamReader reader = new System.IO.StreamReader(path))
    var path = System.IO.Path.Combine(dir, @"Properties", @"AssemblyInfo.cs");
    using (var reader = new System.IO.StreamReader(path))
    {
      string line;
      //Console.WriteLine(line?.Length.ToString() ?? "null");
      dynamic x = "hoge";
      Console.WriteLine(x.Length);
      while ((line = reader.ReadLine()) != null)
      {
        if (line.StartsWith("[assembly: AssemblyVersion("))
        {
          Console.WriteLine(line);
          break;
        }
      }
    }
  }
}
"@

try
{
  # ここでは、プロジェクトの出力先がカレント ディレクトリになる模様。
  #[System.Environment]::CurrentDirectory
  #$PSVersionTable
  $rootDir
  $rootDir.Length
  # コンパイル時に "%LocalAppData%/Temp/" にテンポラリ ソースファイル (*.cs) が生成される模様。
  Add-Type -ReferencedAssemblies $assemblies -TypeDefinition $source -Language CSharp
  #Add-Type -TypeDefinition $source -Language CSharp
  [Test]::DoTest($rootDir)
  [Test]::DoCallerInfoTest()
  #[Test]::CheckLambdaSpec()
}
catch
{
  #Write-Error($_.Exception)
  Write-Error($_.Exception.Message)
  exit -1
}

上記PowerShellスクリプトをプロジェクトディレクトリに"test.ps1"として保存し、ビルド前あるいはビルド後イベントのコマンドラインとして、

powershell -ExecutionPolicy RemoteSigned -File "$(ProjectDir)test.ps1" "$(ProjectDir)\" ";exit $LASTEXITCODE"

を入力しておきます。
ここで、Visual Studio 環境変数PowerShell スクリプトコマンドライン引数として渡すとき、"$(ProjectDir)\" などとします。"$(ProjectDir)" ではダメなようです。末尾になぜか余計なダブルクォーテーションが入ります。

C#コンパイラのバージョン

前述のPowerShellからC#コンパイラを利用する手法自体に関してはWeb上のあちこちで言及されているのですが、そのC#コンパイラのバージョンに関してはほとんど言及がないようです。なので少し調べてみました。

前述の例はC# 5.0対応コンパイラが使えるという前提で記述しています。そもそもプリインストールされているPowerShellのバージョンもWindows OSによって異なるので、もしプロジェクトでPowerShellスクリプトを使う場合は最小バージョンに合わせて記述するか、開発者全員にPowerShellのバージョンアップを促しましょう。