はじめに
今回はC#7.0
より導入されたValueTask<TResult>
について取り上げたいと思います。
learn.microsoft.com
まずValueTask<TResult>
の説明の前に、Task<TResult>
を利用した非同期処理には以下のような問題点が指摘されていました。
- 非同期メソッドでも同期処理になる場合が多い
Task
はclass
なのでnew
の度にManaged Heap
にAllocation
が発生する- 同期処理の時は
Task
を生成しないようにしたい(完了済みのTask
が生成されてしまう)
これらを解決するために登場したのがValueTask<TResult>
で、以下のような対応をしています。
ValueTask<TResult>
は構造体(値型)TResult
もしくはTask<TResult>
を格納する(Task<TResult>
がnull
ならTResult
を返す)
こうすることで同期処理の場合はTask<TResult>
を生成せず、Managed Heap
の無駄なAllocation
が発生しなくできたというわけですね。
使い方
基本Task<TResult>
と変わりません。(私が知らないだけの可能性大)
private static async Task<int> SampleTaskAsync(int value) { if (value == 1000) { await Task.Delay(1); return 0; } // 完了済みのTaskのインスタンスが作られる return 1; } private static async ValueTask<int> SampleValueTaskAsync(int value) { if (value == 1000) { await Task.Delay(1); return 0; } // Taskのインスタンスは生成されない return 1; }
ただし同期処理が全く行われない場合は、むしろValueTask<TResult>
を利用すると無駄なメモリが確保されてしまいます。注意してください。
実験
Unityを利用するとヒープアロケーションを可視化できる(Profiler
という機能でGC Allocatoin
が調べられる)ので、Unityを活用して実験してみます。
public class Test2 : MonoBehaviour { private void Update() { for (var i = 0; i < 1000; i++) { SampleTaskAsync(0).Wait(); } } private static async Task<int> SampleTaskAsync(int value) { if (value == 1000) { await Task.Delay(1); return 0; } // 完了済みのTaskのインスタンスが作られる return 1; } private static async ValueTask<int> SampleValueTaskAsync(int value) { if (value == 1000) { await Task.Delay(1); return 0; } // Taskのインスタンスは生成されない return 1; } }
UnityのProfiler
で調べたところ、まさかのどっちもGC Alloc
がゼロになっていました。
Profiler
の範囲対象外なのか、まさかインスタンスが使い回されるように最適化が入っているのか。
詳細は私にはわかっていません。
よければ有志の方コメント等にて教えてくださると嬉しいです。