Microsoft MVP성태의 닷넷 이야기
.NET Framework: 555. List<T>의 Resize 메서드 구현 [링크 복사], [링크+제목 복사]
조회: 15078
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일

List<T>의 Resize 메서드 구현

사실, .NET Framework의 BCL을 만드는 실력이라면... 적어도 C/C++ 정도는 자유롭게 다루지 않을까... 하는 기대를 하는 것이 무리는 아닙니다. 그렇다면 C/C++의 vector<>가 제공하는 resize를 알 법도 하고... 그렇다면 당연히 List<>에 구현했을 법도 한데... 이상하게 이 메서드는 없습니다.

이 메서드가 은근히 필요할 때가 있는데요. 예를 들어, n 개의 요소를 동적으로 확보하고 임의의 위치를 액세스하고 싶은 경우가 있습니다.

List<int> list = new List<int>();
list[39] = 39; // System.ArgumentOutOfRangeException 예외 발생

C/C++의 vector는 이럴 때 resize(n); 메서드를 호출하고 조정된 크기 내의 인덱스(n - 1)를 임의로 접근하는 것이 가능합니다.

검색해 보면, 다음의 글이 나오는데요.

is there in C# a method for List<T> like resize in c++ for vector<T>
; http://stackoverflow.com/questions/12231569/is-there-in-c-sharp-a-method-for-listt-like-resize-in-c-for-vectort

그리고 그 해법으로 Jon Hanna에 의해 다음의 코드가 답변으로 제시됩니다.

public static class ListExtras
{
    public static void Resize<T>(this List<T> list, int size, T element = default(T))
    {
        int count = list.Count;

        if (size < count)
        {
            list.RemoveRange(size, count - size);
        }
        else if (size > count)
        {
            if (size > list.Capacity)   // Optimization
                list.Capacity = size;

            list.AddRange(Enumerable.Repeat(element, size - count));
        }
    }
}

근데... 이 코드가 참... 애매합니다. 더 할당해야 하는 경우 AddRange를 하고 있는데, 이것은 내부적으로 MoveNext와 List<>.Insert 메서드를 반복하는 동작으로 바뀝니다. 즉, 성능이 걱정된다는 것인데, 여기서 재미있는 점은, AddRange 메서드가 첫 번째 인자를 IEnumerable<T> 타입을 받긴 하지만, 내부적으로 ICollection<T>로 변환 여부를 체크하고 가능한 경우 Array.Copy(To) 메서드를 사용하는 배려가 되어 있다는 점입니다.

public void AddRange(IEnumerable<T> collection)
{
    this.InsertRange(this._size, collection);
}

public void InsertRange(int index, IEnumerable<T> collection)
{
    // ...[생략]...

    ICollection<T> is2 = collection as ICollection<T>;
    if (is2 != null)
    {  // ICollection<T>로 변환이 가능하면, Array.Copy(To)로 처리
        int count = is2.Count;
        if (count > 0)
        {
            this.EnsureCapacity(this._size + count);
    // ...[생략]...
            T[] array = new T[count];
            is2.CopyTo(array, 0);
            array.CopyTo(this._items, index);
    // ...[생략]...
            this._size += count;
        }
    }
    else
    {
        // ICollection<T>로 변환이 안되면, IEnumerable 고유의 복사 작업 진행
        using (IEnumerator<T> enumerator = collection.GetEnumerator())
        {
            while (enumerator.MoveNext())
            {
                this.Insert(index++, enumerator.Current);
            }
        }
    }
    this._version++;
}

따라서, Resize 메서드를 이런 식으로 구현하는 것도 생각해 볼 수 있습니다.

public static void Resize2<T>(this List<T> list, int size)
{
    int count = list.Count;

    if (size < count)
    {
        list.RemoveRange(size, count - size);
    }
    else if (size > count)
    {
        if (size > list.Capacity)
        {
            list.Capacity = size;
        }

        list.AddRange(new T[size - count]);
    }
}

성능 측정을 해보면,

private static void CalcTime1(int count)
{
    Stopwatch sw = new Stopwatch();

    sw.Start();

    for (int i = 0; i < count; i++)
    {
        List list = new List();
        list.Resize(50);
    }

    sw.Stop();
    Console.WriteLine("Enum-Resize: " + sw.ElapsedTicks);
}

private static void CalcTime2(int count)
{
    Stopwatch sw = new Stopwatch();

    sw.Start();

    for (int i = 0; i < count; i++)
    {
        List list = new List();
        list.Resize2(50);
    }

    sw.Stop();
    Console.WriteLine("New-Resize: " + sw.ElapsedTicks);
}

CalcTime1(1);
CalcTime2(1);

CalcTime1(10000);
CalcTime2(10000);

// 출력 결과
Enum-Resize: 8929
New-Resize: 2059

Enum-Resize: 22026
New-Resize: 5902

수치상으로는 4배 정도로 new T[]로 추가한 것이 더 빨랐습니다. 역시 일일이 MoveNext하며 추가하는 것보다 Array.Copy(To)로 메모리 복사를 한 것이 더 빠를 수밖에 없습니다.

단지, ElapsedTicks로 잰 것이고 10,000번이라는 연속 횟수를 감안했을 때 일반적인 거의 모든 프로그램에서는 new T[]로 구현했다고 해서 딱히 성능 향상을 기대하는 것은 무리일 듯 합니다.




어찌 보면 가장 좋은 구현은, 마이크로소프트에서 다음과 같은 resize 메서드를 제공해 주는 것입니다.

public static void Resize3<T>(this List<T> list, int size)
{
    int count = list.Count;

    if (size < count)
    {
        list.RemoveRange(size, count - size);
    }
    else if (size > count)
    {
        if (size > list.Capacity)
        {
            list.Capacity = size;
        }

        list._size = size; // private 필드인 _size의 값을 조정
    }
}

물론 위의 구현을 현재에도 .NET Reflection을 이용해 구현할 수는 있습니다.

Type type = typeof(List<int>);
FieldInfo fieldInfo = type.GetField("_size", BindingFlags.NonPublic | BindingFlags.Instance);

public static void Resize3<T>(this List<T> list, FieldInfo fieldInfo, int size)
{
    int count = list.Count;

    if (size < count)
    {
        list.RemoveRange(size, count - size);
    }
    else if (size > count)
    {
        if (size > list.Capacity)
        {
            list.Capacity = size;
        }

        fieldInfo.SetValue(list, size);
    }
}

하지만 Reflection이니만큼 성능이 new T[]로 했던 경우에 비해 조금 느립니다. 그 외에도, 위의 방법에는 치명적인 단점이 있습니다. 바로 List 타입이 제네릭이기 때문에 반드시 FieldInfo에 대한 정보를 구할 때 인스턴스 타입이 동일하게 지정된 제네릭 타입을 얻어와야 한다는 점입니다.

Type type = typeof(List<>); // 이렇게 얻으면 안됨!

Type type = typeof(List<int>); // List<int>인 경우에만 해당!

따라서, Resize 3번 유형은 마이크로소프트가 내부적으로 해줬을 때 가장 성능이 빠르고 현실적으로 사용할 수 있으므로 외부 개발자 입장에서는 고려하지 않는 것이 좋습니다.

(첨부 파일은 위의 테스트 코드를 포함합니다.)




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







[최초 등록일: ]
[최종 수정일: 6/27/2021]

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

비밀번호

댓글 작성자
 



2016-03-09 04시56분
[초록물꼬기] 와우.. T array 는 기본적으로 ICollection<T> 를 구현하는걸 이용하셨네요.

Resize 는 기본 element 를 지정할 수 있는데 Resize2 는 디폴트로만 초기화가 되는 점이 있긴 하지만..
본 목적이 c++ 의 resize 처럼 하는거라면 크게 문제가 되지 않네요.
굳이 초기화를 한다면 함수 안에서

T[] t = new T[count - size]
for(int i=0; i<count - size; i++)
    t[i] = element;

이런걸 해준다 해도 디폴트 초기화때보다 성능 하락은 1.6배정도밖에 안일어나고
여전히 Resize 보다는 3배정도 빠르네요.


마이크로 소프트에서 Resize3 과 같은 메소드를 제공한다면
private 인 _size 만 바꿔준다고 할 경우 처음 언급해주셨던 System.ArgumentOutOfRangeException 이 또 발생하지 않을까요?
_size 를 바꾸고나서 _items 를 처리 해야할 것 같은데..
결국 가장 좋은 구현은 정성태님께서 제시하신대로 ICollection 을 구현하는 최소단위의 자료구조를 만들어서 넣는 방법이 아닐지
조심스레 생각해봅니다..!

밤중에 좋은 지식 잘 얻어갑니다. ^^
[guest]
2016-03-10 12시02분
물론 Resize3의 경우 _size만 변경한다면 System.ArgumentOutOfRangeException 예외가 발생하겠지만, 그 전에 Capacity 속성에 새로운 크기를 넣어 공간 확보를 해두기 때문에 안전하게 구현됩니다.
정성태

... 46  47  48  49  50  51  52  53  54  55  56  57  58  [59]  60  ...
NoWriterDateCnt.TitleFile(s)
12147정성태2/19/202010937디버깅 기술: 162. x86/x64의 기계어 코드 최대 길이
12146정성태2/18/202011181.NET Framework: 893. eBEST C# XingAPI 래퍼 - 로그인 처리파일 다운로드1
12145정성태2/18/202010424.NET Framework: 892. eBEST C# XingAPI 래퍼 - Sqlite 지원 추가파일 다운로드1
12144정성태2/13/202010449.NET Framework: 891. 실행 시에 메서드 가로채기 - CLR Injection: Runtime Method Replacer 개선 - 두 번째 이야기파일 다운로드1
12143정성태2/13/20208565.NET Framework: 890. 상황별 GetFunctionPointer 반환값 정리 - x64파일 다운로드1
12142정성태2/12/202010247.NET Framework: 889. C# 코드로 접근하는 MethodDesc, MethodTable파일 다운로드1
12141정성태2/10/20209903.NET Framework: 888. C# - ASP.NET Core 웹 응용 프로그램의 출력 가로채기 [2]파일 다운로드1
12140정성태2/10/20209735.NET Framework: 887. C# - ASP.NET 웹 응용 프로그램의 출력 가로채기파일 다운로드1
12139정성태2/9/202011082.NET Framework: 886. C# - Console 응용 프로그램에서 UI 스레드 구현 방법
12138정성태2/9/202013842.NET Framework: 885. C# - 닷넷 응용 프로그램에서 SQLite 사용 [6]파일 다운로드1
12137정성태2/9/20209075오류 유형: 592. [AhnLab] 경고 - 디버거 실행을 탐지했습니다.
12136정성태2/6/20209429Windows: 168. Windows + S(또는 Q)로 뜨는 작업 표시줄의 검색 바가 동작하지 않는 경우
12135정성태2/6/202013070개발 환경 구성: 468. Nuget 패키지의 로컬 보관 폴더를 옮기는 방법 [2]
12134정성태2/5/202013261.NET Framework: 884. eBEST XingAPI의 C# 래퍼 버전 - XingAPINet Nuget 패키지 [5]파일 다운로드1
12133정성태2/5/202010485디버깅 기술: 161. Windbg 환경에서 확인해 본 .NET 메서드 JIT 컴파일 전과 후 - 두 번째 이야기
12132정성태1/28/202012050.NET Framework: 883. C#으로 구현하는 Win32 API 후킹(예: Sleep 호출 가로채기)파일 다운로드1
12131정성태1/27/202012137개발 환경 구성: 467. LocaleEmulator를 이용해 유니코드를 지원하지 않는(한글이 깨지는) 프로그램을 실행하는 방법 [1]
12130정성태1/26/20209598VS.NET IDE: 142. Visual Studio에서 windbg의 "Open Executable..."처럼 EXE를 직접 열어 디버깅을 시작하는 방법
12129정성태1/26/202015192.NET Framework: 882. C# - 키움 Open API+ 사용 시 Registry 등록 없이 KHOpenAPI.ocx 사용하는 방법 [3]
12128정성태1/26/20209984오류 유형: 591. The code execution cannot proceed because mfc100.dll was not found. Reinstalling the program may fix this problem.
12127정성태1/25/20209865.NET Framework: 881. C# DLL에서 제공하는 Win32 export 함수의 내부 동작 방식(VT Fix up Table)파일 다운로드1
12126정성태1/25/202010640.NET Framework: 880. C# - PE 파일로부터 IMAGE_COR20_HEADER 및 VTableFixups 테이블 분석파일 다운로드1
12125정성태1/24/20208519VS.NET IDE: 141. IDE0019 - Use pattern matching
12124정성태1/23/202010349VS.NET IDE: 140. IDE1006 - Naming rule violation: These words must begin with upper case characters: ...
12123정성태1/23/202011804웹: 39. Google Analytics - gtag 함수를 이용해 페이지 URL 수정 및 별도의 이벤트 생성 방법 [2]
12122정성태1/20/20208820.NET Framework: 879. C/C++의 UNREFERENCED_PARAMETER 매크로를 C#에서 우회하는 방법(IDE0060 - Remove unused parameter '...')파일 다운로드1
... 46  47  48  49  50  51  52  53  54  55  56  57  58  [59]  60  ...