水底

ScalaとかC#とかk8sとか

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クラス

  • 不要なvirtualJITコンパイラによるインライン化のような最適化を妨げてしまうので避けるべき
  • sealedを付けることでJITコンパイラによる最適化がかかりやすくなるので検討すべき
  • 当然だがどちらも柔軟性が落ちるので, 特にライブラリ化するときは要注意

Interface Dispatch

ある基底インターフェースを実装した多数の具象クラスが存在する時, 基底インターフェース経由で異なる具象クラスを呼び出すと, 単一の具象クラス呼び出しより少しだがコストがかかる. インターフェースの代わりに抽象クラスを使うことを検討する.

↓この辺りの話

ufcpp.net

blogs.msdn.microsoft.com

Boxingを避ける

  • 値型をやり取りする時に不用意にobjectを介さない(まぁこれはレガシーなAPIを使ってなければ普通は大丈夫でしょう)
  • structをその基底インターフェースとして扱うとboxingが発生してしまう. このような操作が多い場合は雑にclassに置き換えてしまうと良いだろう
  • ちなみに値型の参照渡しはboxingは起こらない

forforeach

  • 常に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 Eventsboxingを見つけて対処していく. 手軽に使えるのはGUIのPerfViewかな? JIT_newの過剰な呼び出しからboxingも当たりをつけられる. 後はCommunity Editionだと機能は限られるがVisual Studio同梱のプロファイラもちょっと確認する分には便利だろう.

大体元記事で重要そうだった部分はまとめられたかな? 言及されていなかったが汎用的で重要な最適化ポイントは非同期系辺りだろうか.