見出し画像

C#実験メモ:Span<T>構造体を使ってみた

C# 7.2で導入されたSpan<T>構造体ですが、なかなか使う機会がなかったのでこの三連休で試しに使ってみました。

C# でコーディングするとき、どうしてもunsafeなコードでポインターを使わなければならない場面があります。例えばBitmapでピクセル単位の画像処理を行うときなどです。しかしunsafeということもあり、できるだけそのコードを排除できるほうがセキュリティ上も望ましいといえます。

C# 7.2では連続したメモリ領域を読み書きするためのSpan<T>構造体というものが使えるようになりました。System.Memoryパッケージを参照する必要があります。

筆者は正直まだ詳しくないので、Span<T>の詳しいことは、こちらのサイトをご覧いただくほうが良いでしょう。

使ってみた感想

1.BitmapDataを扱う場合、unsafeなコードは完全にはなくせない。
   でも、unsafeなコードは最小限で済む
2.実行速度はちょっと落ちるがコードの最適化でなんとかなりそう
3.Span<T>はref structなのでラムダ式の中などで使えない
 (Parallel.Forで使う時は一々中でnewしないといけない)

詳細

以前作成した線画抽出サンプルプログラム(LineExtractTest)を以下のように変更しました。

元々綺麗なコードではない、strideが負になることを考慮していない(=脆弱性がある)……など完成度低いコードですがご容赦を。


1.BitmapDataを扱う場合、unsafeなコードは完全にはなくせない。
  でも、unsafeなコードは最小限で済む

外側のunsafeはなくすことはできても、BitmapDataをSpan<T>で扱えるようにするためには、結局scan0.ToPointer()を呼び出す必要があり、その部分でunsafeが必要になるということでした。

Span<byte> input;
unsafe
{
    input = new Span<byte>(inputData.Scan0.ToPointer(), pixelCount * channelCount);
}

でも、unsafeなコードをscan0.ToPointer()を呼び出す数行だけに局所化することができ、全体的にはコードの安全性が高まったといえるでしょう。

2.実行速度はちょっと落ちるがコードの最適化でなんとかなりそう

厳密に計測したわけではありませんが、リリースビルドを用いて確認した所、ポインターを使用した旧コードでは280msだったものが、Span<T>を使用した新コードでは290ms程度になりました。

この程度なら誤差の範囲ですし、コードの最適化で十分取り返せる感じでしょう。もしかすると、.NET Coreじゃないと真価は発揮できないのかも? 

このあたりはちゃんと調べていないので真に受けないでください。

3.Span<T>はref structなのでラムダ式の中などで使えない
 (Parallel.Forで使う時は一々中でnewしないといけない)

感覚的にはループの外でSpan<T>をnewしておきたいところなのですが、Span<T>はスタックにしか確保できない制約があります。

そのため、Parallel.Forを用いるときにはラムダ式を用いますが、外で宣言されたSpan<T>型のローカル変数をラムダ式内で使用することはできません。したがって、このケースでは、ループ内でnew Span<T>する必要があるということです。

NGなコード例

Span<byte> input;
unsafe
{
    input = new Span<byte>(inputData.Scan0.ToPointer(), pixelCount * channelCount);
}

Parallel.For(0, pixelCount, i =>
{
   //ここでinputは参照できない    
    Span<byte> inputPixel = input.Slice(i * channelCount, channelCount);

OKなコード例

Parallel.For(0, pixelCount, i =>
{
    Span<byte> input;
    unsafe
    {
        input = new Span<byte>(inputData.Scan0.ToPointer(), pixelCount * channelCount);
    }
    Span<byte> inputPixel = input.Slice(i * channelCount, channelCount);

といっても、スタックに確保されるということはnewのオーバーヘッドはそれほど大きくないのだと思います。

まあ、ちょっと感覚的に気持ち悪いというだけですかね。

最後に

Span<T>は使ってみれば簡単な話で、今までなかったのが不思議なぐらいです。こういう機能が増えていくのはありがたいことです。

unsafeなコードを局所化できれば、コーディングミスによる不具合や脆弱性を低減できることでしょう。今後、画像処理などでunsafeなコードを組む時は、積極的に使っていきたいと思います。

願わくは実務でこういうスキルを活かしたいところですが……。