9.1K Views
April 27, 24
スライド概要
C#ベクトルプログラミ ング入門を読んで UNSAFEについて 調べてみた 2024/04/27 C#パフォーマンス勉強会 松井 敏
自己紹介 • 名前 : 松井 敏 a.k.a @moririring • 職業 : Codeer、HACARUS(副業) • 著書 : Unity5 3Dゲーム開発講座 • 表彰 : Microsoft MVP for Developer Technologies • コミュニティ : C#読書会主催、Greek Alphabet Software Academy TA
C#ベクトルプログラミング入門について • C#読書会でC#ベクトルプログラミング入門を読んでいる。 • この本は、SIMD(Single Instruction Multiple Data)をC#で使うための本 • System.Runtime.Intrinsics (イントリンシック)の命令(例えばAvx.Addなど)で、1回に 256ビット処理するプログラムが書ける • 例えばfloat型の配列なら256ビット / 32ビットで8個まとめて扱える。…単純に8倍 速くなるではないが、状況によっては2-8倍以上速くなることもある
C#ベクトルプログラミング入門について • System.Runtime.Intrinsicsの命令は一杯あって、本ではその説明が一番長い(3割 弱)。 • 足し算や掛け算は兎も角、これ何に使うんだろうという命令、例えば偶数のみ複 製とかアンパッチしてインターリーブする(?)命令とかまである。 • しかも説明の最初に「これはごく一部です」と書いてある。 • 正直、読書会でもその章は途中で止めた。
UNSAFE • イントリンシックの命令を扱う際に本の中でunsafe, fixed, stackallocなどが出てくる • 自分はC#歴は10年以上はあるが、業務でunsafeを使ったことはない • もちろんunsafeを使えば速く出来るケースはあったと思うが結果的には使わずに やってきたため知識が足りてない。。 • この本をきっかけにunsafeについて色々学びなおし
質問 • /unsafe オプションを付けたことがある人? • 実際にunsafeのコードを書いたことある人? • C#でポインターを使ったことがある人? • unsafeを使ってパフォーマンスが向上したことがある人?
UNSAFE=ポインタ? • そもそもunsafeは何のために使うのか? • 個人的にはunsafe = ポインタを使いたいからという印象 • MS Learnにもアンセーフコードとポインタという記事がある
ポインタが速い? • ただ、ちょっと考えみるとポインタはあくまで受け方 • 参照だってコピーは発生しない • 使っただけで直ちに速くなるというわけではない…はず • でも、正直、自分もポインタにするだけで速くなるというイメージはあった。。 代償(unsafe)を払ったのだから、それに見合うだけの力(パフォーマンス)を得て当 然みたいな
ポインタ以外 • unsafeにおけるポインタ以外のキーワード。 • fixedとstackalloc
FIXED • fixed ステートメント - ポインター操作のために変数を固定する • fixed ステートメントを使うと、ガベージ コレクターによる移動可能変数の再配 置を防ぎ、その変数へのポインターを宣言することができます。 固定 (またはピ ン留め) された変数のアドレスは、そのステートメントの実行中に変わりません。 宣言されたポインターは、対応する fixed ステートメント内でのみ使用できます。 宣言されたポインターは読み取り専用であり、変更できません。
STACKALLOC • stackalloc 式 (C# リファレンス) • stackalloc 式を使用すると、スタックにメモリ ブロックを割り当てることができ ます。 メソッドの実行中に作成されてスタックに割り当てられたメモリ ブロッ クは、そのメソッドが戻るときに自動的に破棄されます。 stackalloc を使用して 割り当てられたメモリを明示的に開放することはできません。 スタックに割り 当てられたメモリ ブロックは、ガベージ コレクションの対象外であり、fixed ス テートメントを使用してピン留めする必要はありません。
ガベージコレクション • 速度向上のコツの一つがガベージコレクションを避けること • fixedとstackallocを使うとまさにガベージコレクションを避けることが出来る
NEW VS FIXED VS STACKALLOC • SharpLab • やっていることは一緒 • 1番目は普通にnew • 2番目はfixed • 3番目はstackalloc • これを100回。大体1 < 2 < 3で速い
スタックとヒープ • fixedとstackallocの違い。 • stackallocはスタック領域、fixedはヒープ領域からメモリを確保。 • スタックは領域をそのまま使うが、ヒープはアドレスから実体を見に行くので、 一般的にスタックはヒープよりも速い
STACKALLOCの特徴 • ただし、スタックは無条件で使えるわけでない • 例えば幅=2000,高さ=2000にしたら?
STACKALLOCの受け方 • 因みにポインタで受けなければ、stackalloc自体はunsafeは必要ない • ポインタを使わない場合はSpan<T>で受ける • Span<byte> content = stackalloc byte[1000];
SPAN<T> • Span<T> 構造体 • メモリの連続した領域にアクセスするために使う型です。 Span<T> インスタン スは、T 型の配列、String、stackalloc を使って割り当てたバッファー、またはア ンマネージ メモリへのポインターによってバックアップできます。 これはス タック上に割り当てる必要があるため、いくつかの制限があります。 たとえば、 クラス内のフィールドを Span<T> 型にすることはできませんし、非同期操作で スパンを使うこともできません。
BYTE[] VS * VS SPAN<T> • SharpLab • 1はnewをbyte配列 • 2はnewをSpan<T> • 3はstackallocをbyte* • 4はstackallocをSpan<T> • 1 < 2は絶対速い。3 < 4はほんの少し 速い
VARクイズ • varの型は? • var a1 = new byte[w * h]; • Answer: byte[] • var a2 = stackalloc byte[w * h]; • Answer: byte* • var a3 = (w * h > 1000) ? new byte[w * h] : stackalloc byte[w * h]; • Answer: Span<byte> • var a4 = (stackalloc byte[w * h]); • Answer: Span<byte>
属性 • あとパフォーマンスに [SkipLocalsInit]が効くケースもある
[SKIPLOCALSINIT] • SkipLocalsInitAttribute クラス • この属性は安全ではありません。これは、初期化されていないメモリが特定のイ ンスタンスでアプリケーションに表示される可能性があるためです (たとえば、 初期化されていないスタック割り当てメモリからの読み取り)。 メソッドに直接 適用された場合、属性はそのメソッドと、ラムダやローカル関数を含むすべての 入れ子になった関数に適用されます。 型またはモジュールに適用された場合は、 内部に入れ子になっているすべてのメソッドに適用されます。 この属性は、ア センブリでは意図的に許可されていません。 属性を複数の型宣言に適用するに は、代わりにモジュール レベルで使用します。
BENCHMARKDOTNET • Stopwatchでは結果がばらついたので、BenchmarkDotNetを使用するとほぼ SkipLocalsInitはパフォーマンスに効く結果だった。 • BenchmarkDotNetは属性を指定して実行するだけで、時間はかかるがかなり正確 なベンチマークが取れる
ベクトルプログラミングとUNSAFE • ベクトルプログラミング入門は基本unsafeを使っているが、Span<T>のように unsafeを書かなくても済むようになってきている。 • 実際.NET7であればunsafeを使わなくてもベクトルプログラミングを書くことは可 能 • Learnに.NET8のところに「NET 7 ではクロスプラットフォーム ハードウェアの組 み込みが追加されました」と書いてあるが、公式にこの辺のことが書いてある文 章は見つけられてない。。 • 特にハード命令ではなくVectorのサイズで分岐できるようなっているのは大きい
.NET6 VS .NET7.2以降 • 速度も.NET7の方が速かった。 • unsafeを使わない場合MarshalクラスやUnsafeクラスを使う必要がある。
VECTOR512 • .NET8から512ビット扱えるVector512<T>も使えるようになった。同じ書き方でさ らに倍に出来るかも? • C#ベクトルプログラミング入門の作者が書いたC#ではなさそうだが、 512ビッ ト・ベクトルプログラミング入門という本もある。
AVX512が使えるCPU
VECTOR512 • 自分のノートPC、デスクトップPC、会社のPCは全て512ビットは非対応だった。 • 大体ここ2,3年で買ったPCなので、まだまだ非対応のPCの方が多いと思う • 調べた感じだと対応しているCPUは単体で2万ぐらいだった • あと数年はいるかなーという印象
まとめ • unsafeでfixedを使ってポインタで受ければGCが避けられる。 • サイズが小さい場合はそれよりも、stackallocを使って、Span<T>で受けた方が速 いし、unsafeを使う必要がない。但し、.NET7.2以降。 • ベクトルプログラミングも.NET 7以降であればunsafeを使わなくても書くことが 出来る。新しめのPCで.NET 8以降であればVector512も使用可能。
まとめ • 知識は大事。自分がunsafeを使ってなかったのは単なる怠惰。使えば速くなる機 会はあったはずだが、無知によって使う機会をなくしていた。。 • 今回プロファイルも取ってみたけれど、使いたいシチュエーションが変わればま た結果は変わる。あくまで今回の状況限定の話なので、実践とは違う • 知識は大事だけど、実践していない知識は使いどころが限られる。 • 自分の今の知識と経験では、unsafeやポインタに関してまとまったことはあまり 言えない。。。
参考 • C#ベクトルプログラミング入門 • .NET 7こそがC# SIMDプログラミングを始めるのに最適である理由 • 気づいたら、C# が C++ の速度を凌駕している! • C#はunsafeの方が速いという幻想 • Span<T>を使うべき5つの理由 • 【.NET】C#でSIMDを使った高速演算 • 【C#】Unsafeクラスで安全で危険なコードを書こう() • 【C#】unsafeコードを書いてみよう • ++C++ - unsafe • ++C++ - Span構造体