ネコのために鐘は鳴る

寺院に住み着くパソコ〇好き

(C#) #if DEBUG を使わないデバッグ分岐

デバッグとリリースで処理を変えたいときは普通は大体以下のように書きます。

public void Foo()
{
#if DEBUG
    Console.WriteLine("This is Debug.");
#else
    Console.WriteLine("This is Release");
#endif
}

正直見にくいです。コンパイルされない側の記述は Visual Studio のコードアナリティクスもシンタックスハイライトもなくなり、非常に使いにくい。当然、関数名変更などのリファクタリングも効かなくなり、いつの間にかリリースビルドが通らないコードになっていたり、なんてこともあり得ます。

これぐらいの分岐なら全然いいですが、もっといくつもシンボルがあって大量に分岐があちこちに書かれているコードは非常に苦痛。おいおいここはC++じゃないんだぜ

なので、以下のように書きます。

internal static class AssemblyState
{
    public static bool IsDebug =>
#if DEBUG
    true;
#else
   false;
#endif
}

public void Foo()
{
    if(AssemblyState.IsDebug)
    {
        Console.WriteLine("This is Debug.");
    }
    else
    {
        Console.WriteLine("This is Release.");
    }
}

[2022/10/26 追記]

結構この記事を見てくれる人がいたので修正。もともと上記のコードはconst bool IsDebug =として定数で定義していましたが、static bool IsDebug =>として static な getter のみのプロパティに修正しました。 定数で定義しているとif (false) になる部分に対して、絶対に通らない分岐がありますとコンパイラに警告されてしまいます。 プロパティにしておくとこの警告は出ず、かつ実行時にはインライン展開されて最適化され定数で書いた場合と全く同一になります。

プリプロセッサディレクティブをまとめて書くために1つクラスを用意しておいて、そこに定数で定義しておくと、あとは普通にbool型として分岐できます。分岐したい部分がいくつあっても、#ifを書くのは一ヵ所で済みます。

なにより、普通のC#のコードになっているので、シンタックスハイライトが消えたり参照が効かなかったりなんてならないのがいいです。

この場合、実行時に分岐処理は発生しません。実行時に分岐が定数の場合はJITが不要なifと分岐を削除し、機械語レベルで最初のコードと同じになります。つまり、IL には分岐が存在するが、機械語には分岐がないため実行速度も落ちません。

ほんとに大丈夫なのか

将来的に JIT コンパイルの分岐削除がなくなるかもしれないじゃないですかヤダーって話ですが、たぶんそれはないです。

なぜなら .NET の中にも JIT の分岐削除を前提としたコードがpublicで存在しているからです。C#は言語的に非常に破壊的変更に敏感で、特にpublicで公開されているコードの挙動なんかはまず変わりません。

具体的に何かというと、CPU固有命令を提供している部分などです。

たとえばSystem.Runtime.Intrinsics.X86.LzcntクラスにIsSupportedというプロパティがありますが、これは実行環境の CPU が x86 の lzcnt 命令が使える場合は true になり、

if(Lzcnt.IsSupported)
    return Lzcnt.LeadingZeroCount(value);

のように分岐でき、JIT が分岐を消すことを前提にした使い方をします。わざわざ CPU 命令を持ち出すぐらいですから超パフォーマンス重視の部分に使われる想定で、分岐を消さないなんていう無駄はまずありえないです。 つまり分岐は消えます。

 

というように、うまく JIT の挙動を活用すればあちこちに#if DEBUGを書く見にくいコードから解放されます。

ただしプリプロセッサディレクティブを使えばメソッド定義自体の存在をなかったことにしたり、 C#の文法的に無理なこともメタ的に書けるので全てのケースで今回のように書けるわけではないので必要なときもありますけどねー。