C符号の性能向上のためのスパンの利用


私の経験では、アプリケーションのパフォーマンスを向上させるために主なことは、IOの呼び出しの数と期間を減らすことです.しかし、このオプションが実行されると、開発者が取る別のパスがスタック上のメモリを使用しています.スタックサイズが非常に小さいので、スタックは非常に高速な割り当てと解放を可能にします.また、スタックを使用すると、GC上の圧力を減らすことができます.スタックでメモリを割り当てるためにvalue types or stackalloc オペレータはアンマネージメモリの使用と結合した.
第2のオプションは、アンマネージメモリアクセスのAPIが全く冗長であるので、開発者によってめったに使用されません.Span<T> 異なるソースからのメモリの割り当てのない表現であるC .Span<T> メモリとタイプの安全性を確保するより便利なファッションで連続的なメモリの領域と開発者が動作することができます.

スパン実施


リターン


ラッピングヘッドの第一歩Span<T> C言語の更新プログラムに密接に従っていない人のための実装ref returns c≧7.0で紹介した.
読者のほとんどは参照によってメソッド引数を渡すのに慣れ親しんでいますが、現在C CHERHUNEは値自体の代わりに値への参照を返すことができます.
どうやって動くか調べましょう.我々は、伝統的な振舞いと新しいRef Return機能を示す著名な音楽家の配列のまわりで単純なラッパーを作成します.
public class ArtistsStore
{
    private readonly string[] _artists = new[] { "Amenra", "The Shadow Ring", "Hiroshi Yoshimura" };

    public string ReturnSingleArtist()
    {
        return _artists[1];
    }

    public ref string ReturnSingleArtistByRef()
    {
        return ref _artists[1];
    }

    public string AllAritsts => string.Join(", ", _artists);
}
それでは、それらのメソッドを呼びましょう
var store = new ArtistsStore();
var artist = store.ReturnSingleArtist();
artist = "Henry Cow";
var allArtists = store.AllAritsts; //Amenra, The Shadow Ring, Hiroshi Yoshimura

artist = store.ReturnSingleArtistByRef();
artist = "Frank Zappa";
allArtists = store.AllAritsts; //Amenra, The Shadow Ring, Hiroshi Yoshimura

ref var artistReference = ref store.ReturnSingleArtistByRef();
artistReference = "Valentyn Sylvestrov";
allArtists = store.AllAritsts; //Amenra, Valentyn Sylvestrov, Hiroshi Yoshimura
最初の例と2番目の例のコレクションは、最終的な例では変更されませんが、コレクションの2番目のアーティストを変更することができました.あなたが記事のコースの間に後でこのような便利な機能を参照してくださいとして、我々は、ファッションのようなリファレンスでスタックに配置された配列を操作するのに役立ちます.

ref構造体


値型がスタックに割り当てられるかもしれないということを知っているように.また、必ずしも値が使用されるコンテキストによっては必ずしも行われません.値が常にスタックに割り当てられるようにするためにref struct c≧7.0で導入された.Span<T>ref struct したがって、常にスタックに割り当てられていることを確認します.

スパン実施

Span<T>ref struct メモリへのポインタと、以下のようなスパンの長さが含まれます.
public readonly ref struct Span<T>
{
  private readonly ref T _pointer;
  private readonly int _length;
  public ref T this[int index] => ref _pointer + index;
  ...
}
ノートref 修飾子ポインタフィールドの近くに.このようなコンストラクトはプレーンCでは宣言できません.NETコアを介して実装されてByReference<T> .
インデックスを見ることができるようにref return スタック型の構造体に対して参照型の振る舞いを許す.

スパン制限


それを保証するref struct 常にスタックに使用されます.つまり、それらがボックス化されることができないことを含む多くの制限を持っています.object , dynamic または任意のインターフェイスの種類には、参照型のフィールドをすることはできませんし、全体で使用することはできませんawait and yield 境界.さらに、2つのメソッドを呼び出します.Equals and GetHashCode , スローするNotSupportedException . Span<T>ref struct .

文字列の代わりにスパンを使う


既存のコードベースの再処理


変換するコードを調べましょうLinux permissions を返します.アクセスできますhere
オリジナルコードはこちら
internal class SymbolicPermission
{
    private struct PermissionInfo
    {
        public int Value { get; set; }
        public char Symbol { get; set; }
    }

    private const int BlockCount = 3;
    private const int BlockLength = 3;
    private const int MissingPermissionSymbol = '-';

    private readonly static Dictionary<int, PermissionInfo> Permissions = new Dictionary<int, PermissionInfo>() {
            {0, new PermissionInfo {
                Symbol = 'r',
                Value = 4
            } },
            {1, new PermissionInfo {
                Symbol = 'w',
                Value = 2
            }},
            {2, new PermissionInfo {
                Symbol = 'x',
                Value = 1
            }} };

    private string _value;

    private SymbolicPermission(string value)
    {
        _value = value;
    }

    public static SymbolicPermission Parse(string input)
    {
        if (input.Length != BlockCount * BlockLength)
        {
            throw new ArgumentException("input should be a string 3 blocks of 3 characters each");
        }
        for (var i = 0; i < input.Length; i++)
        {
            TestCharForValidity(input, i);
        }

        return new SymbolicPermission(input);
    }

    public int GetOctalRepresentation()
    {
        var res = 0;
        for (var i = 0; i < BlockCount; i++)
        {
            var block = GetBlock(i);
            res += ConvertBlockToOctal(block) * (int)Math.Pow(10, BlockCount - i - 1);
        }
        return res;
    }

    private static void TestCharForValidity(string input, int position)
    {
        var index = position % BlockLength;
        var expectedPermission = Permissions[index];
        var symbolToTest = input[position];
        if (symbolToTest != expectedPermission.Symbol && symbolToTest != MissingPermissionSymbol)
        {
            throw new ArgumentException($"invalid input in position {position}");
        }
    }

    private string GetBlock(int blockNumber)
    {
        return _value.Substring(blockNumber * BlockLength, BlockLength);
    }

    private int ConvertBlockToOctal(string block)
    {
        var res = 0;
        foreach (var (index, permission) in Permissions)
        {
            var actualValue = block[index];
            if (actualValue == permission.Symbol)
            {
                res += permission.Value;
            }
        }
        return res;
    }
}

public static class SymbolicUtils
{
    public static int SymbolicToOctal(string input)
    {
        var permission = SymbolicPermission.Parse(input);
        return permission.GetOctalRepresentation();
    }
}
推論はとても簡単です.stringchar , では、なぜヒープの代わりにスタックに割り当てられないのか.
だから我々の最初の目標は、フィールドをマークすることです_value of SymbolicPermission ASReadOnlySpan<char> の代わりにstring . これを達成するために、我々は宣言しなければならないSymbolicPermission ASref struct フィールドまたはプロパティがタイプできないのでSpan<T> Aのインスタンスでなければref struct .
internal ref struct SymbolicPermission
{
    ...
    private ReadOnlySpan<char> _value;
}
今、我々はすべてを変更するstring 我々の範囲内でReadOnlySpan<char> . 唯一の関心点はGetBlock ここから私たちはSubstring with Slice .
private ReadOnlySpan<char> GetBlock(int blockNumber)
{
    return _value.Slice(blockNumber * BlockLength, BlockLength);
}

評価


結果を計りましょう

我々は、パフォーマンス向上の約10 %である50ナノ秒のアカウントをスピードアップに気付きます.つは、50ナノ秒はそれほどではないと主張することができますが、それはほとんど何も我々を達成するために原価計算!
今、私たちは、この改善を評価するために、それぞれ12文字の18ブロックを持っています.

あなたが見ることができるように、我々は0.5マイクロ秒または5 %のパフォーマンス改善を得ることができました.再び、それはささやかな業績のように見えるかもしれません.しかし、これが本当に絞首刑になる果物であったのを思い出してください.

配列の代わりにスパンを使う


他のタイプの配列を展開しましょう.例を考えるASP.NET Channels pipeline . 以下のコードの後ろの推論はデータがしばしば複数のバッファに同時に存在するかもしれないことを意味するネットワークの上のチャンクに到着するということです.この例ではint .
public unsafe static uint GetUInt32(this ReadableBuffer buffer) {
    ReadOnlySpan<byte> textSpan;

    if (buffer.IsSingleSpan) { // if data in single buffer, it’s easy
        textSpan = buffer.First.Span;
    }
    else if (buffer.Length < 128) { // else, consider temp buffer on stack
        var data = stackalloc byte[128];
        var destination = new Span<byte>(data, 128);
        buffer.CopyTo(destination);
        textSpan = destination.Slice(0, buffer.Length);
    }
    else {
        // else pay the cost of allocating an array
        textSpan = new ReadOnlySpan<byte>(buffer.ToArray());
    }

    uint value;
    // yet the actual parsing routine is always the same and simple
    if (!Utf8Parser.TryParse(textSpan, out value)) {
        throw new InvalidOperationException();
    }
    return value;
}
ここで起こることについて少しそれを壊しましょう.私たちの目標は、バイト数列を解析することですtextSpan into uint .
if (!Utf8Parser.TryParse(textSpan, out value)) {
    throw new InvalidOperationException();
}
return value;
では、入力パラメータをどのように設定するかを見てみましょうtextSpan . 入力パラメーターは、一連のバイトを読み取ることができるバッファのインスタンスです.ReadableBufferISequence<ReadOnlyMemory<byte>> これは基本的に複数のメモリセグメントからなることを意味します.
バッファが一つのセグメントからなる場合には、Span 最初のセグメントから.
if (buffer.IsSingleSpan) {
    textSpan = buffer.First.Span;
}
それ以外の場合は、スタックにデータを割り当て、Span<byte> それに基づきます.
var data = stackalloc byte[128];
var destination = new Span<byte>(data, 128);
次に、メソッドを使用しますbuffer.CopyTo(destination) バッファの各メモリセグメントを反復してコピー先をコピーするSpan . その後、我々はスライスSpan バッファの長さ.
textSpan = destination.Slice(0, buffer.Length);
この例では、新しいSpan<T> APIは、私たちが手動で、その到着前にはるかに便利なファッションでスタックに割り当てられたメモリで動作することができます.

結論

Span<T> 提供する安全で使いやすい代替stackalloc パフォーマンス向上を容易にすることができます.それの各々の使用からの利益が比較的小さい間、それの一貫した使用は千のカットによる死として知られていることを避けることができます.Span<T> 広く使われています.NETコア3.0のコードベースで、perfomance improvement comparing to the previous version .
あなたが使うべきかどうか決めるとき、あなたが考慮するかもしれない若干のものは、ここにありますSpan<T> :
  • あなたのメソッドがデータの配列を受け入れて、そのサイズを変えないならば.入力を変更しない場合は、ReadOnlySpan<T> .
  • あなたのメソッドが若干の統計を数えるためにストリングを受け入れるならば、あなたが受け入れるべき構文解析を実行するためにReadOnlySpan<char> .
  • データが短い配列を返した場合、Span<T> …の助けを得てSpan<T> buf = stackalloc T[size] . 覚えてT 値型である必要があります.