C#/.Netでパフォーマンスのために意識すべきこと
この記事は Performance Considerations of Class Design and General Coding in .NET - CodeProject を雑に日本語でまとめて微妙に参考を付加しただけです. 元記事が2014年なのでちょっと古い.
struct
について
↓の方がよくまとまっている ufcpp.net
が, ↑にも書かれていない踏み込んだ点として, 以下がある
class
の場合, インスタンスごとに32/64bit環境でそれぞれ8/16bytesのオーバーヘッドが発生するがstruct
はそれが一切ない- 大前提として
struct
は値型なので基本的にstack上に置かれる- ただし,
class
のメンバとして扱われる場合はheap上に置かれる
- ただし,
Equals(object)
,GetHashCode()
だけでなくT#IEquatable<T>
のEquals(T)
も実装しないとboxingが発生する- ただし, メンバが全てblittableなら最適化がかかる(byteレベルの比較になる)ので不要
- [個人的な補足] 仕様変更時の見落としより実装のコスト方が遥かに低いので常に実装すべき
virtual
メソッドとsealed
クラス
- 不要な
virtual
はJITコンパイラによるインライン化のような最適化を妨げてしまうので避けるべき sealed
を付けることでJITコンパイラによる最適化がかかりやすくなるので検討すべき- 当然だがどちらも柔軟性が落ちるので, 特にライブラリ化するときは要注意
Interface Dispatch
ある基底インターフェースを実装した多数の具象クラスが存在する時, 基底インターフェース経由で異なる具象クラスを呼び出すと, 単一の具象クラス呼び出しより少しだがコストがかかる. インターフェースの代わりに抽象クラスを使うことを検討する.
↓この辺りの話
Boxingを避ける
- 値型をやり取りする時に不用意に
object
を介さない(まぁこれはレガシーなAPIを使ってなければ普通は大丈夫でしょう) struct
をその基底インターフェースとして扱うとboxingが発生してしまう. このような操作が多い場合は雑にclass
に置き換えてしまうと良いだろう- ちなみに値型の参照渡しはboxingは起こらない
for
かforeach
か
- 常に
for
の方が同等か速い- が, 多くの場合
foreach
を使っても最適化の過程でfor
と等価になる
- が, 多くの場合
- 抽象化とパフォーマンスのトレードオフ
キャスト
- 言わずもがな極力避けるべき
- 特にダウンキャスト(基底=>派生)はコストも高く
InvalidCastException
の可能性もあるのでより避けるべき - どうしてもの場合は
as
,is
演算子を適宜使う- どちらも大差ないが, 複数の型をチェックするなら
is
の方が一般的
- どちらも大差ないが, 複数の型をチェックするなら
P/Invoke
- 当然managedな関数の呼び出しに比べてコストが大きい
- チューニング
- 1回の呼び出しで多くのことをさせるべき(ループ内で繰り返し呼び出すようなことは避ける)
- 受け渡すデータは出来る限りマーシャリングコストが低いblittableな型にすべき
- .Netの文字列はUnicodeなので, Windows APIsを叩くときはANSIではなくUnicode版を使う
- 不用意にPinningせぞ, するとしてもできる限り短くすること
- 大量のデータを扱うときは前向きに検討すること
- 信頼できるコードであり扱うデータも適切にサニタイズされているのであれば, セキュリティチェックを外す
[System.Security.SuppressUnmanagedCodeSecurity]
属性を付与することで高速化が期待できる
デリゲート
実行時オーバーヘッドは軽微だが, 作成時のコストがそこそこある. ループ内で繰り返し呼び出すような場合はループ外で作成する
例外
try
自体は殆どコストが掛からないが, Throwが当然遅い. 空のメソッド呼び出しと比べると数千倍遅い.
dynamic
当然遅いのでできる限り避ける
↓このあたりの話
Dynamic Language Runtime Overview
コード生成(と題されているが実際には動的コード実行と生成の話)
- 動的なクラスインスタンス生成は
Activator.CreateInstance
で, メソッド呼び出しはMethodInfo.Invoke
で行えるが遅い. CPU負荷だけでなく不要なメモリアロケーションによりGCにも負担をかける - プログラムとそのエクステンションでプログラム側からエクステンションのコードを動的に実行する必要があるような場合は, 共通するインターフェースを通して行うと良い
- [個人的な補足] MEFを使おうなという話
- 共通インターフェースでの呼び出しができないような場合はコード生成が選択肢に入るかもしれない. 以下コード生成の話
- 複数回生成する必要がある場合は生成用のデリゲートをキャッシュしておくことでオーバーヘッドを減らすことができる
- 元記事では
System.Reflection.Emit
を利用したキャッシュ有りで動的な呼び出し方法が紹介されている - [個人的な補足]
System.Reflection.Emit
もいいが, 記述・メンテナンスコストが割に合わないので大人しく式木使ったほうが良い事がほとんどだろう 参考
- 元記事では
プリプロセッシング
外部データを扱う場合, データに何らかの変換が必要ならばできる限り事前に行うべき. 関心の分離
プロファイリング
この辺りのツールを使って例外情報のException Thrown Eventsやboxingを見つけて対処していく. 手軽に使えるのはGUIのPerfViewかな? JIT_new
の過剰な呼び出しからboxingも当たりをつけられる. 後はCommunity Editionだと機能は限られるがVisual Studio同梱のプロファイラもちょっと確認する分には便利だろう.
大体元記事で重要そうだった部分はまとめられたかな? 言及されていなかったが汎用的で重要な最適化ポイントは非同期系辺りだろうか.