Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

最速C# 7.x

1,698 views

Published on

C#7.x で安全かつ高速なコードを記述するためのいろいろをまとめました。

Published in: Engineering
  • Be the first to comment

最速C# 7.x

  1. 1. 最速C#7.x 2018/08/06 株式会社コアコンセプトテクノロジー HQ事業部 山本礼貴
  2. 2. アジェンダ  前提  C#7.xで変わったstructと引数  どうしたら速くなるか  C#7.xでrefを使いこなす  すべてはスタックを使いこなすため  まとめ
  3. 3. 前提 ここで扱う話と扱わない 話について
  4. 4. 前提(1)  C#7.3前提  メジャーバージョン(7.0)に拘るのは残念なこと  言語開発がアジャイル化しているので、最新版に触れるのは 重要
  5. 5. 前提(2)  高速化とは(原則)  コピーを減らすこと(今回の要点1)  GCを極力動かさないこと(今回の要点2)  キャッシュが効きやすいコードを書くこと  ロジックの簡素化  ループの簡素化  e.t.c…(これらは別の機会に)  並列化すること  これは別の機会に
  6. 6. 前提(3)  安全なコードでなければならない  unsafeコードやポインタは使わない  一般に危険とされるコードは使わない  これまで通り、C#を使う人にとってC#は安全な言語
  7. 7. 前提(4) readonly なフィールドは必要  変数の保護が無かったらC言語と変わらない。  フィールドの保護のためにアクセシビリティやプロパティが あった  変数を保護できない言語はチームで扱うと難易度が高いのは 自明  だからunsafeでポインタを使うのは、C#ではメンテナンス性 を犠牲にする行為として扱われる  ref を使うことすらデザインとして非推奨だった  速度を犠牲にするのかという問題とは常に向き合う必要があった
  8. 8. C#7.xで 変わった structと 引数 構造体とスタックの扱い 方をエンジニアが細かく 制御できるようになった しかし、無条件に速くは ならない
  9. 9. readonlyがstruct全体の属性と して使用できる  言語的に構造体全体のImmutable<不変性>を保証する書き 方ができるようになった  readonly struct と書くと、それはImmutableとなる  例えばthis代入はコンパイルエラーになるようになった  readonly struct Foo{ public readonly int _x; public Foo(x) => _x = x; public void Update(int x) => this = new Foo(x); }  structではエラーとならない(従来のC#では readonly フィー ルドは書き換え可能だったということ)
  10. 10. でもreadonlyってたしか…  ref で渡せないよね?  readonly のものは参照渡しをすることが出来ない  readonly int x; foo(ref x); // コンパイルエラー  値の保護と引き換えに値渡し(コピー)しかできないということ  コピーとはそのまま処理時間の増加を意味する  内部を書き換えるメソッド呼んでも、書き換えできない  readonly Rect rect; rect.Offset(3, 3); // メソッドは呼ばれるけど変更されない (defensive copyというやつ)
  11. 11. Defensive Copyとは?  readonly の値型フィールドのメソッド、プロパティが使用 される場合、フィールドの値を保護するためにスタック上 にコピーを作る仕組み  readonly は安全のために必要なので、やめるわけにいいかな い(なぜなら以下のようなコードが実行可能だから)  class Foo { readonly Rect _rect = new Rect(); void Process() { _rect.Offset(2, 2); } }  Foo.Process() 実行時に _rect を守るには暗黙的にコピーするし かない。  コピーとはそのまま処理時間の増加を意味する
  12. 12. in引数が追加された  readonly 属性を持つものを参照渡しできる  readonly ref という名前も検討されていたが out との対として in の名称が採用された  メソッド側だけ in を書けば、呼び出し側は意識せずに使える  変数、フィールド、プロパティなど何でも渡せる  readonlyのフィールドでも渡せる(もしかして速くなった?)
  13. 13. inパラメータ万能?速い?  ref を書かないでも値型を参照渡ししてくれる  値型をreadonly参照で受け取る  参照してくれる  readonly のフィールドも渡せる  参照してくれる  プロパティを渡せる  スタックに1回コピーを作って、それを参照してくれる(横着を認 めてくれる)  何故か遅い  ベンチマークをしたら値で渡すより遅いことが多い
  14. 14. またもや defensive copy  in で参照を受け取るとそのパラメータは readonly 属性を持 つ  メソッド/プロパティを使うとdefensive copyでスタックにコ ピーを生成する  値をそのまま渡した場合より悪い結果に  標準.NET Frameworkのstructは全部プロパティにすら触れない有 様  コピーとはそのまま処理時間の増加を意味する  もう、どうすりゃいいの?(←C#7.2を弄り始めたころの自分)
  15. 15. どうしたら 速くなるか 解決編
  16. 16. readonly struct + inパラメータ  in にしてまともに機能するのは readonly structのみ  値だけではなく型自体を readonly にするという仕様が追加さ れて、初めて defensive copy が無くなった  this への代入もできなくなっている!  スタック上に値を保持できる  ついでにコールスタック経路にあるオブジェクトはGCによって移 動されない  これだけで「コピーをしない」「GCをなるべく動かさない」 という原則の2つに則る
  17. 17. readonly struct の上手な使い方  16バイト超から readonly struct(64bitの場合)  不変の値型としてnew できるのでフィールドをgetter/setterで ラッピングしない  静的解析でケチがつくので、場所を絞ってCA1051警告を抑止する  最適化が爆速で効く(後程ベンチマークあり)  標準の構造体のImmutable型を自作(ex. Rect -> ImmutableRect)  implicitで自動変換可能にしておく  自作のメソッドは in ImmutableXxx の形で受ける  呼び出し前で1回暗黙キャストがかかるが、2-3回程度メンバーに アクセスするだけでお釣りが来るほど高速  内部ではひたすら in で渡す
  18. 18. ベンチマーク Processの引数の受け止め方によって速度がどのように変わるでしょ うか。 in引数にしたり、自作のImmutableRectに暗黙のキャストをしたり、 最初からImmutableRectで扱ったりして、所要時間を比較します。 https://github.com/nenaaki/ReadonlyStructBenchmark
  19. 19. ベンチマークの強い味方  .NET Frameworkでベンチマークを書く際は、 BenchmarkDotNetがおすすめ。
  20. 20. ベンチマーク結果 元の形 引数 修飾 読取り元 ImmutableRect キャスト 平均値 (us) Rect Property 327.52 Rect in Property 325.94 Rect Property あり 352.24 Rect in Property あり 319.82 ImmutableRect Property 234.00 ImmutableRect in Property 218.57 Rect Local var 172.55 Rect in Local var 285.42 ImmutableRect in Local var 101.10 Rect に つ い て は キ ャ ス ト し て も 速 い Defensivecopy の せ い で 遅 い
  21. 21. C#7.xで refを使いこ なす
  22. 22. refを使うと何が良いか  値のコピーをせずに参照で処理できる  GCの動作頻度を下げることができる  stackalloc / Span<T> の合わせ技で(後述)
  23. 23. ref戻り値/refローカル変数  コレクション系を高速化できる可能性がある  「値の取得→書き換え→書き戻し」の流れを「参照の取得→ 書き換え」に変えることができる  foreachなどを高速化する。ループの都度コピーが生じない  注意点  値がコピーされないので高速になるが、中身を書き換えるとコレ クションが変化することに注意。  HashCodeが変化するので、HashSetやDictionaryには禁止  安全性を考慮すると、readonly struct のみに適用するのが 良い  非readonlyでも限定的なところでは有用なのでちょっと紹介
  24. 24. ベンチマーク メソッド名 実行時間(us) UpdateRectRefArray 1.245 (配列より速い?) UpdateRectArray 1.279
  25. 25. AsRef<T> public readonly struct ReferenceArray<T> { private readonly T[] _array; public ReferenceArray(T[] array) => _array = array; public ReferenceArrayEnumerator<T> GetEnumerator() => new ReferenceArrayEnumerator<T>(_array); public struct ReferenceArrayEnumerator<T> { private int _index; private readonly T[] _array; public ReferenceArrayEnumerator(T[] array) => (_index, _array) = (-1, array); public ref T Current => ref _array[_index]; public bool MoveNext() => ++_index < _array.Length; } } public static class ReferenceArrayExtensions { public static ReferenceArray<T> AsRef<T>(this T[] array) => new ReferenceArray<T>(array); } 参考: https://ufcpp.net/study/csharp/sp_ref.html IEnumerable<T>やIEnamurator<T> を実装する必要は実はない GetEnumeratorとMoveNextとCurrentがあれば foreach できる
  26. 26. すべては スタックを 使いこなす ため
  27. 27. stackalloc/Span<T>  これから深い闇を生みそうな機能  .NET のスタック領域は割と小さいのだが、StackOverflowを正しく 処理できるエンジニアは世に少ないと思う  10000くらい処理対象があるときに、100~1000刻みくらいの 処理をTaskに分割して、タスク内ではスタック中心で処理する。  TaskのオーバーヘッドもGCのオーバーヘッドも減らせる。  Span<T>は.NET Core 2.1まではやや遅い  ベンチマークはこのサイトにありますので参考に https://blog.meilcli.net/2018/06/cstackallocstruct.html  これも ref をどれだけ使いこなせるか勝負  どんどんC++組んでる気分になっていく
  28. 28. ref struct  ヒープに配置できない構造体  つまりGCが動かないスタイルでコード記述が可能  Span<T>とstackallocのために作られたもの  一時的な情報を処理するために速度だけを追求した作りが 望ましい。そして外部からは常に ref で扱う。
  29. 29. その他小ネタ  ValueTupleを使うとヒープを使わないのでGCの動作頻度を下 げることが出来る  小さなclassはstruct版も作ると良い  readonly structとユーティリティメソッドの集合という構成にして おくとかなり良い  引数に this をではなく ref this を使うと拡張メソッドも高速になる  毎秒に数万~数億回呼ぶようなコアなところを先に手がけよう  VisualStudioの分析ツールを活用  必ずUTとベンチマークを作ること  BenchmarkDotNet(ベンチマークはこれ1つでいい:無料)  AxoCover(コードカバレッジ検査用のプラグイン:無料)
  30. 30. まとめ C++に近づいていくなぁ
  31. 31. 速くするためには(遅くしないためには)  標準ライブラリに対してはC#7.xの拡張は効果が薄い  Defensive copy を発生させない状況のみで効果があるため  基盤コードをゴリゴリとスクラッチするときに、絶大な威 力を発揮する  readonly structを作る(徹底的に in 引数とする)  コレクションの再実装(戻り値を ref 化したい)  Span<T>と ref 変数を使いこなす  参照(ref/in)はC++の参照と同じなので、C#7.xの拡張は 「C++で速いシチュエーション」をC#でも同じように動作 させるための拡張としてイメージすると良い
  32. 32. リンク  このスライドのベンチマークソースコード https://github.com/nenaaki/ReadonlyStructBenchmark  AsRef()の参考コード(++C++; // 未確認飛行 C) https://ufcpp.net/study/csharp/sp_ref.html  Span<T>のベンチマーク(滅入るんるん) https://blog.meilcli.net/2018/06/cstackallocstruct.html
  33. 33. ご清聴ありがとうございました

×