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

... 31  32  33  34  35  36  37  38  39  40  41  [42]  43  44  45  ...
NoWriterDateCnt.TitleFile(s)
4797김철환1/13/201711562책에 관한 질문입니다 [3]
4796Bere...1/13/201711690++ 후위연산자와 = 을 함께 사용할 때 생성되는 IL 코드 관련... [2]
4795김철환1/11/201712784이벤트 부분을 읽고 있는데 이해가 안되서 질문합니다.. [11]
4794김철환1/10/201710081안녕하세요 c# 6.0 책을 구매한 사람인데요 [3]
4793장준영1/7/201712117안녕하세요 c언어 처음 공부해보는 학생입니다 [4]파일 다운로드1
4792김재영1/4/201713125소스코드 공개 전 성태님의 의견을 듣고싶습니다 [3]
4791C#초보12/28/201612937비동기 소켓 close시 ObjectDisposedException 문제점 질문 있습니다.. [1]
4790미나리12/24/201613174파워포인트 쇼 제어 SimpleHttpServer.cs 작동문제 [4]파일 다운로드1
4789김솔지12/21/201612013프린트 시, 프린트하는 파일의 파일명 구하는 부분에 대해서 질문드립니다. [1]
4788짜두12/19/201611740Visual Studio 2015 에서 msbuild 12 사용 [5]
4787guest12/18/201613538VLC라이브러리에 대해 아시나요? [3]파일 다운로드1
4785Hyou...12/16/201613663WPF 개발 시 MVVM 프레임워크 사용 [2]
4784ds12/15/201610315문의 드립니다. [2]
4783후배12/13/201611916MemoryStream에 관한 질문 입니다. [5]
4782김형민12/6/201610250[ C# 6.0 ] 126p 오타인가요? [6]
4781질문자11/29/201610366ms워드 저장 오류 [1]
4780최진11/28/201614805안녕 하세요 빌드 관련해서 질문드립니다 꾸벅 [4]
4779손니11/28/201611143안녕하세요 질문하다 드려도 될까요 [3]
4778김상호11/25/201610676재귀호출->비재귀호출 [2]파일 다운로드1
4777권오영11/12/201612928아래 질문 상세 소스전체입니다.. [3]
4776권오영11/11/201610842제가 이클립스를 공부중인데..이상한것을 찾았습니다.. [2]
4775이성환11/11/201614298안녕하세요. SnapsToDevicePixels 질문입니다. [5]파일 다운로드1
4774popo11/10/201610907.net SSL통신 관련 질문 드립니다. [1]
4773김상호11/4/201613403재귀함수 반복문 변환 [1]파일 다운로드1
4772자연인10/27/201614394hwpctrl을 사용하는 사이트에서 나와 브라우저를 종료하면 오류메세지가 나옵니다. [1]파일 다운로드1
4771문종훈10/18/201614437.net 소스 질문이 있습니다 [2]
... 31  32  33  34  35  36  37  38  39  40  41  [42]  43  44  45  ...