Microsoft MVP성태의 닷넷 이야기
제네릭과 배열 관련 기초 질문입니다. [링크 복사], [링크+제목 복사]
조회: 11440
글쓴 사람
이성환 (vactorman at naver.com)
홈페이지
첨부 파일
 

매번 기본적인 질문만 드려 죄송스럽게 생각하고 있습니다.
이번에도 도움을 얻고자 이렇게 또 질문을 올리게 되었습니다.

코드가 포함되어 있어 내용 약간 깁니다.

List<T>에서 타입인자를 값형식으로 사용했을 경우 Boxing / Unboxing 이 발생하지 않는 것으로 알고 있었습니다.
원리가 궁금해서 Decompiler를 통해 List<T>의 구조를 살펴봤는데
Add 되는 요소를 T[] _items 에 보관하는 방식을 사용하더군요.
Capacity 조절 방식도 Int.MaxValue 까지 2배수로 증가하면서 Array.Copy로 복사해서 증가시키는 방법으로 사용하던데요.

그래서 여기서부터 의문이 생겼습니다.

일단 아래 코드로 테스트를 해봤습니다.
(CLR via C#의 제네릭 챕터에 수록된 코드입니다.)

class Program
    {
        static void Main(string[] args)
        {
            ValueTypePerTest();
        }

        private static void ValueTypePerTest()
        {
            using (new OperationTimer("List<int>"))
            {
                var l = new List<int>(10000000);
                for (int i = 0; i < 10000000; i++)
                {
                    l.Add(i);
                    var x = l[i];
                }
               l = null;
            }

            using (new OperationTimer("Array"))
            {
                var a = new int[10000000];
                for (int i = 0; i < 10000000; i++)
                {
                    a[i] = i;
                    var x = a[i];
                }
                a = null;
            }
        }
    }

    public class OperationTimer : IDisposable
    {
        private long startTime;

        private string title;

        private int collectionCount;

        public OperationTimer(string title)
        {
            this.PrepareForOperation();
            this.title = title;
            this.collectionCount = GC.CollectionCount(0);
            this.startTime = Stopwatch.GetTimestamp();
        }

        private void PrepareForOperation()
        {
            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.Collect();
        }

        public void Dispose()
        {
            Console.WriteLine(
                "{0, 6:###.00} seconds (GCs = {1, 3}) {2}",
                (Stopwatch.GetTimestamp() - this.startTime) / (double)Stopwatch.Frequency,
                GC.CollectionCount(0) - this.collectionCount,
                this.title);
        }
    }

(원래 두 번째 테스트는 배열이 아니라 ArrayList로 되어 있던 걸
배열 자체의 동작을 알고 싶어 수정해봤습니다.)

결과는 둘 다 GC 수집이 0번으로 나타났습니다.

List<int>의 경우 Capacity를 지정해 주지 않으면 3회의 GC 동작이 있었고
Capacity를 지정해 주면 0회의 GC 동작이 있었습니다.

Capacity 설정 차이에 따른 GC 동작의 변화는 이해가 됩니다.
(Capacity 설정하지 않으면 이전 Capacity의 두 배수 배열을 생성하여 Copy 하기 때문에 이전 배열에 대한 참조는 GC 대상.)

근데 궁금한 점은 배열 사용 자체에서 생겼습니다.
    
    

닷넷에서는 배열 역시 System.Array 추상 클래스를 상속받아 구현된 참조형식으로 알고 있습니다.

결국 int[] 나 Stream[] 모두 생성한 변수 자체는 참조 형식이며 힙에 생성된다는 얘긴데

이것은 곧 해당 변수가 최종적으로는 GC의 수집대상이 되어야 한다는 얘기로 이해하고 있습니다.
(잘못 알고 있나 해서 찾아봤습니다. CLR via C# 458p 에 내용이 있긴하더군요.)

여기에 값형식 배열이라면 해당 변수에 추가되는 요소들이
실제 값형식 요소들이 박싱되어 힙에 저장된다는 얘긴지, 스택의 주소만 저장된다는 얘긴지는 모르겠습니다만
어쨌든 배열 변수를 생성했다면 그 변수는 힙에 생성되고 이는 GC 수집 대상이 되어야 한다는 것으로 이해했습니다.

그렇다면 위 코드의 실행 결과가 잘 이해가 되지 않습니다.

ArrayList 처럼 명시적으로 object 형식으로 각 요소를 언박싱하지는 않지만
어쨌든 배열 변수의 참조는 힙에 생성되므로 GC의 수집이 최소 1회는 있어야 한다고 생각했습니다.

그런데 0회 발생 했다는 얘기는 아예 수집 대상이 안 됐다는 것인데
GC의 수집 대상에서 제외되려면 해당 변수가 다른 곳에서 참조되거나
(이건 가능성 없어 보입니다.)

배열 자체가 스택에 위치한 변수어야만 가능한 것이 아닌가요?

값형식 배열로 선언하여 생성한 변수 자체는 참조 형식인데 GC의 대상이 되지 않는다는 얘기는
생성 위치가 힙이 아니고 스택이란 얘기인지,
그럼 추가되는 각 요소의 들이 스택과 힙 중 정확히 어디에 저장되는 지
헷갈리기 시작합니다.

게다가 var l = new List<int>(10000000); 이 변수는 아무리 봐도 힙에 생성되었을 텐데
GC의 동작이 0회 였다면 변수 l 은 어떻게 된 건지 모르겠습니다.

요약하자면

1. 배열로 선언하여 생성한 변수의 메모리 위치는 스택인지 힙인지?
2. 생성 위치가 힙이라면 GC의 대상이 되어야하는데 테스트 코드에서 대상이 되지 않은 이유는 무엇인지?
3. 그렇다면 값형식 배열의 요소들이 실제로 어디에 적재되는지?
4. var l = new List<int>(10000000) 이 변수는 어디에 생성되고 어떻게 사라졌는지?

정도 입니다.


지식적으로 그냥 그렇다 라고 생각하고 넘어갔던 것들을

직접 코드로 되집어 보니 정확하게 알고 있는 게 별로 없다는 사실을 새삼 느끼게 됩니다.

송구하지만 도움 부탁드립니다.








[최초 등록일: ]
[최종 수정일: 5/8/2013]


비밀번호

댓글 작성자
 



2013-05-09 03시24분
[정환나라] 허.....
오늘 중요한걸 깨우치고 갑니다....
[guest]
2013-05-09 03시25분
[정환나라] 이거 댓글 기록 시간이 서버 기준으로 남는건가요?
지금 12:25분인데 03시25분으로 나오네요?
[guest]
2013-05-09 04시29분
[정환나라] 제가 초보라 그래서 정확하게 알지는 못하지만 Debug를 하면서 확인해보니 new OperationTimer(string)시에 GC.CollectionCount(0)를 하면 ("Array" 때를 말씀드립니다) 4가 리턴됩니다.
근데 콘솔 출력시 GC.CollectionCount(0)빼기 GC.CollectionCount(0)에서 리턴받은 수를 그대로 빼니 0이 되는것 아닌가요?

즉, 같은 수를 빼니 당연히 0이 되는게 아닌가 하는 말입니다.
[guest]
2013-05-09 04시34분
[정환나라] 그리고 1번을 말씀드리면,
배열로 선언한 변수듸 메모리 위치는 힙 이라고, 본인이 언급하셨던 CLR via C# 458p 중간쯤에

"배열이 참조타입이기 떄문에 언박싱된 100개의 Int32 값을 보관할 메모리 블록이 관리되는 힙에 할당되어야 한다."
라고 되어 있습니다.

그리고 2번은 제가 윗 댓글에 적어 놨구요.

3번은, 배열은 일단 말씀하신대로 System.Array 추상 클래스로 파생된거라 힙에 할당될것입니다. 그게 1번과 연동되는 말이겠죠?

4번도 동일한 답변으로 이어지지 않을까요?

제가 틀린것이 있으면 말씀해주세요. 저도 배우는 처지라서요

[guest]
2013-05-09 05시00분
[이성환] GC.CollectionCount(0) 를 호출하면 가장 최근에 0세대 수집을 수행한 회수가 반환되는 것으로 알고 있습니다. OperationTimer의 생성자에서 이미 수행한 0세대 GC 동작 회수를 기록하고
Dispose() 에서 0세대 동작 회수를 빼면 실제 OperationTimer 객체 생성부터 Dispose() 호출 사이에 발생했던 0세대 GC 동작 회수를 알 수 있기 때문에 틀린 것으로 보이지는 않습니다.
(게다가 OperationTimer는 CLR via C# 내용 그대로 사용했습니다. 제거 틀렸다면 책도 틀렸을 겁니다.)

[guest]
2013-05-09 05시05분
[이성환님]
답변을 모두 달아드리기에는 재미가 없군요. ^^

1. 힙이 맞습니다.
2. GC의 대상이 되는 것은 맞지만, a = null을 했다고 해서 GC가 발생되어야 할까요?
3. 값형식이 메서드의 지역 변수에 있을 때는 스택이지만, 클래스 내부에 있을 때는 힙에 할당됩니다.
4. 힙에 있었고, 2번과 같은 이유입니다.

2번의 답은 스스로 찾아보세요. ^^ 답과는 상관이 없지만, 크기가 85000bytes가 넘어가면 0,1,2 세대의 힙이 아닌 별도로 LOH(Large Object Heap)에 할당됩니다.

정환나라 님... 근데 어떤 걸 깨우치셨다는 거죠? ^^ 덧글 기록 시간은... 음... 일부러 그렇게 설정해 두고 있습니다. 참고로 UTC 시간입니다.
정성태
2013-05-09 05시05분
[이성환] 사실 "배열이 참조타입이기 때문에 언박싱된 100개의 Int32 값을 보관할 메모리 블록이 관리되는 힙에 할당되어야 한다." 의 의미가 명확하지 않아 의문이 시작됐습니다.

즉, 배열이 참조 타입이고 언박싱된 100개의 Int32 값을 보관할 메모리 블록이 관리되는 힙에 할당된다는 얘기가 배열 각 요소들이 박싱되어서 힙에 저장된다는 얘긴지
아니면 각 배열 요소의 참조를 저장할 공간만 100개가 생긴다는 건지 명확하게 이해하지 못했다는 거죠.

만약 배열의 각 요소들이 박싱되어 힙에 저장된다면 실제 생성된 이들 요소 전체가 역시 GC의 수집 대상이 되어야 합니다.
그게 아니라 배열 요소의 참조를 저장할 공간 100개가 생기고 실제 요소는 스택에 저장된다면 스택이 반환될 때 실제 요소들도 제거되겠지만
그렇다하더라도 힙에 생성되었던 참조 저장을 위한 공간 100개는 GC의 수집 대상이 되어야 하는게 아닌 가 하는 것이었습니다.
[guest]
2013-05-09 05시16분
[이성환] 님.
int [100] 배열이 힙에 할당되면, 4byte * 100 = 400바이트 공간이 힙에 할당되고, 관리를 위한 12바이트 영역이 더 할당됩니다. 이에 대해서는 다음의 글을 참고하세요.

.NET Array는 왜 12bytes의 기본 메모리를 점유할까?
; http://www.sysnet.pe.kr/2/0/1173

참고로, 참조타입의 경우 힙에 할당될 때 실제 데이터이외의 관리 데이터가 점유되는 크기는 다음과 같습니다.

primitive type 배열: 12 bytes
기타 type의 배열: 16 bytes
배열이 아닌 일반 참조형 개체: 12 bytes
정성태
2013-05-09 05시32분
[이성환] 아.. 하긴 GC의 동작이 조건이 정해져 있는데 참조만 모두 제거된다고 반드시 동작하라리라는 보장이 없긴하군요.
(하긴 그거 알면 자리 깔아도 될 듯...)

결국 참조가 모두 제거된다고 하더라도 GC 동작이 반드시 발생하는 것은 알 수 없다 정도가 답인 건가요?
LOH 할당 때문에 GC.CollectionCount(2) 로 확인해 봤지만 결과가 같아서 그냥 GC가 동작할 조건이 아닐 거라도 어렴풋이 추측은 하고 있었습니다.
(그게 정답이면 답을 다 알려 주신 거 아닌가요? 아니라면... 더 찾아 봐야하겠지만...;;;;)

그렇다면 마지막으로 하나만 더 확인하고 싶은 것이 있는데요.

결국 List<T>에 값형식 사용이 ArrayList에 비해 크게 이점을 얻는 것은
실제 요소를 가져와서 사용할 때 형변환에 드는 비용이 없다 정도인 게 맞는 지 궁금합니다.

물론 다른 부분도 많겠지만 가장 크게 강조하는 것은 역시
박싱 / 언박싱이 없다고 강조하는데
제가 생각하기에 이미 제네릭을 사용하더라도 요소의 추가 자체는 최소 한 번 박싱되어 힙에 저장되기 때문에
단순히 박싱 / 언박싱 이라고 크게 범주를 잡으면 틀린 말이 되어 버리는 것 아닌 가 하는 것입니다.

정확히 박싱 / 언박싱은 해당 요소를 가져와 사용할 때 발생하지 않기 때문에 이점이 있다

하는 표현이 맞는 게 아닌 가 해서요.

그냥 그렇다고 넘어 가면 될 것을 너무 지엽적인 부분에 매달리는 게 아닌 가 싶네요.
[guest]
2013-05-09 05시39분
[이성환] 아.. 저장 시 이미 같은 T 타입으로 힙에 저장되니까 박싱은 없겠군요.

그럼 그냥 박싱 / 언박싱이 발생하지 않는 다는 말이 맞는 말이 되는군요.

형변환이 없었는데 왜 박싱된다고 생각했는지...

여튼 의문점이 해결됐습니다. 답변 감사드립니다.(__)
[guest]
2013-05-09 11시01분
[정환나라] 뭐... 자세라고 해야 하나요? 그런것 때문이죠. 갑자기 저와는 다른 그런게 느껴져서요 ㅎ
[guest]

[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
5934양승조4/24/202414Reflection 관련 질문 드립니다. [1]
5933vict...4/23/2024145cpp 라이브러리 디버깅 방법 문의 드립니다. [1]
5932EK4/12/2024678다음 제네릭에서 널익셉션이 뜨는 이유를 알수있을까요? [1]
5931이대희4/2/2024972Windows 앱 SDK C# 템플릿의 용도가 무엇인지요? [1]
5930vict...2/22/20241877ef core, FromSqlRaw 맵핑 질문입니다. [4]
5929a2/17/20241949.Net 8 에서 디버거 변경 [1]
5928vict...2/5/20242108wpf에서 대량 데이터 보여주는 방법 추천 부탁드립니다. [1]
5926엄태영1/12/20242053잘못된 Task 사용으로 인한 데드락 관련 질문 입니다. [3]
5925Euni...12/22/20232005Visual Studio에서 nodejs 사용시 npm install -g @vue/cli 실행시 오류 [1]
5924Euni...12/21/20232084Visual Studio에서 nodejs 사용시 C:\Program Files\nodejs\\node.exe" "C:\Program Files\nodejs\\node_modules\npm\bin\npm-cli.js" prefix -g 를 찾지 못하는 설치 오류 [2]
5923정두호12/4/20232536MSSQL 데이터 전송과 공유폴더의 데이터 전송 차이점 [1]
5922Heeg...10/27/20234693C++의 double pointer를 C#에서 구현하는 방법이 잘 안됩니다. [3]
5921한예지 donator10/3/20234465마샬링 정의 및 목적이 궁금합니다. [5]
5920한예지 donator10/3/20233356C#과 WIN32 API 관계 질문드립니다. [4]
5919이건우9/27/20232996WinForm의 로딩속도 관련 질문입니다 [2]
5917한예지 donator9/14/20233093동기화 도구 질문 있습니다. [4]
5916한예지 donator9/3/20233253Thread.Sleep(500), await Task.Delay(500), Task.Delay(500) 차이점이 궁금합니다. [2]
5915한예지 donator8/30/20233115비동기 코드를 for 문 안에 작성한 경우 제어 변수가 올바르게 동작하지 않는 이유가 궁금합니다. [3]
5914한상욱8/11/20232973.net wpf에서 skiasharp 의 skelement 를 canvas로 사용 하고 있습니다. [1]
5913김태우8/10/20233061지역변수로 이해하는 메서드매개변수 게시글 댓글 [3]
5912guest4/25/20235349[참고 - 초보용] Sqlite 디비는 double이 없고 Real이 대신합니다 [3]
5911guest4/24/20233577Form1.cs와 외부 class.cs와 통신 (static async method포함) [4]파일 다운로드1
5910guest4/24/20233414Async 메서드와 try~catch [1]
5909guest4/22/20233822Visual Studio 구매 시(1인 개발자) [4]
5908guest4/22/20233657텅빈 원그리기 [5]
[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...