(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#の文法的に無理なこともメタ的に書けるので全てのケースで今回のように書けるわけではないので必要なときもありますけどねー。