Microsoft MVP성태의 닷넷 이야기
.NET Framework: 1002. C# - ReadOnlySequence<T> 소개 [링크 복사], [링크+제목 복사],
조회: 11640
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일

C# - ReadOnlySequence<T> 소개

이번엔 다음의 글을 정리한 것에 불과합니다.

CREATING A READONLYSEQUENCE FROM ARRAY DATA
; https://www.stevejgordon.co.uk/creating-a-readonlysequence-from-array-data-in-dotnet

그러니까, 개별 버퍼를 연결해 한 개의 개체로 다룰 수 있는 표현 방법을 .NET BCL 자체에서 제공한 것이 바로 ReadOnlySequence<T>입니다.

ReadOnlySequence<T> Struct
; https://docs.microsoft.com/en-us/dotnet/api/system.buffers.readonlysequence-1

제가 "표현 방법"이라고 언급했는데, 왜냐하면 .NET BCL에서 이에 대한 모든 concrete 코드를 제공하지 않기 때문입니다. 대충 상황을 좀 보면, ReadOnlySequence<T>는 (기본 생성자 외에) 단일 버퍼를 받아들일 수 있는 3개의 생성자와,

public ReadOnlySequence(ReadOnlyMemory<T> memory)
public ReadOnlySequence(T[] array)
public ReadOnlySequence(T[] array, int start, int length)

다중 버퍼가 표현된 ReadOnlySequenceSegment<T>를 받아들일 수 있는 1개의 생성자를 제공합니다.

public ReadOnlySequence(ReadOnlySequenceSegment<T> startSegment, int startIndex, ReadOnlySequenceSegment<T> endSegment, int endIndex)

여기서 ReadOnlySequenceSegment<T> 타입은,

ReadOnlySequenceSegment<T> Class
; https://docs.microsoft.com/en-us/dotnet/api/system.buffers.readonlysequencesegment-1

추상 클래스여서 사용 전에 구현을 필요로 합니다.

namespace System.Buffers
{
    public abstract class ReadOnlySequenceSegment<T>
    {
        public ReadOnlyMemory<T> Memory { get; protected set; }
        public ReadOnlySequenceSegment<T>? Next { get; protected set; }
        public long RunningIndex { get; protected set; }
    }
}

따지고 보면 개념은 정말 간단합니다. 그냥 우리가 기존에 알고 있던 연결 리스트(Linked-List)와 Span의 구획 지정을 담아 명시적으로 ReadOnlySequenceSegment<T> 타입을 만든 것입니다.

자, 그럼 이걸로 별개의 배열을 연결하는 Segment 타입을 구현해 볼까요?

class ArraySegment<T> : ReadOnlySequenceSegment<T>
{
    public ArraySegment(T [] array)
    {
        this.Memory = array;
    }

    public ArraySegment<T> Add(T [] array)
    {
        var segment = new ArraySegment<T>(array);
        segment.RunningIndex = RunningIndex + Memory.Length;

        Next = segment;
        return segment;
    }
}

간단하죠. 다음은 이러한 Segment로 ReadOnlySequence를 활용할 차례입니다.

class Program
{
    static void Main(string[] args)
    {
        int[] array1 = { 1, 2, 3 };
        int[] array2 = { 6, 7, 8, 9, 10 };

        ArraySegment<int> segment1 = new ArraySegment<int>(array1);
        ArraySegment<int> segment2 = segment1.Add(array2);
        
        ReadOnlySequence<int> seq = new ReadOnlySequence<int>(segment1, 0, segment2, segment2.Memory.Length);

        foreach (var segment in seq)
        {
            foreach (int item in segment.Span)
            {
                Console.Write(item + ", ");
            }
        }

        Console.WriteLine();

    }
}

/* 출력 결과
1, 2, 3, 6, 7, 8, 9, 10
*/

ReadOnlySequence의 생성자에 segment 2개를 전달했는데, 이것은 연결 리스트의 첫 번째와 끝에 해당하는 segment입니다. 따라서, 가령 3개의 버퍼를 구성한다면 이렇게 처리할 수 있습니다.

{
    int[] array1 = { 1, 2, 3 };
    int[] array2 = { 4, 5, };
    int[] array3 = { 6, 7, 8, 9, 10 };

    ArraySegment<int> segment1 = new ArraySegment<int>(array1);
    ArraySegment<int> segment3 = segment1.Add(array2).Add(array3);

    ReadOnlySequence<int> seq = new ReadOnlySequence<int>(segment1, 0, segment3, segment3.Memory.Length);

    foreach (var segment in seq)
    {
        foreach (int item in segment.Span)
        {
            Console.Write(item + ", ");
        }
    }

    Console.WriteLine();
}

/* 출력 결과
1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
*/

코드를 작성해 봤으니, 이제 다음의 그림 한장으로 ReadOnlySequence<T>의 역할을 이해할 수 있을 것입니다.

[출처: https://www.codemag.com/article/1807051/Introducing-.NET-Core-2.1-Flagship-Types-Span-T-and-Memory-T]
seq_buffer_1.png




그런데, 사실 여러 개의 분리된 버퍼를 연결해 주는 것에 그치지 않고 열거 자체를 자연스럽게 해주면 더 좋았을 것입니다. 이런 용도의 확장 메서드를 만들어 구현할 수도 있지만,

public static class Extension
{
    public static IEnumerable<T> AsEnumerable<T>(this ReadOnlySequence<T> seq)
    {
        foreach (var segment in seq)
        {
            int len = GetLength(segment); // https://www.sysnet.pe.kr/2/0/12481

            for (int i = 0;i < len; i ++)
            {
                yield return GetItem(segment, i);
            }
        }
    }

    private static T GetItem(ReadOnlyMemory<T> segment, int index)
    {
        return segment.Span[index]; // 성능 손실! https://www.sysnet.pe.kr/2/0/12475
    }

    private static int GetLength<T>(ReadOnlyMemory<T> segment)
    {
        return segment.Length;
    }
}

마이크로소프트 측은 대신 별도의 reader를 정의하는 식으로 해결을 했습니다.

SequenceReader<T> Struct
; https://docs.microsoft.com/en-us/dotnet/api/system.buffers.sequencereader-1

AN INTRODUCTION TO SEQUENCEREADER
; https://www.stevejgordon.co.uk/an-introduction-to-sequencereader

그래서 이렇게 열거할 수 있습니다.

SequenceReader<int> reader = new SequenceReader<int>(seq);

while (reader.End is false)
{
    if (reader.TryRead(out int item) == true)
    {
        Console.Write($"{item}, ");
    }
}

참고로, SequenceReader 역시 ref struct이기 때문에 enumerable 메서드는 작성할 수 없습니다. (게다가 이번엔 우회 방법도 없습니다.)

public static IEnumerable<T> AsEnumerable<T>(this ReadOnlySequence<T> seq) where T : unmanaged, IEquatable<T>
{
    SequenceReader<T> reader = new SequenceReader<T>(seq);

    // Error CS4013 Instance of type 'SequenceReader<T>' cannot be used inside a nested function, query expression, iterator block or async method
    while (reader.End is false)
    {
        if (reader.TryRead(out T item) == true)
        {
            yield return item;
        }
    }
}




정리해 보면!

예전에는, 간혹 버퍼 할당을 불연속적으로 하는 경우 나중에 결국 이를 하나로 합쳐서 반환/전달해야 하므로 GC Heap에 2중으로 부담을 갖게 됩니다. 바로 그런 경우에 그냥 그것을 하나로 표현할 수 있는 공통 방법으로 다룰 수 있도록 하는 것이 ReadOnlySequence<T>의 용도입니다. 그리고 사실 이건 기존에도 필요에 따라 얼마든지 구현해봤을 기능이고, 다시 말해 처음 나와서 낯설은 것일뿐 어찌 보면 별 거 아닙니다.

현재 BCL 내에서 이를 활용한 사례는 System.IO.Pipelines이고, 그것 내부에서는 ReadOnlySequenceSegment<T> 타입을 구현한 (internal로 정의된) BufferSegment가 있습니다.

마지막으로, .NET Framework의 경우 관련 타입을 System.Memory에 구현하고 있기 때문에 .NET 4.5부터 Nuget 참조를 통해 사용할 수 있습니다.

(첨부 파일은 이 글의 예제 코드를 포함합니다.)




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]







[최초 등록일: ]
[최종 수정일: 1/9/2021]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




... 46  47  48  49  50  51  52  53  54  55  56  57  58  59  [60]  ...
NoWriterDateCnt.TitleFile(s)
12142정성태2/12/202010714.NET Framework: 889. C# 코드로 접근하는 MethodDesc, MethodTable파일 다운로드1
12141정성태2/10/202010268.NET Framework: 888. C# - ASP.NET Core 웹 응용 프로그램의 출력 가로채기 [2]파일 다운로드1
12140정성태2/10/202010146.NET Framework: 887. C# - ASP.NET 웹 응용 프로그램의 출력 가로채기파일 다운로드1
12139정성태2/9/202011503.NET Framework: 886. C# - Console 응용 프로그램에서 UI 스레드 구현 방법
12138정성태2/9/202014275.NET Framework: 885. C# - 닷넷 응용 프로그램에서 SQLite 사용 [6]파일 다운로드1
12137정성태2/9/20209401오류 유형: 592. [AhnLab] 경고 - 디버거 실행을 탐지했습니다.
12136정성태2/6/20209839Windows: 168. Windows + S(또는 Q)로 뜨는 작업 표시줄의 검색 바가 동작하지 않는 경우
12135정성태2/6/202013507개발 환경 구성: 468. Nuget 패키지의 로컬 보관 폴더를 옮기는 방법 [2]
12134정성태2/5/202013617.NET Framework: 884. eBEST XingAPI의 C# 래퍼 버전 - XingAPINet Nuget 패키지 [5]파일 다운로드1
12133정성태2/5/202010842디버깅 기술: 161. Windbg 환경에서 확인해 본 .NET 메서드 JIT 컴파일 전과 후 - 두 번째 이야기
12132정성태1/28/202012426.NET Framework: 883. C#으로 구현하는 Win32 API 후킹(예: Sleep 호출 가로채기)파일 다운로드1
12131정성태1/27/202012490개발 환경 구성: 467. LocaleEmulator를 이용해 유니코드를 지원하지 않는(한글이 깨지는) 프로그램을 실행하는 방법 [1]
12130정성태1/26/20209986VS.NET IDE: 142. Visual Studio에서 windbg의 "Open Executable..."처럼 EXE를 직접 열어 디버깅을 시작하는 방법
12129정성태1/26/202015507.NET Framework: 882. C# - 키움 Open API+ 사용 시 Registry 등록 없이 KHOpenAPI.ocx 사용하는 방법 [3]
12128정성태1/26/202010333오류 유형: 591. The code execution cannot proceed because mfc100.dll was not found. Reinstalling the program may fix this problem.
12127정성태1/25/202010183.NET Framework: 881. C# DLL에서 제공하는 Win32 export 함수의 내부 동작 방식(VT Fix up Table)파일 다운로드1
12126정성태1/25/202011004.NET Framework: 880. C# - PE 파일로부터 IMAGE_COR20_HEADER 및 VTableFixups 테이블 분석파일 다운로드1
12125정성태1/24/20208845VS.NET IDE: 141. IDE0019 - Use pattern matching
12124정성태1/23/202010664VS.NET IDE: 140. IDE1006 - Naming rule violation: These words must begin with upper case characters: ...
12123정성태1/23/202012168웹: 39. Google Analytics - gtag 함수를 이용해 페이지 URL 수정 및 별도의 이벤트 생성 방법 [2]
12122정성태1/20/20209120.NET Framework: 879. C/C++의 UNREFERENCED_PARAMETER 매크로를 C#에서 우회하는 방법(IDE0060 - Remove unused parameter '...')파일 다운로드1
12121정성태1/20/20209707VS.NET IDE: 139. Visual Studio - Error List: "Could not find schema information for the ..."파일 다운로드1
12120정성태1/19/202011147.NET Framework: 878. C# DLL에서 Win32 C/C++처럼 dllexport 함수를 제공하는 방법 - 네 번째 이야기(IL 코드로 직접 구현)파일 다운로드1
12119정성태1/17/202011179디버깅 기술: 160. Windbg 확장 DLL 만들기 (3) - C#으로 만드는 방법
12118정성태1/17/202011854개발 환경 구성: 466. C# DLL에서 Win32 C/C++처럼 dllexport 함수를 제공하는 방법 - 세 번째 이야기 [1]
12117정성태1/15/202010848디버깅 기술: 159. C# - 디버깅 중인 프로세스를 강제로 다른 디버거에서 연결하는 방법파일 다운로드1
... 46  47  48  49  50  51  52  53  54  55  56  57  58  59  [60]  ...