Event : Visual Studio Users Community Japan #1
Date : 2019/09/14
ソフトウェア/サービス開発において最も後回しにされるものの代表が「パフォーマンスの向上」です。C#/.NET の最大の武器は開発生産性ですが、C# 7.0 以降はパフォーマンス向上のための機能追加が多数行われています。いくつかのポイントを押さえることで実装時からより高速なコードを書くことができるようになります。
このドキュメントでは、そんなポイントとなる箇所をふんだんにお届けします。
12. ASP.NET Core + IIS
In Process Hosting Model を利用すると現状最速の模様 (by しばやん雑記
ASP.NET Core 2.2 以降の場合、既定で有効
Linux (Docker) よりも 30% ほど速い
速度を取るか、ポータビリティを取るか
最速のホスティング環境
https://blog.shibayan.jp/entry/20181225/1545727432
19. 性能劣化に直結するコピーを抑止
構造体のサイズが大きいときに検討したい
参照渡し (ref / in / out)
常に
気を配る
static void Main()
{
var a = 1;
ref var d = ref PassThrough(ref a);
d = 2;
}
static ref int PassThrough(ref int b)
{
ref var c = ref b;
return ref c;
}
20. Defensive Copy の発生
Readonly 保証のためにコピーを作って関数 call をする場合がある
コピー回数削減どころか、逆にコピー回数が増えることも
in (ref readonly) 引数の難しさ
// Bar がフィールドを
// 書き換えていない保証がない
struct Foo
{
public readonly int X;
public void Bar(){}
}
// せっかく参照渡ししたけど…
void Call(in Foo x)
{
var a = x.X; // コピーなし
x.Bar(); // 防衛的コピー
x.Bar(); // 防衛的コピー
}
21. Defensive Copy の抑止
全フィールドが書き換えられないことを保証
readonly struct (C# 7.2)
可能な限り
つける
// 書き換えていない保証をする
readonly struct Foo
{
public readonly int X;
public void Bar(){}
}
// 読み取り専用の参照渡しが効果を発揮
void Call(in Foo x)
{
var a = x.X; // コピーなし
x.Bar(); // コピーなし
x.Bar(); // コピーなし
}
22. Defensive Copy の抑止
関数内でフィールドの書き換えがないことを保証
struct Foo
{
public int X;
public int Y;
public readonly int Add() => X + Y;
public int Sub() => X - Y;
}
readonly 関数メンバー (C# 8.0)
// 関数単位で挙動が決まる
void Call(in Foo x)
{
x.Add(); // コピーなし
x.Sub(); // 防衛的コピー
}
可能な限り
つける
struct でのみ
適用可能
23. コピー抑止
構造体の拡張メソッドを作るときには積極的に参照渡しにしたい
防衛的コピー周りの理由で Generics における in 引数にはできない
参照渡しの拡張メソッド
// OK
public static void Foo(ref this int value){}
public static void Foo2(in this int value){}
// struct 制約があれば ref 引数は OK
public static void Foo3<T>(ref this T value)
where T : struct
{}
24. 参照渡しの演算子 overload
コピー抑止
in 引数のみ認めらているので防衛的コピーに注意
readonly struct Complex
{
public double R { get; }
public double I { get; }
public Complex(double r, double i) => (this.R, this.I) = (r, i);
// in 引数が認められるようになった
public static Complex operator +(in Complex x, in Complex y)
=> new Complex(x.R + y.R, x.I + y.I);
}
25. Good-bye 匿名型 / Tuple
LINQ みたいな局所的な利用 / 多値戻り値には最適
言語機能にも統合されているので書き心地も最高
ValueTuple
// ValueTuple : ヒープ未使用
var q1 = collection.Select(x => (value: x, power: x * x));
// 匿名型 : ヒープ利用
var q2 = collection.Select(x => new { Value = x, Power = x * x });
// Tuple : ヒープ利用 (名前を付けられない)
var q3 = collection.Select(x => Tuple.Create(x, x * x));
34. I/O 待ち = CPU が暇してる
CPU は最も高価な計算リソース
余裕を持たせ過ぎるではなく十分に「使い切る」ことが重要
非同期 I/O を利用
// RSS を取得している間 CPU が暇
var url = "http://blog.xin9le.net/rss";
var node = XElement.Load(url);
// これなら通信待ちを別処理に有効活用できる
var url = "http://blog.xin9le.net/rss";
var client = new HttpClient();
var rss = await client.GetStringAsync(url);
var node = XElement.Parse(rss);
41. 拡張メソッドを自作すると便利
// ラムダ式の中で利用したい変数を state として別途渡すことでキャプチャを回避
public static T FirstOrDefault<T, TState>
(this IEnumerable<T> source, Func<T, TState, bool> predicate, TState state)
{
if (source == null) throw new ArgumentNullException(nameof(source));
if (predicate == null) throw new ArgumentNullException(nameof(predicate));
foreach (var x in source)
if (predicate(x, state))
return x;
return default;
}
// こんな感じで使う
.FirstOrDefault((x, state) => x.Id == state, id);
42. 意図しない変数キャプチャを防止
ローカル関数のキャプチャは高速だけど若干のペナルティはある
静的ローカル関数 (C# 8.0)
// これまでのローカル関数 (C# 7.0 以降)
static void Main()
{
var a = 3;
var b = 4;
var result = LocalFunction(a);
int LocalFunction(int x)
=> x + b; // b をキャプチャしてる
}
// 静的ローカル関数 (C# 8.0 以降)
static void Main()
{
var a = 3;
var b = 4;
var result = LocalFunction(a);
static int LocalFunction(int x)
=> x + b; // コンパイルエラー
}
43. 変数キャプチャの外出し
static void Main()
{
// 途中で return していても
// 関数の最初で隠しインスタンスが
// 生成されてしまう
var id = 0;
if (true)
return;
// このコードは通らないのに理不尽!
Foo(x => x == id);
}
static void Main()
{
// 隠しインスタンスの生成なし
var id = 0;
if (true)
return;
CallFoo(id);
}
// 変数キャプチャされるコードパスを
// 別関数にすることで効率化
static void CallFoo(int id)
=> Foo(x => x == id);