Microsoft MVP성태의 닷넷 이야기
제네릭과 배열 관련 기초 질문입니다. [링크 복사], [링크+제목 복사]
조회: 11338
글쓴 사람
이성환 (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]

... 46  [47]  48  49  50  51  52  53  54  55  56  57  58  59  60  ...
NoWriterDateCnt.TitleFile(s)
3663Sung...12/10/201511830UWP 서버 프로그래밍 작업 중 [5]
3662Sung...12/9/201511688uwp앱에서 접속한 클라이언트 주소값을 받아서 표시하고 싶은데 어떤 메소드를 써야 할까요? [7]
3661김무진12/9/201511065Oracle 환경에서 데이터를 조회할때 한글이 ? 이렇게 표시가됩니다. [1]
3660질문자12/7/201512944OpenFileDialog 호출시 hang걸리는 문제 [4]
3659Sung...12/4/201512601UWP 앱에서 textBox로 클라에서 받은 값을 나타내고 싶은데 안되고 있습니다. [5]
3658강준12/3/201511837Visual Studio (Xamarin) vs Eclipse [2]
3656DEVY...12/1/201510752MasterPageFile 사용시 다국어 처리 질문입니다. [1]
3653DEVY...11/26/201512240ds:Signature 질문입니다. [7]
3654윤용한11/27/201511987    답변글 [답변]: ds:Signature 질문입니다. [3]
3657윤용한12/1/201514278    답변글 [답변]: ds:Signature 질문입니다. [1]
3651노태현11/20/201544969MariaDB - ASP.NET오류의 원인조차 못 찾고 있습니다.. [2]파일 다운로드2
3652노태현11/20/201511817    답변글 [답변]: MariaDB - ASP.NET오류의 원인조차 못 찾고 있습니다.. [3]
3649kokon11/17/201512275예제 파일 실행이 안 되네요 [5]
3647Sang...11/15/201510740Part 3 목차? [5]
3646힘찬도약11/13/201515570c# mscorlib System.IO IOException [8]파일 다운로드2
3644힘찬도약11/11/201514726c# user.config파일 [2]
3645spow...11/13/201511727    답변글 [답변]: c# user.config파일 - Json.NET을 이용한 설정파일 처리 [1]파일 다운로드1
3643힘찬도약11/11/201513526C# 함수의 processing time과 재호출 [14]
3642.net11/10/201512215c# 으로 작성된 com+ 에 대한 문제입니다. [2]
3641힘참도약11/9/201512632c# log file 관련해서 질문드립니다. [5]
3638윤창선11/4/201513214사설IP가 부여된 무선라우터간 영상전송 관련 문의 [8]
3634Hyun...11/2/201510778c# 에서 webkit browser에서 webgl을 이용하는 사이트에 접속이 안됩니다. [1]
3633힘찬도약10/31/201511411mysql insert where not exists [6]
3632힘찬도약10/27/201512098C# Lock 관련해서 질문드립니다. [6]
3655iwc11/30/201510201    답변글 [답변]: C# Lock 관련해서 질문드립니다.
3631강준10/26/201513059iis 8.5 preload 기능에 대해 질문이 있습니다. [9]
... 46  [47]  48  49  50  51  52  53  54  55  56  57  58  59  60  ...