1. 概要
タイトル通りです。概要として書くことがあまりありません。
「c# 言語上では定数ではないけれど、JITがコンパイルする際には定数扱いされて様々な最適化の対象となる」 、そういったケースがよくあります。
私がよく遭遇するケースに 実行時の OS による場合分けがあります。例えばこんな感じです。
if (OperatingSystem.IsWindows())
{
// Windows 固有のコード
}
else if (OperatingSystem.IsLinux())
{
// Linux 固有のコード
}
else ...
こういったコードを書くと c# コンパイラはほぼこのままの内容のILコードを出力しますが、JIT コンパイラが実行環境に合わせて最適化をしてくれます。
具体的には、例えば Windows 上であれば、JIT コンパイラは OperatingSystem.IsWindows()
の部分を true
に置き換え、OperatingSystem.IsLinux()
の部分を false
に置き換えて、更にそれに基づいた最適化を行います。
その結果、JIT コンパイラの出力結果では、Windows 固有のコードのみが残り、if (OperatingSystem.IsWindows())
の条件判断そのものも省略されます。
この他にも、JIT によって定数とみなされ最適化されるケースはいくつかあります。
本稿では、自分の覚え書きのついでにそれらについて紹介しようと思います。
2. 定数化に関する最適化の具体例
実行時に定数として扱われるいくつかのケースについて、以下に具体例を示します。
なお、実行環境は以下の通りです。
- OS: Windows 10 64bit
- .NET Runtime: 9.0.7
あと、以前の投稿 の反省を踏まえて、c# のプロジェクトファイルに以下の設定を追加して、Quick JIT を無効にしてあります。
既定では Quick JIT は有効になっており、本稿で紹介する最適化は行われない可能性があります。
<TieredCompilationQuickJit>false</TieredCompilationQuickJit>
2.1 sizeof(Int32)
の場合
まずは簡単な例から始めます。
以下が実験用に用意したソースコードです。
[MethodImpl(MethodImplOptions.NoInlining)]
public static Int32 GetSizeOfInt32() => sizeof(Int32);
[MethodImpl(MethodImplOptions.NoInlining)]
public static Int32 GetBitCountOfInt32() => sizeof(Int32) * 8;
このソースコードをコンパイルしてデコンパイルすると以下のようになります。
[MethodImpl(MethodImplOptions.NoInlining)]
public static int GetSizeOfInt32()
{
return 4;
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static int GetBitCountOfInt32()
{
return 32;
}
c# のコンパイルの時点で sizeof(Int32)
が 4
に置き換わり、更に定数式の最適化が行われていることが分かります。
まぁ、sizeof(Int32)
は c# 言語上でも元々定数として扱われるので、この結果の予想は容易だと思います。
2.2 Unsafe.SizeOf<Int32>()
の場合
Unsafe.SizeOf<Int32>()
ではどうなるでしょうか。
以下が c# のサンプルコードです。
[MethodImpl(MethodImplOptions.NoInlining)]
public static Int32 GetUnsafeSizeOfInt32() => Unsafe.SizeOf<Int32>();
[MethodImpl(MethodImplOptions.NoInlining)]
public static Int32 GetUnsafeBitCountOfInt32() => Unsafe.SizeOf<Int32>() * 8;
これをコンパイルしてデコンパイルすると以下のようになります。
[MethodImpl(MethodImplOptions.NoInlining)]
public static int GetUnsafeSizeOfInt32()
{
return Unsafe.SizeOf<int>();
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static int GetUnsafeBitCountOfInt32()
{
return Unsafe.SizeOf<int>() * 8;
}
コンパイル時では一切の最適化がされていないことがわかります。
このソースコードを Resease ビルドしたものをデバッガで追ってみたところ、以下のようになりました。
; In the case of GetUnsafeSizeOfInt32():
; equivalent to "return 4;".
mov eax,4
ret
; In the case of GetUnsafeBitCountOfInt32():
; equivalent to "return 32;".
mov eax,20h
ret
何れも、実行時に単純な定数に最適化されていることが分かります。
2.3 Unsafe.SizeOf<UIntPtr>()
の場合
次は Unsafe.SizeOf<UIntPtr>()
です。
IntPtr
, UIntPtr
などの型のサイズは実行環境によって異なることがありますが、どうなるでしょうか。
以下が c# のソースコードです。
[MethodImpl(MethodImplOptions.NoInlining)]
public static Int32 GetUnsafeSizeOfUIntPtr() => Unsafe.SizeOf<UIntPtr>();
[MethodImpl(MethodImplOptions.NoInlining)]
public static Int32 GetUnsafeBitCountOfUIntPtr() => Unsafe.SizeOf<UIntPtr>() * 8;
これをコンパイルしてデコンパイルすると以下のようになります。
[MethodImpl(MethodImplOptions.NoInlining)]
public static int GetUnsafeSizeOfUIntPtr()
{
return Unsafe.SizeOf<nuint>();
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static int GetUnsafeBitCountOfUIntPtr()
{
return Unsafe.SizeOf<nuint>() * 8;
}
コンパイル時には最適化が全くされていないことがわかります。
このソースコードを Resease ビルドしたものをデバッガで追ってみたところ、以下のようになりました。
; In the case of GetUnsafeSizeOfUIntPtr():
; equivalent to "return 8;".
mov eax,8
ret
; In the case of GetUnsafeBitCountOfUIntPtr():
; equivalent to "return 64;".
mov eax,40h
ret
何れも、実行時に単純な定数に最適化されていることが分かります。
2.4 OperatingSystem.IsWindows()
および OperatingSystem.IsLinux()
の場合
先に 1. 概要 で結論を述べてしまっていますが、OperatingSystem.IsWindows()
および OperatingSystem.IsLinux()
の場合です。
以下が c# のソースコードです。
[MethodImpl(MethodImplOptions.NoInlining)]
public static Boolean IsWindows() => OperatingSystem.IsWindows();
[MethodImpl(MethodImplOptions.NoInlining)]
public static Boolean IsLinux() => OperatingSystem.IsLinux();
これをコンパイルしてデコンパイルすると以下のようになります。
[MethodImpl(MethodImplOptions.NoInlining)]
public static bool IsWindows()
{
return OperatingSystem.IsWindows();
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static bool IsLinux()
{
return OperatingSystem.IsLinux();
}
コンパイル時には最適化が全くされていないことがわかります。
このソースコードを Resease ビルドしたものをデバッガで追ってみたところ、以下のようになりました。
; In the case of IsWindows():
; equivalent to "return true;".
mov eax,1
ret
; In the case of IsLinux():
; equivalent to "return false;".
xor eax,eax
ret
何れも、実行時に true
あるいは false
の定数に最適化されていることが分かります。
2.5 Environment.Is64BitProcess
の場合
実のところ、Environment.Is64BitProcess
をデコンパイルすると常に IntPtr.Size == 8
を返していることがわかるので結果は予想できますが、一応実験してみました。
以下が c# のソースコードです。
[MethodImpl(MethodImplOptions.NoInlining)]
public static Boolean Is64BitProcess() => Environment.Is64BitProcess;
これをコンパイルしてデコンパイルすると以下のようになります。
[MethodImpl(MethodImplOptions.NoInlining)]
public static bool Is64BitProcess()
{
return Environment.Is64BitProcess;
}
コンパイル時には最適化が全くされていないことがわかります。
このソースコードを Resease ビルドしたものをデバッガで追ってみたところ、以下のようになりました。
; In the case of Is64BitProcess():
; equivalent to "return true;".
mov eax,1
ret
単純な定数に最適化されていることが分かります。
2.6 VectorXXX.IsHardwareAccelerated
の場合
次に、ベクター操作のハードウェアアクセラレーションが有効かどうかを調べるコードが実行時にどうなるかを実験します。
ハードウェアアクセラレータが有効かどうかは実行環境依存ですので、IL へのコンパイル時点では何も特別なことは起きないはずです。
以下が c# のソースコードです。
[MethodImpl(MethodImplOptions.NoInlining)]
public static Boolean GetVector512IsHardwareAccelerated() => Vector512.IsHardwareAccelerated;
[MethodImpl(MethodImplOptions.NoInlining)]
public static Boolean GetVector256IsHardwareAccelerated() => Vector256.IsHardwareAccelerated;
[MethodImpl(MethodImplOptions.NoInlining)]
public static Boolean GetVectorIsHardwareAccelerated() => Vector.IsHardwareAccelerated;
これをコンパイルしてデコンパイルすると以下のようになります。
[MethodImpl(MethodImplOptions.NoInlining)]
public static bool GetVector512IsHardwareAccelerated()
{
return Vector512.IsHardwareAccelerated;
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static bool GetVector256IsHardwareAccelerated()
{
return Vector256.IsHardwareAccelerated;
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static bool GetVectorIsHardwareAccelerated()
{
return Vector.IsHardwareAccelerated;
}
予想通り、コンパイル時には最適化が全くされていないことがわかります。
このソースコードを Resease ビルドしたものをデバッガで追ってみたところ、以下のようになりました。
; In the case of GetVector512IsHardwareAccelerated():
; equivalent to "return false;".
xor eax,eax
ret
; In the case of GetVector256IsHardwareAccelerated():
; equivalent to "return true;".
mov eax,1
ret
; In the case of GetVectorIsHardwareAccelerated():
; equivalent to "return true;".
mov eax,1
ret
何れも単純な定数に最適化されていることが分かります。
筆者の PC の CPU は Intel Core i7-7700K ですので、256 ビットには対応してはいるが 512 ビットには対応していないという結果で合っているはずです。
2.7 VectorXXX<ELEMENT_T>.IsSupported
の場合
次は、ある型がベクターの要素として利用可能かどうかを調べるコードが実行時にどうなるかを実験します。
以下が c# のソースコードです。
[MethodImpl(MethodImplOptions.NoInlining)]
public static Boolean GetVector256IsSupported<ELEMENT_T>() => Vector256<ELEMENT_T>.IsSupported;
[MethodImpl(MethodImplOptions.NoInlining)]
public static Boolean GetVectorIsSupported<ELEMENT_T>() => Vector<ELEMENT_T>.IsSupported;
これをコンパイルしてデコンパイルすると以下のようになります。
[MethodImpl(MethodImplOptions.NoInlining)]
public static bool GetVector256IsSupported<ELEMENT_T>()
{
return Vector256<ELEMENT_T>.IsSupported;
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static bool GetVectorIsSupported<ELEMENT_T>()
{
return Vector<ELEMENT_T>.IsSupported;
}
コンパイル時には最適化が全くされていないことがわかります。
このソースコードを Resease ビルドしたものをデバッガで追ってみたところ、以下のようになりました。
; In the case of GetVector256IsSupported<UInt32>():
; equivalent to "return true;".
mov eax,1
ret
; In the case of GetVector256IsSupported<Char>():
; equivalent to "return false;".
xor eax,eax
ret
; In the case of GetVectorIsSupported<UInt32>():
; equivalent to "return true;".
mov eax,1
ret
; In the case of GetVectorIsSupported<Char>():
; equivalent to "return false;".
xor eax,eax
ret
型パラメタとして UInt32
と Char
を与えています。
何れも、定数として最適化されています。
そもそも VectorXXX
が要素として Char
をサポートしていないので、型パラメタとして Char
を与えた場合は常に false
が返ります。
2.8 VectorXXX<ELEMENT_T>.Count
の場合
次は、VectorXXX<ELEMENT_T>
クラスの Count
プロパティを参照するコードが実行時にどうなるかについて実験します。
Count
プロパティは、型パラメタ ELEMENT_T
を VectorXXXX
の要素として使用した場合に VectorXXX
が格納できる要素の数を示します。
実際の話、例えば Vector256<Uint>.Count
が持てる要素の数は 256bit / 32bit == 8
なので Vector256<Uint>.Count
の代わりに 8
と書くこともできますが、パフォーマンスに影響しないのであれば Vector256<Uint>.Count
と書いた方が分かりやすくていいでしょう。
以下が c# のソースコードです。
[MethodImpl(MethodImplOptions.NoInlining)]
public static Int32 GetVector256Count<ELEMENT_T>() => Vector256<ELEMENT_T>.Count;
[MethodImpl(MethodImplOptions.NoInlining)]
public static Int32 GetVectorCount<ELEMENT_T>() => Vector<ELEMENT_T>.Count;
これをコンパイルしてデコンパイルすると以下のようになります。
[MethodImpl(MethodImplOptions.NoInlining)]
public static int GetVector256Count<ELEMENT_T>()
{
return Vector256<ELEMENT_T>.Count;
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static int GetVectorCount<ELEMENT_T>()
{
return Vector<ELEMENT_T>.Count;
}
コンパイル時には最適化が全くされていないことがわかります。
このソースコードを Resease ビルドしたものをデバッガで追ってみたところ、以下のようになりました。
; In the case of GetVector256Count<UInt32>():
; equivalent to "return 8;".
mov eax,8
ret
; In the case of GetVectorCount<UInt32>():
; equivalent to "return 8;".
mov eax,8
ret
何れも、定数として最適化されています。
これで安心してソースコード上に Vector256<ELEMENT_T>.Count
をいくらでも書けます。
2.9 【応用】定数を含んだ式の場合
ここまでで、様々なメソッドやプロパティが実行時に定数として最適化されることを述べてきましたが、それらを含んだ式がどのように最適化されるかを調べるために、ちょっとだけ実用的なメソッドを作ってみました。
そのメソッドの仕様は以下の通りです。
-
public static Boolean IsBetterToUseVector256<ELEMENT_T>()
このメソッドは以下の条件が成り立つ場合にtrue
を返し、それ以外の場合にfalse
を返す。- 実行環境にて 256 bit ベクター操作のハードウェアアクセラレーションが有効であり、かつ
-
ELEMENT_T
型を要素としたベクター操作がサポートされており、かつ - ベクター操作で一度に操作できる要素の数が 8 以上である場合。
-
public static Boolean IsBetterToUseVector<ELEMENT_T>()
このメソッドは以下の条件が成り立つ場合にtrue
を返し、それ以外の場合にfalse
を返す。- 実行環境にて ベクター操作のハードウェアアクセラレーションが有効であり、かつ
-
ELEMENT_T
型を要素としたベクター操作がサポートされており、かつ - ベクター操作で一度に操作できる要素の数が 8 以上である場合。
-
public static Boolean IsBetterToUseVector256<ELEMENT_T>(Int32 minimumCount)
このメソッドは以下の条件が成り立つ場合にtrue
を返し、それ以外の場合にfalse
を返す。- 実行環境にて 256 bit ベクター操作のハードウェアアクセラレーションが有効であり、かつ
-
ELEMENT_T
型を要素としたベクター操作がサポートされており、かつ - ベクター操作で一度に操作できる要素の数が
minimumCount
以上である場合。
-
public static Boolean IsBetterToUseVector<ELEMENT_T>(Int32 minimumCount)
このメソッドは以下の条件が成り立つ場合にtrue
を返し、それ以外の場合にfalse
を返す。- 実行環境にて ベクター操作のハードウェアアクセラレーションが有効であり、かつ
-
ELEMENT_T
型を要素としたベクター操作がサポートされており、かつ - ベクター操作で一度に操作できる要素の数が
minimumCount
以上である場合。
これらのメソッドの c# のソースコードを以下に示します。
[MethodImpl(MethodImplOptions.NoInlining)]
public static Boolean IsBetterToUseVector256<ELEMENT_T>() => Vector256.IsHardwareAccelerated && Vector256<ELEMENT_T>.IsSupported && Vector256<ELEMENT_T>.Count >= 8;
[MethodImpl(MethodImplOptions.NoInlining)]
public static Boolean IsBetterToUseVector<ELEMENT_T>() => Vector256.IsHardwareAccelerated && Vector<ELEMENT_T>.IsSupported && Vector<ELEMENT_T>.Count >= 8;
[MethodImpl(MethodImplOptions.NoInlining)]
public static Boolean IsBetterToUseVector256<ELEMENT_T>(Int32 minimumCount) => Vector256.IsHardwareAccelerated && Vector256<ELEMENT_T>.IsSupported && Vector256<ELEMENT_T>.Count >= minimumCount;
[MethodImpl(MethodImplOptions.NoInlining)]
public static Boolean IsBetterToUseVector<ELEMENT_T>(Int32 minimumCount) => Vector256.IsHardwareAccelerated && Vector<ELEMENT_T>.IsSupported && Vector<ELEMENT_T>.Count >= minimumCount;
これをコンパイルしてデコンパイルすると以下のようになります。
[MethodImpl(MethodImplOptions.NoInlining)]
public static bool IsBetterToUseVector256<ELEMENT_T>()
{
if (Vector256.IsHardwareAccelerated && Vector256<ELEMENT_T>.IsSupported)
{
return Vector256<ELEMENT_T>.Count >= 8;
}
return false;
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static bool IsBetterToUseVector<ELEMENT_T>()
{
if (Vector256.IsHardwareAccelerated && Vector<ELEMENT_T>.IsSupported)
{
return Vector<ELEMENT_T>.Count >= 8;
}
return false;
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static bool IsBetterToUseVector256<ELEMENT_T>(int minimumCount)
{
if (Vector256.IsHardwareAccelerated && Vector256<ELEMENT_T>.IsSupported)
{
return Vector256<ELEMENT_T>.Count >= minimumCount;
}
return false;
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static bool IsBetterToUseVector<ELEMENT_T>(int minimumCount)
{
if (Vector256.IsHardwareAccelerated && Vector<ELEMENT_T>.IsSupported)
{
return Vector<ELEMENT_T>.Count >= minimumCount;
}
return false;
}
コンパイル時には最適化が全くされていないことがわかります。
このソースコードを Resease ビルドしたものをデバッガで追ってみたところ、以下のようになりました。
まずは、パラメタがないメソッドの場合です。
; In the case of IsBetterToUseVector256<UInt32>():
; equivalent to "return true;".
mov eax,1
ret
; In the case of IsBetterToUseVector256<UInt64>():
; equivalent to "return false;".
xor eax,eax
ret
; In the case of IsBetterToUseVector256<Char>():
; equivalent to "return false;".
xor eax,eax
ret
; In the case of IsBetterToUseVector<UInt32>():
; equivalent to "return true;".
mov eax,1
ret
; In the case of IsBetterToUseVector<UInt64>():
; equivalent to "return false;".
xor eax,eax
ret
; In the case of IsBetterToUseVector<Char>():
; equivalent to "return false;".
xor eax,eax
ret
これらのメソッドは単純に定数を返すように最適化されています。
次に、パラメタがあるメソッドの場合です。
; In the case of IsBetterToUseVector256<UInt32>(Int minimumCount):
; equivalent to "return minimumCount <= 8;"
cmp ecx,8
setle al
movzx eax,al
ret
; In the case of IsBetterToUseVector256<UInt64>(Int minimumCount):
; equivalent to "return minimumCount <= 4;"
cmp ecx,4
setle al
movzx eax,al
ret
; In the case of IsBetterToUseVector256<Char>(Int minimumCount):
; equivalent to "return false;"
xor eax,eax
ret
; In the case of IsBetterToUseVector<UInt32>(Int minimumCount):
; equivalent to "return minimumCount <= 8;"
cmp ecx,8
setle al
movzx eax,al
ret
; In the case of IsBetterToUseVector<UInt64>(Int minimumCount):
; equivalent to "return minimumCount <= 4;"
cmp ecx,4
setle al
movzx eax,al
ret
; In the case of IsBetterToUseVector<Char>(Int minimumCount):
; equivalent to "return false;"
xor eax,eax
ret
何れのメソッドも、単に false
を返すか、あるいはパラメタを定数と比較した結果の Boolean
値を返すように最適化されています。
3. まとめ
- c# の言語仕様上は定数ではないが、実行時に定数として最適化されるいくつかのケースについて紹介した。
- c# の言語仕様上は定数式ではない式も、実行時の最適化によって簡略化されることがあることを確認した。
4. おまけ
本稿のテーマとは外れますが、同じ最適化つながりの豆知識を紹介しておきます。
4.1 typeof(T)
同士の '==' または !=
演算子による比較は非常に高速である
このテクニックはジェネリックメソッドなどの実装でよく使われます。
例として、.NET Runtime の MemoryExtensions
クラスのソースコード にある拡張メソッド SequenceCompareTo<T>(this ReadOnlySpan<T>, ReadOnlySpan<T>)
を挙げます。
このメソッドでは、型パラメタ T
が特定の型である場合、その型に特化したコードを実行する仕組みになっており、その際の型の比較を typeof(TYPE1_T) == typeof(TYPE2_T)
のように行っています。
このような型の比較は実行時に JIT によって true
または false
に置換され、比較自体が省略されるので、結果として非常に高速となります。
4.2 単純な演算の最適化
以下のような演算は c# コンパイラあるいは JIT コンパイラによって最適化されるため、非常に高速です。
しかし、用途は非常に限られますので、 「こういう書き方をしても余計なオーバーヘッドにはならない」 ぐらいに覚えておくといいかもしれません。
以下に、UInt32
型の変数 value
を対象としたいくつかの演算に関する最適化の内容について示します。
式 | c# コンパイラの結果 (デコンパイル内容) |
JIT コンパイラの結果 |
---|---|---|
value + 0 |
value |
value |
value - 0 |
value |
value |
value * 0 |
0u |
0u |
value * 1 |
value |
value |
value / 1 |
value / 1 |
value |
value >> 0 |
value |
value |
value << 0 |
value |
value |
value | 0 |
value | 0 |
value |
value | 0xffffffffu |
value | 0xffffffffu |
0xFFFFFFFFu |
value & 0 |
value & 0u |
0u |
value & 0xffffffffu |
value & 0xffffffffu |
value |
以上のことから、例えば4つの Byte
値から1つの UInt32
値を組み立てる以下のようなコードも、表記は冗長ではあるものの、パフォーマンス上は全く問題ないことがわかります。
public static UInt32 MakeUInt32(Byte b0, Byte b1, Byte b2, Byte b3)
=> ((UInt32)b0 << (0 * 8))
| ((UInt32)b1 << (1 * 8))
| ((UInt32)b2 << (2 * 8))
| ((UInt32)b3 << (3 * 8));
ちなみに、このソースコードをコンパイルしてデコンパイルすると以下のような結果になります。
public static uint MakeUInt32(byte b0, byte b1, byte b2, byte b3)
{
return (uint)(b0 | (b1 << 8) | (b2 << 16) | (b3 << 24));
}
5. 本稿で使用したソースコード
Experiment.CSharp.cs (コンソールアプリケーションの Main メソッドがあるクラスのソースファイル)
using System;
using System.Runtime.CompilerServices;
using Experiment.CSharp.Library;
namespace Experiment.CSharp
{
internal sealed class Program
{
[MethodImpl(MethodImplOptions.NoOptimization)]
private static void Main()
{
var plusZero = ExperimentOfConstants.PlusZero(0x12345678U);
var minusZero = ExperimentOfConstants.MinusZero(0x12345678U);
var multiplyZero = ExperimentOfConstants.MultiplyZero(0x12345678U);
var multiplyOne = ExperimentOfConstants.MultiplyOne(0x12345678U);
var divideOne = ExperimentOfConstants.DivideOne(0x12345678U);
var shiftRightZero = ExperimentOfConstants.ShiftRightZero(0x12345678U);
var shiftLeftZero = ExperimentOfConstants.ShiftLeftZero(0x12345678U);
var bitwiseOrAllZero = ExperimentOfConstants.BitwiseOrAllZero(0x12345678U);
var bitwiseOrAllOne = ExperimentOfConstants.BitwiseOrAllOne(0x12345678U);
var bitwiseAndAllZero = ExperimentOfConstants.BitwiseAndAllZero(0x12345678U);
var bitwiseAndAllOne = ExperimentOfConstants.BitwiseAndAllOne(0x12345678U);
var sizeOfInt32 = ExperimentOfConstants.GetSizeOfInt32();
var bitCountOfInt32 = ExperimentOfConstants.GetBitCountOfInt32();
var unsafeSizeOfInt32 = ExperimentOfConstants.GetUnsafeSizeOfInt32();
var unsafeBitCountOfInt32 = ExperimentOfConstants.GetUnsafeBitCountOfInt32();
var unsafeSizeOfUIntPtr = ExperimentOfConstants.GetUnsafeSizeOfUIntPtr();
var unsafeBitCountOfUIntPtr = ExperimentOfConstants.GetUnsafeBitCountOfUIntPtr();
var unsafeSizeOfNFloat = ExperimentOfConstants.GetUnsafeSizeOfNFloat();
var unsafeBitCountOfNFloat = ExperimentOfConstants.GetUnsafeBitCountOfNFloat();
var isWindows = ExperimentOfConstants.IsWindows();
var isLinux = ExperimentOfConstants.IsLinux();
var is64BitProcess = ExperimentOfConstants.Is64BitProcess();
var vector512IsHardwareAccelerated = ExperimentOfConstants.GetVector512IsHardwareAccelerated();
var vector256IsHardwareAccelerated = ExperimentOfConstants.GetVector256IsHardwareAccelerated();
var vectorIsHardwareAccelerated = ExperimentOfConstants.GetVectorIsHardwareAccelerated();
var vector256UInt32IsSupported = ExperimentOfConstants.GetVector256IsSupported<UInt32>();
var Vector256CharIsSupported = ExperimentOfConstants.GetVector256IsSupported<Char>();
var vectorUInt32IsSupported = ExperimentOfConstants.GetVectorIsSupported<UInt32>();
var VectorCharIsSupported = ExperimentOfConstants.GetVectorIsSupported<Char>();
var vector256UInt32Count = ExperimentOfConstants.GetVector256Count<UInt32>();
var vectorUInt32Count = ExperimentOfConstants.GetVectorCount<UInt32>();
var isWindows64bit = ExperimentOfConstants.IsWindows64bit();
var isBetterToUseVector256Uint32 = ExperimentOfConstants.IsBetterToUseVector256<UInt32>();
var isBetterToUseVector256Uint32_8 = ExperimentOfConstants.IsBetterToUseVector256<UInt32>(8);
var isBetterToUseVector256UInt64 = ExperimentOfConstants.IsBetterToUseVector256<UInt64>();
var isBetterToUseVector256UInt64_8 = ExperimentOfConstants.IsBetterToUseVector256<UInt64>(8);
var isBetterToUseVector256Char = ExperimentOfConstants.IsBetterToUseVector256<Char>();
var isBetterToUseVector256Char_8 = ExperimentOfConstants.IsBetterToUseVector256<Char>(8);
var isBetterToUseVectorUint32 = ExperimentOfConstants.IsBetterToUseVector<UInt32>();
var isBetterToUseVectorUint32_8 = ExperimentOfConstants.IsBetterToUseVector<UInt32>(8);
var isBetterToUseVectorUInt64 = ExperimentOfConstants.IsBetterToUseVector<UInt64>();
var isBetterToUseVectorUInt64_8 = ExperimentOfConstants.IsBetterToUseVector<UInt64>(8);
var isBetterToUseVectorChar = ExperimentOfConstants.IsBetterToUseVector<Char>();
var isBetterToUseVectorChar_8 = ExperimentOfConstants.IsBetterToUseVector<Char>(8);
Console.WriteLine($"PlusZero(0x12345678U)=0x{plusZero:x8}");
Console.WriteLine($"MinusZero(0x12345678U)=0x{minusZero:x8}");
Console.WriteLine($"MultiplyZero(0x12345678U)=0x{multiplyZero:x8}");
Console.WriteLine($"MultiplyOne(0x12345678U)=0x{multiplyOne:x8}");
Console.WriteLine($"DivideOne(0x12345678U)=0x{divideOne:x8}");
Console.WriteLine($"ShiftRightZero(0x12345678U)=0x{shiftRightZero:x8}");
Console.WriteLine($"ShiftLeftZero(0x12345678U)=0x{shiftLeftZero:x8}");
Console.WriteLine($"BitwiseOrAllZero(0x12345678U)=0x{bitwiseOrAllZero:x8}");
Console.WriteLine($"BitwiseOrAllOne(0x12345678U)=0x{bitwiseOrAllOne:x8}");
Console.WriteLine($"BitwiseAndAllZero(0x12345678U)=0x{bitwiseAndAllZero:x8}");
Console.WriteLine($"BitwiseAndAllOne(0x12345678U)=0x{bitwiseAndAllOne:x8}");
Console.WriteLine($"GetSizeOfInt32()={sizeOfInt32}");
Console.WriteLine($"GetBitCountOfInt32()={bitCountOfInt32}");
Console.WriteLine($"GetUnsafeSizeOfInt32()={unsafeSizeOfInt32}");
Console.WriteLine($"GetUnsafeBitCountOfInt32()={unsafeBitCountOfInt32}");
Console.WriteLine($"GetUnsafeSizeOfUIntPtr()={unsafeSizeOfUIntPtr}");
Console.WriteLine($"GetUnsafeBitCountOfUIntPtr()={unsafeBitCountOfUIntPtr}");
Console.WriteLine($"GetUnsafeSizeOfNFloat()={unsafeSizeOfNFloat}");
Console.WriteLine($"GetUnsafeBitCountOfNFloat()={unsafeBitCountOfNFloat}");
Console.WriteLine($"IsWindows()={isWindows}");
Console.WriteLine($"IsLinux()={isLinux}");
Console.WriteLine($"Is64BitProcess()={is64BitProcess}");
Console.WriteLine($"GetVector512IsHardwareAccelerated()={vector512IsHardwareAccelerated}");
Console.WriteLine($"GetVector256IsHardwareAccelerated()={vector256IsHardwareAccelerated}");
Console.WriteLine($"GetVectorIsHardwareAccelerated()={vectorIsHardwareAccelerated}");
Console.WriteLine($"GetVector256IsSupported<UInt32>()={vector256UInt32IsSupported}");
Console.WriteLine($"GetVector256IsSupported<Char>()={Vector256CharIsSupported}");
Console.WriteLine($"GetVectorIsSupported<UInt32>()={vectorUInt32IsSupported}");
Console.WriteLine($"GetVectorIsSupported<Char>()={VectorCharIsSupported}");
Console.WriteLine($"GetVector256Count<UInt32>()={vector256UInt32Count}");
Console.WriteLine($"GetVectorCount<UInt32>()={vectorUInt32Count}");
Console.WriteLine($"IsWindows64bit()={isWindows64bit}");
Console.WriteLine($"IsBetterToUseVector256<UInt32>()={isBetterToUseVector256Uint32}");
Console.WriteLine($"IsBetterToUseVector256<UInt32>(8)={isBetterToUseVector256Uint32_8}");
Console.WriteLine($"IsBetterToUseVector256<UInt64>()={isBetterToUseVector256UInt64}");
Console.WriteLine($"IsBetterToUseVector256<UInt64>(8)={isBetterToUseVector256UInt64_8}");
Console.WriteLine($"IsBetterToUseVector256<Char>()={isBetterToUseVector256Char}");
Console.WriteLine($"IsBetterToUseVector256<Char>(8)={isBetterToUseVector256Char_8}");
Console.WriteLine($"IsBetterToUseVector<UInt32>()={isBetterToUseVectorUint32}");
Console.WriteLine($"IsBetterToUseVector<UInt32>(8)={isBetterToUseVectorUint32_8}");
Console.WriteLine($"IsBetterToUseVector<UInt64>()={isBetterToUseVectorUInt64}");
Console.WriteLine($"IsBetterToUseVector<UInt64>(8)={isBetterToUseVectorUInt64_8}");
Console.WriteLine($"IsBetterToUseVector<Char>()={isBetterToUseVectorChar}");
Console.WriteLine($"IsBetterToUseVector<Char>(8)={isBetterToUseVectorChar_8}");
Console.Beep();
Console.WriteLine("Complete");
_ = Console.ReadLine();
}
}
}
Experiment.CSharp.Library.cs (最適化の検証対象を記述するクラスのソースファイル)
このソールファイルは最適化の検証用のため、最適化が適用される範囲を絞り込む目的で、各メソッドに [MethodImpl(MethodImplOptions.NoInlining)]
属性をつけています。
プログラムのロジック自体には影響はなく、むしろパフォーマンスを劣化させますので、本稿のような検証目的でもない限りは、[MethodImpl(MethodImplOptions.NoInlining)]
属性は外すべきです。
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
namespace Experiment.CSharp.Library
{
public static class ExperimentOfConstants
{
#region 基本演算編
// 0 を足す
[MethodImpl(MethodImplOptions.NoInlining)]
public static UInt32 PlusZero(UInt32 value) => value + 0;
// 0 を引く
[MethodImpl(MethodImplOptions.NoInlining)]
public static UInt32 MinusZero(UInt32 value) => value - 0;
// 0 をかける
[MethodImpl(MethodImplOptions.NoInlining)]
public static UInt32 MultiplyZero(UInt32 value) => value * 0;
// 1 をかける
[MethodImpl(MethodImplOptions.NoInlining)]
public static UInt32 MultiplyOne(UInt32 value) => value * 1;
// 1 で割る
[MethodImpl(MethodImplOptions.NoInlining)]
public static UInt32 DivideOne(UInt32 value) => value / 1;
// 0 ビットだけ右シフトする
[MethodImpl(MethodImplOptions.NoInlining)]
public static UInt32 ShiftRightZero(UInt32 value) => value >> 0;
// 0 ビットだけ左シフトする
[MethodImpl(MethodImplOptions.NoInlining)]
public static UInt32 ShiftLeftZero(UInt32 value) => value << 0;
// 0 とビット単位の OR をとる
[MethodImpl(MethodImplOptions.NoInlining)]
public static UInt32 BitwiseOrAllZero(UInt32 value) => value | 0;
// 0xffffffff とビット単位の OR をとる
[MethodImpl(MethodImplOptions.NoInlining)]
public static UInt32 BitwiseOrAllOne(UInt32 value) => value | UInt32.MaxValue;
// 0 とビット単位の AND をとる
[MethodImpl(MethodImplOptions.NoInlining)]
public static UInt32 BitwiseAndAllZero(UInt32 value) => value & 0;
// 0xffffffff とビット単位の AND をとる
[MethodImpl(MethodImplOptions.NoInlining)]
public static UInt32 BitwiseAndAllOne(UInt32 value) => value & UInt32.MaxValue;
#endregion
#region sizeof() 関連
// sizeof(Int32) は実行時に定数扱いされるかどうか ?
[MethodImpl(MethodImplOptions.NoInlining)]
public static Int32 GetSizeOfInt32() => sizeof(Int32);
// sizeof(Int32) * 8 は実行時に定数扱いされるかどうか ?
[MethodImpl(MethodImplOptions.NoInlining)]
public static Int32 GetBitCountOfInt32() => sizeof(Int32) * 8;
// Unsafe.SizeOf<Int32>() は実行時に定数扱いされるかどうか ?
[MethodImpl(MethodImplOptions.NoInlining)]
public static Int32 GetUnsafeSizeOfInt32() => Unsafe.SizeOf<Int32>();
// Unsafe.SizeOf<Int32>() * 8 は実行時に定数扱いされるかどうか ?
[MethodImpl(MethodImplOptions.NoInlining)]
public static Int32 GetUnsafeBitCountOfInt32() => Unsafe.SizeOf<Int32>() * 8;
// Unsafe.SizeOf<UIntPtr>() は実行時に定数扱いされるかどうか ?
[MethodImpl(MethodImplOptions.NoInlining)]
public static Int32 GetUnsafeSizeOfUIntPtr() => Unsafe.SizeOf<UIntPtr>();
// Unsafe.SizeOf<UIntPtr>() * 8 は実行時に定数扱いされるかどうか ?
[MethodImpl(MethodImplOptions.NoInlining)]
public static Int32 GetUnsafeBitCountOfUIntPtr() => Unsafe.SizeOf<UIntPtr>() * 8;
// Unsafe.SizeOf<NFloat>() は実行時に定数扱いされるかどうか ?
[MethodImpl(MethodImplOptions.NoInlining)]
public static Int32 GetUnsafeSizeOfNFloat() => Unsafe.SizeOf<NFloat>();
// Unsafe.SizeOf<NFloat>() * 8 は実行時に定数扱いされるかどうか ?
[MethodImpl(MethodImplOptions.NoInlining)]
public static Int32 GetUnsafeBitCountOfNFloat() => Unsafe.SizeOf<NFloat>() * 8;
#endregion
#region 実行環境編
// OperatingSystem.IsWindows() は実行時に定数扱いされるかどうか ?
[MethodImpl(MethodImplOptions.NoInlining)]
public static Boolean IsWindows() => OperatingSystem.IsWindows();
// OperatingSystem.IsLinux() は実行時に定数扱いされるかどうか ?
[MethodImpl(MethodImplOptions.NoInlining)]
public static Boolean IsLinux() => OperatingSystem.IsLinux();
// Environment.Is64BitProcess は実行時に定数扱いされるかどうか ?
[MethodImpl(MethodImplOptions.NoInlining)]
public static Boolean Is64BitProcess() => Environment.Is64BitProcess;
#endregion
#region Vector 編
// VectorXXX.IsHardwareAccelerated は実行時に定数扱いされるかどうか ?
[MethodImpl(MethodImplOptions.NoInlining)]
public static Boolean GetVector512IsHardwareAccelerated() => Vector512.IsHardwareAccelerated;
// VectorXXX.IsHardwareAccelerated は実行時に定数扱いされるかどうか ?
[MethodImpl(MethodImplOptions.NoInlining)]
public static Boolean GetVector256IsHardwareAccelerated() => Vector256.IsHardwareAccelerated;
// Vector.IsHardwareAccelerated は実行時に定数扱いされるかどうか ?
[MethodImpl(MethodImplOptions.NoInlining)]
public static Boolean GetVectorIsHardwareAccelerated() => Vector.IsHardwareAccelerated;
// VectorXXX<ELEMENT_T>.IsSupported は実行時に定数扱いされるかどうか ?
[MethodImpl(MethodImplOptions.NoInlining)]
public static Boolean GetVector256IsSupported<ELEMENT_T>() => Vector256<ELEMENT_T>.IsSupported;
// Vector<ELEMENT_T>.IsSupported は実行時に定数扱いされるかどうか ?
[MethodImpl(MethodImplOptions.NoInlining)]
public static Boolean GetVectorIsSupported<ELEMENT_T>() => Vector<ELEMENT_T>.IsSupported;
// Vector256<ELEMENT_T>.Count は実行時に定数扱いされるかどうか ?
[MethodImpl(MethodImplOptions.NoInlining)]
public static Int32 GetVector256Count<ELEMENT_T>() => Vector256<ELEMENT_T>.Count;
// Vector<ELEMENT_T>.Count は実行時に定数扱いされるかどうか ?
[MethodImpl(MethodImplOptions.NoInlining)]
public static Int32 GetVectorCount<ELEMENT_T>() => Vector<ELEMENT_T>.Count;
#endregion
#region 応用編
// 4つの Byte 値から Uint32 値を組み立てる。
// 冗長な演算は省略されるか ?
[MethodImpl(MethodImplOptions.NoInlining)]
public static UInt32 MakeUInt32(Byte b0, Byte b1, Byte b2, Byte b3)
=> ((UInt32)b0 << (0 * 8))
| ((UInt32)b1 << (1 * 8))
| ((UInt32)b2 << (2 * 8))
| ((UInt32)b3 << (3 * 8));
// Windows かつ 64bit かどうかを調べる。
// 復帰値はどこまで最適化されるか ?
[MethodImpl(MethodImplOptions.NoInlining)]
public static Boolean IsWindows64bit() => OperatingSystem.IsWindows() && Environment.Is64BitProcess;
// Vector256<ELEMENT_T> を積極的に使用すべきかどうかを調べる。
// 復帰値はどこまで最適化されるか ?
[MethodImpl(MethodImplOptions.NoInlining)]
public static Boolean IsBetterToUseVector256<ELEMENT_T>() => Vector256.IsHardwareAccelerated && Vector256<ELEMENT_T>.IsSupported && Vector256<ELEMENT_T>.Count >= 8;
// Vector256<ELEMENT_T> を積極的に使用すべきかどうかを調べる。
// 復帰値はどこまで最適化されるか ?
[MethodImpl(MethodImplOptions.NoInlining)]
public static Boolean IsBetterToUseVector256<ELEMENT_T>(Int32 minimumCount) => Vector256.IsHardwareAccelerated && Vector256<ELEMENT_T>.IsSupported && Vector256<ELEMENT_T>.Count >= minimumCount;
// Vector<ELEMENT_T> を積極的に使用すべきかどうかを調べる。
// 復帰値はどこまで最適化されるか ?
[MethodImpl(MethodImplOptions.NoInlining)]
public static Boolean IsBetterToUseVector<ELEMENT_T>() => Vector256.IsHardwareAccelerated && Vector<ELEMENT_T>.IsSupported && Vector<ELEMENT_T>.Count >= 8;
// Vector<ELEMENT_T> を積極的に使用すべきかどうかを調べる。
// 復帰値はどこまで最適化されるか ?
[MethodImpl(MethodImplOptions.NoInlining)]
public static Boolean IsBetterToUseVector<ELEMENT_T>(Int32 minimumCount) => Vector256.IsHardwareAccelerated && Vector<ELEMENT_T>.IsSupported && Vector<ELEMENT_T>.Count >= minimumCount;
#endregion
}
}
Experiment.CSharp.Library.cs をコンパイルしてデコンパイルした結果
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
namespace Experiment.CSharp.Library;
public static class ExperimentOfConstants
{
[MethodImpl(MethodImplOptions.NoInlining)]
public static uint PlusZero(uint value)
{
return value;
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static uint MinusZero(uint value)
{
return value;
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static uint MultiplyZero(uint value)
{
return 0u;
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static uint MultiplyOne(uint value)
{
return value;
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static uint DivideOne(uint value)
{
return value / 1;
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static uint ShiftRightZero(uint value)
{
return value;
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static uint ShiftLeftZero(uint value)
{
return value;
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static uint BitwiseOrAllZero(uint value)
{
return value | 0u;
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static uint BitwiseOrAllOne(uint value)
{
return value | 0xFFFFFFFFu;
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static uint BitwiseAndAllZero(uint value)
{
return value & 0u;
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static uint BitwiseAndAllOne(uint value)
{
return value & 0xFFFFFFFFu;
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static int GetSizeOfInt32()
{
return 4;
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static int GetBitCountOfInt32()
{
return 32;
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static int GetUnsafeSizeOfInt32()
{
return Unsafe.SizeOf<int>();
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static int GetUnsafeBitCountOfInt32()
{
return Unsafe.SizeOf<int>() * 8;
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static int GetUnsafeSizeOfUIntPtr()
{
return Unsafe.SizeOf<nuint>();
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static int GetUnsafeBitCountOfUIntPtr()
{
return Unsafe.SizeOf<nuint>() * 8;
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static int GetUnsafeSizeOfNFloat()
{
return Unsafe.SizeOf<NFloat>();
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static int GetUnsafeBitCountOfNFloat()
{
return Unsafe.SizeOf<NFloat>() * 8;
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static bool IsWindows()
{
return OperatingSystem.IsWindows();
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static bool IsLinux()
{
return OperatingSystem.IsLinux();
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static bool Is64BitProcess()
{
return Environment.Is64BitProcess;
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static bool GetVector512IsHardwareAccelerated()
{
return Vector512.IsHardwareAccelerated;
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static bool GetVector256IsHardwareAccelerated()
{
return Vector256.IsHardwareAccelerated;
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static bool GetVectorIsHardwareAccelerated()
{
return Vector.IsHardwareAccelerated;
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static bool GetVector256IsSupported<ELEMENT_T>()
{
return Vector256<ELEMENT_T>.IsSupported;
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static bool GetVectorIsSupported<ELEMENT_T>()
{
return Vector<ELEMENT_T>.IsSupported;
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static int GetVector256Count<ELEMENT_T>()
{
return Vector256<ELEMENT_T>.Count;
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static int GetVectorCount<ELEMENT_T>()
{
return Vector<ELEMENT_T>.Count;
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static uint MakeUInt32(byte b0, byte b1, byte b2, byte b3)
{
return (uint)(b0 | (b1 << 8) | (b2 << 16) | (b3 << 24));
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static bool IsWindows64bit()
{
if (OperatingSystem.IsWindows())
{
return Environment.Is64BitProcess;
}
return false;
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static bool IsBetterToUseVector256<ELEMENT_T>()
{
if (Vector256.IsHardwareAccelerated && Vector256<ELEMENT_T>.IsSupported)
{
return Vector256<ELEMENT_T>.Count >= 8;
}
return false;
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static bool IsBetterToUseVector256<ELEMENT_T>(int minimumCount)
{
if (Vector256.IsHardwareAccelerated && Vector256<ELEMENT_T>.IsSupported)
{
return Vector256<ELEMENT_T>.Count >= minimumCount;
}
return false;
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static bool IsBetterToUseVector<ELEMENT_T>()
{
if (Vector256.IsHardwareAccelerated && Vector<ELEMENT_T>.IsSupported)
{
return Vector<ELEMENT_T>.Count >= 8;
}
return false;
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static bool IsBetterToUseVector<ELEMENT_T>(int minimumCount)
{
if (Vector256.IsHardwareAccelerated && Vector<ELEMENT_T>.IsSupported)
{
return Vector<ELEMENT_T>.Count >= minimumCount;
}
return false;
}
}