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

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

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のバージョンアップを促しましょう。

Directory.Exists/File.Existsメソッドのタイムアウト時間

.NETのSystem.IO.Directory.Exists()メソッドは指定ディレクトリの存在有無をチェックするメソッドですが、タイムアウト時間が設けられています。ローカルドライブのディレクトリにアクセスする場合は、よほど低速なシステムか膨大なストレージでないかぎり、メソッドの実行は一瞬(数ミリ秒オーダー)で終わりますが、チェック対象がネットワーク共有フォルダーなどの場合、アクセスできないときはシステム規定のタイムアウト時間が経過するまで延々とリトライを続けます。この動作はSystem.IO.File.Exists()も同様です。なので普通、これらのI/O処理はメインスレッドで直接実行したりせず、サブスレッドに処理を逃がして非同期で実行するのがセオリーです。今どきのC#であればTPLやasync/awaitを使うのが常套手段でしょう。

Windows向けの.NET 4.x実装では、Directory.Exists()は内部でWin32 Shell APIPathFileExists()PathIsDirectory()を使っているものと思われます。確かネットワークアクセスのタイムアウト時間はWindows OSのシステム設定に依存するはずで、既定値は20秒だったと思います。また、タイムアウト時間はアプリケーション側では指定できないはずです。無効なネットワーク共有フォルダーにアクセスして、意図的にタイムアウトさせて時間を計測するために下記のC#スクリプトを試してみましたが、環境によって約20-30秒と開きがあるようです。ときどき40秒近くかかることもありました。ネットワーク共有フォルダーへのアクセスのタイムアウト時間に関しては、おそらく名前解決など、システムのネットワーク構成にも依存するものと思われます。

Action<string> checkDir = (path) => { var sw = new Stopwatch(); sw.Start(); Print(Directory.Exists(path)); sw.Stop(); Print(sw.Elapsed); };
checkDir(@"\\1.2.3.4\hoge");

Action<string> checkFile = (path) => { var sw = new Stopwatch(); sw.Start(); Print(File.Exists(path)); sw.Stop(); Print(sw.Elapsed); };
checkFile(@"\\1.2.3.4\hoge\fuga.txt");

C#スクリプト

C#コードをスクリプトとして対話的に実行できる「C# Interactive」はVisual Studio 2015 Update 1で追加された機能ですが、こういったAPIの挙動を調査するのに重宝しています。PrintメソッドはInteractiveScriptGlobalsクラスの静的メソッドのひとつですが、おそらくC# 6.0で追加されたusing static機能をC# Interactive内部で暗黙的に使い、using static InteractiveScriptGlobals;とすることでクラス名を省略できているものと思われます。using static自体はJava 5におけるimport staticの後追いのようなもので、不要な混乱を招きかねないため積極的に使うべき類の機能ではありませんが、こういったスクリプトコードに関してはC/C++Pythonなどのようにグローバルメソッドが簡潔に使えるということは重要ですね。

ちなみにC#スクリプトの拡張子は.csxで、C# Interactive外のVisual Studioコードエディターでもコード補完が効きますが、この拡張子のファイルをVisual Studio 2015 Update 3で編集していると、IDEが突然クラッシュするという残念な不具合があるようです。VS2017は試していません。

スレッドの強制終了について

I/O処理のタイムアウト時間が過ぎる前に処理を強制的に中断しようとして、ワーカースレッドをメインスレッドからSystem.Threading.Thread.Abortメソッドで強制終了しようなどとするのはご法度です。中断の結果として起こり得る未定義動作の危険性が説明されています。

Abortメソッドの危険レベルとしてはWin32 APITerminateThread関数と同程度で、よほどのことがないかぎり使うべきではありません。現に.NET Coreの仕様では、Thread.Abortメソッドは削除されているそうです。

JavaThreadクラスにもstopメソッドがありますが、やはり同様に非推奨となっています。

C# 6.0コンパイラーの興味深い挙動

C#言語はnullに関連する演算子が豊富です。

null合体演算子

null合体演算子は左側オペランドがnullでない場合は左側オペランドを返し、nullの場合は右側オペランドを返します。参照型のほか、Null許容型(Nullable)にも適用可能です。C# 2.0で追加されました。

x ?? alt

三項演算子で書くと以下のようになります。

x != null ? x : alt

null条件演算子

null条件演算子オペランドがnullでない場合はメンバーアクセス(.もしくは[])を実行します。参照型のほか、Null許容型(Nullable)にも適用可能です。C# 6.0で追加されました。

x?.SomeMethod();

if文で書くと以下のようになります。

if (x != null) { x.SomeMethod(); }

もしここでSomeMethod()が値型Tの戻り値を持つ場合、x?.SomeMethod()の評価結果はNull許容型T?となります。三項演算子で書くと以下のようになります。

x != null ? x.SomeMethod() : default(T?)

応用

これらの演算子を応用して、「引数がnullでない場合はToString()を呼び出し、nullの場合は代替のリテラル文字列"N/A"を返す」という処理をスマートに書いてみます。

// (1)
public static string ConvertToNAIfNull<T>(T x)
{
  return x?.ToString() ?? "N/A";
}

もしこれらの演算子を使わずに書くとすれば、以下のようになります。

// (2)
public static string ConvertToNAIfNull<T>(T x)
{
  return x != null ? x.ToString() : "N/A";
}

いずれにせよ1行で書けますが、(2)はxが2回出現して冗長なので、特にメソッドではなくインラインで書く際に不便そうですね。厳密に言うと、(1)と(2)は完全互換ではありませんが、通例ToString()は空文字列""を返却することはあってもnullを返却することはないので実用上は問題ないはずです。

ここで興味深いのが、(1)のジェネリクスには参照型やNull許容型だけでなく、通常の値型を渡しても実体化できるということです。csc 2015 (Visual C# 2015) でもgmcs 4.6.2でも動作します。

Console.WriteLine(ConvertToNAIfNull(new int?(10)));
Console.WriteLine(ConvertToNAIfNull(default(int?)));
Console.WriteLine(ConvertToNAIfNull(10));

値型に対する?.演算子呼び出しは無効なので、一見コンパイルエラーになってしまう気がしますが、(1)のジェネリクスC#コンパイラーが内部で以下のように展開するせいで、値型を渡してもコンパイルエラーにはならず実体化できる、というからくりになっているようです。

// (3)
public static string ConvertToNAIfNull<T>(T x)
{
  if (x != null) {
    var t = x.ToString();
    if (t != null) {
      return t;
    }
  }
  return "N/A";
}

値型に対してはx != nullコンパイルエラーにはならず常に真なので、(2)や(3)のジェネリクスに値型を渡せるのは当然です。いずれにせよ、ConvertToNAIfNull()に値型を渡すと冗長なメソッドになってしまいますが。

Windows 10でPhotoshop CS5が起動に失敗するときの対処

Windows 10 Anniversary Update (1607) をインストールした後、Photoshop CS5が起動時にクラッシュするようになりました。管理者特権だと起動します。仕方ないので以下のEXEをWindows 7互換モードに設定すると起動するようになりました。一方、Illustrator CS5は特に対応不要でした。

もともとCS5はWindows 10に正式対応していないし、無償・有償サポートの期限も切れている*1ので、まあ仕方ないといえば仕方ないんですが、別に新機能が欲しいわけでもないのにサブスクリプション契約を続けなければならないAdobe Creative Cloud (CC) に移行したいとは思いません。CS5に限らず、この先もWin10に大型アップデートが来るたびに同じようなアプリケーション互換性のトラブルが発生しそうです。ただでさえカオスでキメラな設計のWin10に、これ以上余計な機能や仕様変更を加えてどうするつもりなのか。もはや余計な機能追加なんて誰も望んでいません。本来アプリケーションを安定して動かすのがOSの役目なのに、今のWindowsは出しゃばりすぎです。

余談ですがWin10のUpdateOrchestratorの挙動は極悪すぎます。勝手にWindows Updateを実行したうえ、ユーザーの許可なく勝手にスリープやハイバネーションを解除してPCを再起動するなんて、横暴にもほどがあります。どれだけユーザーをバカにしているんでしょうか。Creators Updateでは多少緩和されているようですが、それにしてもアップデートのたびにプライバシー設定をリセットするのは極めて卑劣なやり口です。あとせっかくカスタマイズしたWindowsエクスプローラーの設定をリセットするのもやめて欲しい。

※2021-02-28追記:
今のところWindows 10 1909でもCS5を使うことができています。

そういえば、LightWave 11や2015もWindows 10 1809以降で動かなくなったようです。手元のWindows 10 1909でもLightWave 11.6.2のModeler/Layoutが起動時にクラッシュします。EXEをWindows 7互換モードに設定しても対処不能。どうやらMicrosoft IMEの仕様変更が原因らしく、ATOKGoogle日本語入力に切り替えることで回避できるそうですが……

変な仕様変更を入れて互換性を破壊するWindowsもアレですが、IMEの仕様変更ごときで起動しなくなるLightWaveの設計も相当アホだと思います。一般的なWindows API後方互換性自体は比較的高く、普通にWindows APIを使用して開発されたWin32アプリであれば、まず動かなくなるようなことはないはず。

※2021-05-04追記:
Windows 10 20H2に更新すると、LightWave 11.6.2のModelerもLayoutも起動するようになりました。20H1 (2004) でIMEにまた大幅な仕様変更が入ったらしく、その影響で今度は逆にLightWaveが起動するようになったようです。ただし、MS-IMEの互換性オプションで、「以前のバージョンの Microsoft IME を使う」をONにすると、LightWave 11が起動しなくなります。残念ながら20H1以降の新しいMS-IMEはバグだらけで使い物にならないので、以前のバージョンにしておいたほうがよいです。サードパーティー製のIMEを導入できない場合、Windows 10でのLightWave 11の利用は諦めましょう。

*1:Adobe CS5のリリースは2010年で、Windows 7時代の製品群ですが、無償サポートは2012年に、有償サポートは2014年に終了しています。