Span을 사용하여 C 코드의 성능 향상

29993 단어 csharp
내 경험에 따르면 응용 프로그램의 성능을 향상시키는 주요 방법은 IO 호출의 수량과 지속 시간을 줄이는 것이다.그러나 이 옵션을 사용하면 개발자가 선택한 또 다른 경로는 창고에 있는 메모리를 사용하는 것이다.창고는 작은 부분만 분배해야 하지만, 창고의 크기가 매우 작기 때문에 매우 빠른 분배와 분배를 취소할 수 있습니다.이 밖에 굴뚝을 사용하면 GC의 압력을 낮출 수 있다.스택에 메모리를 분배하기 위해 value types 또는 stackalloc 연산자를 사용하고 비위탁 관리 메모리를 결합하여 사용할 수 있다.
비관리형 메모리에서 액세스하는 API가 매우 지루하기 때문에 개발자는 두 번째 옵션을 거의 사용하지 않습니다.Span<T>는 C#7.2의 일련의 값 유형으로 서로 다른 소스 메모리의 무분배 표시입니다.Span<T> 개발자가 더욱 편리한 방식으로 연속 메모리 영역을 처리하여 메모리와 유형의 안전을 확보할 수 있다.

Span 구현


Ref 반환


C 언어의 갱신에 세심한 관심을 기울이지 않는 사람들에게 Span<T> 실현을 둘러싼 첫 번째 단계는 C 7.0에 도입된 ref returns 학습이다.
대부분의 독자들은 인용을 통해 전달하는 방법의 매개 변수에 익숙하지만, 현재 C#는 값 자체가 아니라 값에 대한 인용을 되돌려줍니다.
어떻게 작동하는지 봅시다.우리는 일련의 걸출한 음악가들을 둘러싸고 간단한 포장기를 만들 것이다. 이 포장기는 전통적인 행위와 새로운ref반환 기능을 동시에 보여준다.
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
주의, 비록 첫 번째와 두 번째 예시에서 원시 소장품은 마지막 예시에서 수정되지 않았지만, 우리는 이미 이 소장품의 두 번째 예술가를 변경할 방법을 강구했다.본문 뒤에서 보듯이 이 유용한 기능은 인용과 유사한 방식으로 창고에 있는 그룹을 조작하는 데 도움을 줄 것입니다.

참조 구조


우리가 알고 있는 바와 같이 창고에 값 유형을 분배할 수 있습니다.또한 값이 사용되는 컨텍스트에 따라 결정되는 것은 아닙니다.값이 항상 스택에 할당되는지 확인하기 위해 C#7.0에 ref struct 개념을 도입했습니다.Span<T>는 하나ref struct이기 때문에 우리는 그것이 항상 창고에 분배된다고 확신한다.

Span 구현

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#in에서 선언할 수 없습니다.NET Core는 ByReference<T>를 통해 구현됩니다.
보시다시피 인덱스는 ref return 를 통해 이루어집니다. 인덱스는 창고 구조의 유사한 인용 형식만 허용합니다.

경계 제한

ref struct 가 항상 스택에서 사용되는지 확인하기 위해 많은 제한이 있습니다. 예를 들어 포장할 수 없고, object, dynamic 유형의 변수나 인터페이스 유형에 분배할 수 없으며, 인용 유형의 필드가 될 수 없고, awaityield 경계를 뛰어넘어 사용할 수 없습니다.그 밖에 두 가지 방법EqualsGetHashCode을 호출하여 하나NotSupportedException를 던진다.Span<T>ref struct.

문자열 대신 간격 사용하기


기존 코드 라이브러리 재작업


Linux permissions를 8진법으로 바꾸는 코드를 연구해 봅시다.액세스 가능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 의 필드 SymbolicPermissionReadOnlySpan<char> 가 아니라 string 로 표시하는 것이다.이 점을 실현하기 위해서 우리는 SymbolicPermissionref struct 로 성명해야 한다. 왜냐하면 필드나 속성은 Span<T> 유형이 될 수 없기 때문이다.
internal ref struct SymbolicPermission
{
    ...
    private ReadOnlySpan<char> _value;
}
이제 우리는 할 수 있는 모든 것ref structstring로 바꾸기만 하면 된다.유일하게 재미있는 것은 ReadOnlySpan<char> 방법이다. 왜냐하면 여기서 우리는 GetBlock 로 바꾸기 때문이다.
private ReadOnlySpan<char> GetBlock(int blockNumber)
{
    return _value.Slice(blockNumber * BlockLength, BlockLength);
}

평가


저희가 결과를 재보도록 하겠습니다.

우리는 속도가 50나노초 증가했다는 것을 알아차렸다. 이것은 대략 성능 향상의 10퍼센트 수준이다.50나초가 많지 않다고 말할 수도 있지만, 우리는 그것을 실현하는 데 거의 어떤 비용도 필요하지 않다.
현재, 우리는 권한의 이 개선을 평가할 것이다. 권한마다 18개의 블록이 있고, 블록마다 12개의 문자가 포함되어 있으며, 우리가 현저한 개선을 얻을 수 있는지를 볼 것이다.

보시다시피, 우리는 0.5마이크로초 또는 5%의 성능 개선을 얻으려고 노력했습니다.마찬가지로 이것은 적당한 성과로 보일 수도 있다.하지만 이것은 정말 걸을 수 없는 열매라는 것을 명심하세요.

그룹 대신 Span 사용하기


다른 종류의 그룹을 확장합시다.고려ASP.NET Channels pipeline의 예입니다.다음 코드의 배후 원인은 데이터가 통상적으로 네트워크에서 블록 형식으로 도착하기 때문이다. 이것은 데이터 블록이 여러 개의 버퍼에 동시에 주재할 수 있음을 의미한다.이 예에서 이러한 데이터는 Substring 로 해석됩니다.
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;
}
우리 이곳에서 발생한 일을 분해합시다.우리의 목표는 바이트 서열Sliceint로 해석하는 것이다.
if (!Utf8Parser.TryParse(textSpan, out value)) {
    throw new InvalidOperationException();
}
return value;
이제 입력 매개 변수를 textSpan 에 채우는 방법을 봅시다.입력 매개 변수는 연속 바이트 시퀀스를 읽을 수 있는 버퍼 실례입니다.uinttextSpan에서 계승되었는데 이것은 기본적으로 여러 개의 메모리 세그먼트로 구성되어 있음을 의미한다.
버퍼가 단일 세그먼트로 구성되어 있다면, 우리는 첫 번째 세그먼트의 밑바닥 ReadableBuffer 만 사용합니다.
if (buffer.IsSingleSpan) {
    textSpan = buffer.First.Span;
}
그렇지 않으면, 우리는 창고에 데이터를 분배하고 이를 바탕으로 ISequence<ReadOnlyMemory<byte>> 를 만든다.
var data = stackalloc byte[128];
var destination = new Span<byte>(data, 128);
그리고 우리는 방법Span을 사용합니다. 이 방법은 버퍼의 모든 메모리 세그먼트를 교체하고 목표Span<byte>에 복사합니다.이후에 우리는 슬라이스 버퍼 길이의 abuffer.CopyTo(destination)만 필요로 한다.
textSpan = destination.Slice(0, buffer.Length);
이 예는 새로운 Span API를 보여 줍니다. 도착하기 전보다 더 편리한 방식으로 창고에 수동으로 분배된 메모리를 처리할 수 있습니다.

결론

Span은(는) 안전하고 사용하기 쉬운 대체품을 제공하여 성능 개선을 쉽게 할 수 있습니다.매번 그것을 사용할 때의 수익은 상대적으로 작지만, 일치 사용하면 이른바'천도참사'를 피할 수 있다.Span<T>는 전국적으로 광범위하게 사용된다.NET Core 3.0 Code Libraryperfomance improvement comparing to the previous version를 사용할 수 있습니다.
사용 여부Span<T>를 결정할 때 다음 사항을 고려할 수 있습니다.
  • 만약 당신의 방법이 데이터 그룹을 받아들이고 크기를 바꾸지 않는다면.입력을 수정하지 않으면 stackalloc 을 고려할 수 있습니다.
  • 통계 데이터를 계산하거나 문법 분석을 실행하기 위해 문자열을 받아들인다면 Span<T> 받아들여야 합니다.
  • 만약 방법이 짧은 데이터 그룹을 되돌려준다면, Span<T> 의 도움으로 ReadOnlySpan<T> 되돌려줄 수 있습니다.ReadOnlySpan<char>는 값 유형이어야 한다는 것을 기억하십시오.
  • 좋은 웹페이지 즐겨찾기