Genius DM

C# > Span<T> 본문

.NET

C# > Span<T>

Damon Jung 2018. 7. 31. 19:55

Span<T>



Background

.NET 은 관리되는 플랫폼으로, 관리된다는 의미는 메모리 접근 및 관리가 안전하면서도 자동으로 관리된다는 의미이다. 모든 타입이 .NET 에 의해 관리되며 .NET 은 실행 스택이나 Managed Heap 상의 메모리를 할당해준다.

플랫폼간 이벤트나 아래 단의 개발을 할 때 네이티브 객체에 대한 접근을 원하거나 시스템 메모리 접근을 원할 수도 있는데, 바로 이것 때문에 interop 이 생겼다. .NET 에는 네이티브 세계로 마샬링될 수 있는 타입들이 존재하며 해당 타입들은 네이티브 API 를 호출하거나 관리되는/네이티브 타입으로 변환할 수 있고 관리되는 코드 상에서 네티이브 구조체를 정의할 수도 있다.



문제점 1 : 메모리 엑세스 패턴

.NET 세계에서 알아야 할 3가지 타입의 메모리가 있다.

    • Managed Heap 메모리, Array 를 예로들 수 있다.
    • Stack 메모리, stackalloc 에 의해 생성된 객체를 예로들 수 있다.
    • Native 메모리, 네티이브 포인터 참조를 예로들 수 있다.

각 메모리 타입은 해당 메모리를 고안한 특정 언어 기능을 사용할 수 있다.

    • Heap 메모리에 엑세스하기 위해서 fixed 포인터를 지원가능한 타입 ( string 같은 ) 에 사용하거나 해당 메모리에 접근 가능한 다른 .NET 타입, 예를 들면 Array 나 Buffer 같은 것을 사용하자. 
    • Stack 메모리에 엑세스하기 위해 stackalloc 을 사용한 포인터를 사용하자.
    • Unmanaged 시스템 메모리에 접근하려면 마샬 API 를 이용한 포인터를 사용한다.

메모리 엑세스 패턴마다 다른 코드 작성이 필요하다. 해당 메모리들에 대한 단일 내장 타입은 존재하지 않는다.


문제점 2 : 성능

대부분의 앱에서 CPU 를 차지하는 명령은 스트링과 연관되어 있다. 프로파일러 세션을 구동하면 CPU 사용 시간의 95% 가 string 을 호출하거나 관련된 함수를 호출하는데 사용되었다는 사실을 알게 될 것이다. Trim, IsNullOrWhiteSpace, SubString 등이 아마 string API 로 가장 많이 사용되는 것이 아닌가 생각한다. 그리고 이 작업은 매우 무겁다.

    • Trim() 이나 SubString() 은 항상 새로운 string 객체를 반환한다. 만약 부분적인 것만 잘라내거나 원본 string 의 일정 부분만 따로 저장하여 복사해둘 수 있다면, string 객체를 매번 반환할 필요가 전혀 없다.
    • IsNullOrWhiteSpace() 는 메모리 복사가 필요한 string 객체를 받는다. ( string 객체는 불변이기 때문이다 )
    • 특히 string 접합은 매우 비싼 비용이 드는 작업이다. N 개의 문자열 객체를 받아 N 개의 복사를 수행하고 N - 1 개의 임시 문자열 객체를 생성하여 최종 문자열 객체를 리턴하게 된다. N - 1 카피 또한 순차적 쓰기 및 반환된 string 메모리에 직접적으로 접근할 수 있다면, 전혀 필요 없는 요소이다.





Span<T>

System.Span<T> 는 스택에만 할당 가능한 타입이고 ( ref struct ) 모든 메모리 접근 패턴을 감싼 객체이다. Span<T> 를 더미 참조와 길이를 지닌, 모든 3가지 메모리 접근 타입을 허용하는 구현이라고 생각해도 무방하다.

Span<T> 를 생성자를 통해 만들 수 있으며 암시적 연산자를 통해 array, stackalloc 된 포인터, 그리고 관리되지 않는 포인터로부터 생성도 가능하다.

// Use implicit operator Span<char>(char[]).
Span<char> span1 = new char[] { 's', 'p', 'a', 'n' };

// Use stackalloc.
Span<byte> span2 = stackalloc byte[50];

// Use constructor.
IntPtr array = new IntPtr();

Span<int> span3 = new Span<int>(array.ToPointer(), 1);


Span<T> 객체를 보유하게 되면 지정한 인덱스로 값을 할당할 수 있고 해당 Span 의 일부를 리턴할 수도 있다.


// Create an instance.
Span<char> span = new char[] { 's', 'p', 'a', 'n' };

// Access the reference of the first element.
ref char first = ref span[0];

// Assign the reference with a new value.
first = 'S';

// You get "Span".
Console.WriteLine(span.ToArray());

// Return a new span with start index = 1 and end index = span.Length - 1.
// You get "pan".
Span<char> span2 = span.Slice(1);

Console.WriteLine(span2.ToArray());


이제 Slice() 를 사용해서 보다 빠르게 Trim() 행위를 수행할 수 있다.


private static void Main(string[] args)
{
    string test = "   Hello, World! ";
    Console.WriteLine(Trim(test.ToCharArray()).ToArray());
}

private static Span<char> Trim(Span<char> source)
{
    if (source.IsEmpty)
    {
        return source;
    }

    int start = 0, end = source.Length - 1;
    char startChar = source[start], endChar = source[end];

    while ((start < end) && (startChar == ' ' || endChar == ' '))
    {
        if (startChar == ' ')
        {
            start++;
        }

        if (endChar == ' ')
        {
            end—;
        }

        startChar = source[start];
        endChar = source[end];
    }

    return source.Slice(start, end - start + 1);
}


위 코드는 문자열 대상으로 카피를 하지 않고 새로운 문자열을 생성하지도 않는다. Slice() 를 통해 원본 문자열의 일부를 리턴할 뿐이다. Span<T> 는 참조 구조체이기 때문에 모든 참조 구조체에 적용이 가능하다. 따라서 Span<T> 를 필드, 프로퍼티, 반복문, async 함수에서 사용할 수는 없다.




Memory<T>

System.Memory<T> 는 System.Span<T> 를 감싼 객체이며, 반복문과 async 함수에서도 접근이 가능하게 해준다. Span 속성을 Memory<T> 상에서 사용하면 관련된 메모리에 접근할 수 있어서 파일 스트림이나 네트워크 통신같은 비동기 시나리오에 아주 유용하다.

아래 코드는 간단한 사용법 예시이다.

private static async Task Main(string[] args)
{
    Memory<byte> memory = new Memory<byte>(new byte[50]);
    int count = await ReadFromUrlAsync("https://www.microsoft.com", memory).ConfigureAwait(false);
    Console.WriteLine("Bytes written: {0}", count);
}

private static async ValueTask<int> ReadFromUrlAsync(string url, Memory<byte> memory)
{
    using (HttpClient client = new HttpClient())
    {
        Stream stream = await client.GetStreamAsync(new Uri(url)).ConfigureAwait(false);
        return await stream.ReadAsync(memory).ConfigureAwait(false);
    }
}


프레임워크 클래스 라이브러리와 코어 프레임워크 ( FCL/CoreFx) 가 Stream, 문자열, .NET Core 2.1 에서 사용 가능한 여러가지를 위한 Span 타입 기반의 API 를 추가 할 예정이다.




ReadOnlySpan<T> and ReadOnlyMemory<T>

System.ReadOnlySpan<T> 는 System.Span<T> 의 읽기 전용 버전이며 인덱서가 읽기 전용 참조 객체를 반환해준다. System.ReadOnlySpan<T> 읽기 전용 참조 구조체를 사용하면 읽기 전용 메모리에 접근하게 된다. 이는 문자열 타입에 유용한데, 문자열이 불변성을 지니고 있기 때문이다. 

위 코드를 Trim() 을 ReadOnlySpan<T> 를 이용하여 구현하여 아래처럼 다시 작성할 수 있다.

private static void Main(string[] args)
{
    // Implicit operator ReadOnlySpan(string).
    ReadOnlySpan<char> test = "   Hello, World! ";
    Console.WriteLine(Trim(test).ToArray());
}

private static ReadOnlySpan<char> Trim(ReadOnlySpan<char> source)
{
    if (source.IsEmpty)
    {
        return source;
    }

    int start = 0, end = source.Length - 1;
    char startChar = source[start], endChar = source[end];

    while ((start < end) && (startChar == ' ' || endChar == ' '))
    {
        if (startChar == ' ')
        {
            start++;
        }

        if (endChar == ' ')
        {
            end—;
        }

        startChar = source[start];
        endChar = source[end];
    }

    return source.Slice(start, end - start + 1);
}


함수 본문에서는 아무것도 변화된게 없다. 그저 Span<T> 를 ReadOnlySpan<T> 로 변경하고 암시적 연산자를 이용해 문자열을 ReadOnlySpan<char> 로 변경했을 뿐이다.


System.ReadOnlyMemory<T> 는 System.Memory<T> 의 읽기 전용 버전이다. 반복문과 async 함수에서 사용할 수 있으며 이 타입을 이용해 읽기 전용 메모리에 접근할 수 있다.



Memory Extensions

System.MemoryExtensions 객체는 Span 타입을 다루는 다른 타입에 대한 확장 메서드를 지니고 있다. 아래는 자주 사용되는 확장 메서드 리스트이다. 대부분 Span 타입을 사용하는 기존 API 와 동일하다.

    • AsSpan, AsMemory : 배열을 Span<T> 나 Memory<T> 또는 읽기 전용으로 변환한다.
    • BinarySearch, IndexOf, LastIndexOf : 요소 및 인덱스를 찾는다.
    • IsWhiteSpace, Trim, TrimStart, TrimEnd, ToUpper, ToUpperInvariant, ToLower, ToLowerInvariant : 문자열과 유사한 Span<char> 함수



Memory Marshal

간혹 메모리 타입이나 시스템 버퍼에 접근하기 위한 로우 레벨 엑세스가 필요할 때와 Span 을 읽기 전용 Span 으로 변환하고 싶을 때가 있다. System.Runtime.InteropServices.MemoryMarshal static 클래스는 이러한 기능을 제공하여 메모리 접근을 용이하게 해준다. 아래 코드는 Span 타입을 이용하여 제목을 대문자 처리하는 코드이다. 임시 문자열 메모리 할당이 없기 때문에 상당히 빠르다.

private static void Main(string[] args)
{
    string source = "span like types are awesome!";

    // source.ToMemory() converts source from string to ReadOnlyMemory<char>,
    // and MemoryMarshal.AsMemory converts ReadOnlyMemory<char> to Memory<char>
    // so you can modify the elements.
    TitleCase(MemoryMarshal.AsMemory(source.AsMemory()));

    // You get "Span like types are awesome!";
    Console.WriteLine(source);
}



private static void TitleCase(Memory<char> memory)
{
    if (memory.IsEmpty)
    {
        return;
    }

    ref char first = ref memory.Span[0];
    if (first >= 'a' && first <= 'z')
    {
        first = (char)(first - 32);
    }
}



Conclusion

Span<T> 와 메모리<T> 는 통일된 방식으로 Array 같은 연속 메모리에 접근할 수 있게 해주며 어떻게 메모리가 할당되었는지에 관계없이 처리해준다. 네이티브 개발 시나리오와 고성능 시나리오에서 모두 유용하다. 특히 문자열을 처리하는 곳에 Span 타입을 사용하면서 상당한 성능 향상을 볼 수 있을 것이다. C# 7.2 의 매우 훌륭한 기능이다.



위 글과 그림은 https://blogs.msdn.microsoft.com/mazhou/2018/03/25/c-7-series-part-10-spant-and-universal-memory-management/ 에서 발췌, 번역하였습니다.

















Span<T>



Background

In the event of interop or low-level development, you may want the access to the native objects and system memory, here is why the interop part comes, there are types that can marshal into the native world, invoke native APIs, convert managed/native types and define a native structure from the managed code.



Problem1 : Memory access pattern.

In .NET world, there are three types of memory you may be interested:

    • Managed heap memory, such as an array;
    • Stack memory, such as objects created by stackalloc;
    • Native memory, such as a native pointer reference.

Each type of memory access may need to use language features that are designed for it:

    • To access heap memory, use the fixed (pinned) pointer on supported types (like string), or use other appropriate .NET types that have access to it, such as an array or a buffer;
    • To access stack memory, use pointers with stackalloc;
    • To access unmanaged system memory, use pointers with Marshal APIs.

You see, different access pattern needs different code, no single built-in type for all contiguous memory access.


Problem2 : Performance

In many applications, the most CPU consuming operations are string operations. If you run a profiler session against your application, you may find the fact that 95% of the CPU time is used to call string and related functions. Trim, IsNullOrWhiteSpace, and SubString may be the most frequently used string APIs, and they are also very heavy:

    • Trim() or SubString() returns a new string object that is part of the original string, this is unnecessary if there is a way to slice and return a portion of the original string to save one copy.
    • IsNullOrWhiteSpace() takes a string object that needs a memory copy (because string is immutable.)
    • Specifically,  string concatenation is expensive, it takes n string objects, makes n copy, generate n - 1 temporary string objects, and return a final string object, the n – 1 copies can be eliminated if there is a way to get direct access to the return string memory and perform sequential writes.





Span<T>

System.Span<T> is a stack-only type (ref struct) that wraps all memory access patterns, it is the type for universal contiguous memory access. You can think the implementation of the Span<T> contains a dummy reference and a length, accepting all 3 memory access types.

You can create a Span<T> using its constructor overloads or implicit operators from array, stackalloc’d pointers and unmanaged pointers.

// Use implicit operator Span<char>(char[]).
Span<char> span1 = new char[] { 's', 'p', 'a', 'n' };

// Use stackalloc.
Span<byte> span2 = stackalloc byte[50];

// Use constructor.
IntPtr array = new IntPtr();

Span<int> span3 = new Span<int>(array.ToPointer(), 1);


Once you have a Span<T> object, you can set value with a specified index, or return a portion of the span:


// Create an instance.
Span<char> span = new char[] { 's', 'p', 'a', 'n' };

// Access the reference of the first element.
ref char first = ref span[0];

// Assign the reference with a new value.
first = 'S';

// You get "Span".
Console.WriteLine(span.ToArray());

// Return a new span with start index = 1 and end index = span.Length - 1.
// You get "pan".
Span<char> span2 = span.Slice(1);

Console.WriteLine(span2.ToArray());


You can then use the Slice() method to write a high performance Trim() method:


private static void Main(string[] args)
{
    string test = "   Hello, World! ";
    Console.WriteLine(Trim(test.ToCharArray()).ToArray());
}

private static Span<char> Trim(Span<char> source)
{
    if (source.IsEmpty)
    {
        return source;
    }

    int start = 0, end = source.Length - 1;
    char startChar = source[start], endChar = source[end];

    while ((start < end) && (startChar == ' ' || endChar == ' '))
    {
        if (startChar == ' ')
        {
            start++;
        }

        if (endChar == ' ')
        {
            end—;
        }

        startChar = source[start];
        endChar = source[end];
    }

    return source.Slice(start, end - start + 1);
}


The above code does not copy over strings, nor generate new strings, it returns a portion of the original string by calling the Slice(). Because Span<T> is a ref struct, all ref struct restrictions apply. i.e. you cannot use Span<T> in fields, properties, iterator and async methods.




Memory<T>

System.Memory<T> is a wrapper of System.Span<T>, make it accessible in iterator and async methods. Use the Span property on the Memory<T> to access the underlying memory, this is extremely helpful in the asynchronous scenarios like File Streams and network communications (HttpClient etc..)

The following code shows simple usage of this type.

private static async Task Main(string[] args)
{
    Memory<byte> memory = new Memory<byte>(new byte[50]);
    int count = await ReadFromUrlAsync("https://www.microsoft.com", memory).ConfigureAwait(false);
    Console.WriteLine("Bytes written: {0}", count);
}

private static async ValueTask<int> ReadFromUrlAsync(string url, Memory<byte> memory)
{
    using (HttpClient client = new HttpClient())
    {
        Stream stream = await client.GetStreamAsync(new Uri(url)).ConfigureAwait(false);
        return await stream.ReadAsync(memory).ConfigureAwait(false);
    }
}


The Framework Class Library/Core Framework (FCL/CoreFx) will add APIs based on the span-like types for Streams, strings and more in .NET Core 2.1.




ReadOnlySpan<T> and ReadOnlyMemory<T>

System.ReadOnlySpan<T> is the read-only version of the System.Span<T> struct where the indexer returns a readonly ref object instead of ref object. You get read-only memory access when using System.ReadOnlySpan<T> readonly ref struct.System.ReadOnlySpan<T>. This is useful for string type, because string is immutable, it is treated as read-only span. 

We can rewrite the above code to implement the Trim() method using ReadOnlySpan<T>:

private static void Main(string[] args)
{
    // Implicit operator ReadOnlySpan(string).
    ReadOnlySpan<char> test = "   Hello, World! ";
    Console.WriteLine(Trim(test).ToArray());
}

private static ReadOnlySpan<char> Trim(ReadOnlySpan<char> source)
{
    if (source.IsEmpty)
    {
        return source;
    }

    int start = 0, end = source.Length - 1;
    char startChar = source[start], endChar = source[end];

    while ((start < end) && (startChar == ' ' || endChar == ' '))
    {
        if (startChar == ' ')
        {
            start++;
        }

        if (endChar == ' ')
        {
            end—;
        }

        startChar = source[start];
        endChar = source[end];
    }

    return source.Slice(start, end - start + 1);
}


As you can see, Nothing is changed in the method body; I just changed the parameter type from Span<T> to ReadOnlySpan<T>, and used the implicit operator to convert a string literal to ReadOnlySpan<char>.


System.ReadOnlyMemory<T> is the read-only version of System.Memory<T> struct where the Span property is a ReadOnlySpan<T>. When using this type, you get read-only access to the memory and you can use it with an iterator method or async method.



Memory Extensions

The System.MemoryExtensions class contains extension methods for different types that manipulates with span types, here is a list of commonly used extension methods, many of them are the equivalent implementations for existing APIs using the span types.

    • AsSpan, AsMemory: Convert arrays into Span<T> or Memory<T> or their read-only counterparts.
    • BinarySearch, IndexOf, LastIndexOf: Search elements and indexes.
    • IsWhiteSpace, Trim, TrimStart, TrimEnd, ToUpper, ToUpperInvariant, ToLower, ToLowerInvariant: Span<char> operations similar to string.



Memory Marshal

In some case, you probably want to have lower level access to the memory types and system buffers, and convert between spans and read-only spans. The System.Runtime.InteropServices.MemoryMarshal static class provides such functionalities to allow you control these access scenarios. The following code shows to title case a string using the span types, this is high performant because there is no temporary string allocations.

private static void Main(string[] args)
{
    string source = "span like types are awesome!";

    // source.ToMemory() converts source from string to ReadOnlyMemory<char>,
    // and MemoryMarshal.AsMemory converts ReadOnlyMemory<char> to Memory<char>
    // so you can modify the elements.
    TitleCase(MemoryMarshal.AsMemory(source.AsMemory()));

    // You get "Span like types are awesome!";
    Console.WriteLine(source);
}



private static void TitleCase(Memory<char> memory)
{
    if (memory.IsEmpty)
    {
        return;
    }

    ref char first = ref memory.Span[0];
    if (first >= 'a' && first <= 'z')
    {
        first = (char)(first - 32);
    }
}



Conclusion

Span<T> and Memory<T> enables a uniform way to access contiguous memory, regardless how the memory is allocated. It is very helpful for native development scenarios, as well as high performance scenarios. Especially, you will gain significant performance improvements while using span types to work with strings. It is a very nice feature innovated in C# 7.2.


Articles and images are originally from https://blogs.msdn.microsoft.com/mazhou/2018/03/25/c-7-series-part-10-spant-and-universal-memory-management/


































Comments