Microsoft MVP성태의 닷넷 이야기
제네릭과 배열 관련 기초 질문입니다. [링크 복사], [링크+제목 복사]
조회: 11321
글쓴 사람
이성환 (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)
5016지누7/19/20187450    답변글 [답변]: ASP.Net Core 를 안드로이드에서 작동하는게 가능한가요?
5014최홍준7/16/20187333Credential Provider Child Window 띄우기 [1]
5013Soul...7/16/20187935HtmlElement 스크롤 문의 [2]
5012황윤하7/13/20188474SMTP 예외처리 관련 질문 [1]
5011guest7/9/201815989c#에서 c++ DllImport 문의 입니다. [1]파일 다운로드2
5010C# 꿈...7/8/20188617C# WebService 에서 질문이 있습니다. [1]
5009C#초보자7/5/20189889[C#] 감시 프로그램을 제작했는데, 작동은 하나 폼이 멈춰버렵니다... [1]파일 다운로드1
5008도와주세요7/5/20189544 리스트 아이템의 타입을 추출해서 다시 재활용하고 싶어요.2 [6]파일 다운로드1
5007도와주세요7/4/201810667리스트 아이템의 타입을 추출해서 다시 재활용하고 싶어요. [1]
5006정환나라7/4/20189649소켓 통신과 쓰레드에 관해 질문드립니다 [4]
5005psh7/4/20188556인용에 관련해 문의 드립니다 [1]파일 다운로드2
5004까오리7/4/20188641iis8.0에서 닷넷1.1을 사용하기 위한 질문입니다. [1]
5002멋쟁이7/2/20188920WPFApp에 관한 초보 질문입니다. [1]파일 다운로드1
5001김학완7/1/20189101시작하세요 C# 7.1 프로그래밍 P53쪽 질문입니다. [2]
5000레몬6/26/201810751안녕하세요 성태님 도움으로 C# 네이버 카페 스팸글 작성되면 삭제되는 프로그램을 만들었는데요..여쭤볼게 하나 있습니다. [3]
4999jt6/21/20188556검색어 입력, 엔터 > 페북 로그인창으로 이동합니다. [4]
4998개발희망6/11/20189505C# 형변환 질문있습니다! [1]
4997초보개발자6/5/201821742C++ dll C#에서 사용하는데 보호된 메모리 오류떠요 한번만 도와주세요ㅜ [1]파일 다운로드1
4996swc6/5/20189971현업에서 주로 사용하는 DB업데이트 방법 질문입니다. [1]파일 다운로드1
4993학생6/3/20188167질문 드립니다 [1]
4991최진안5/24/20188163Credential Provider 질문 [2]
4990heyg...5/21/20189048Sybase Adaptive Server Anywhere 6 버전에 대한 질문입니다. [9]
4989강한음5/18/201811284clickonce 배포 후 실행 무응답 [4]
4988C#7....5/17/20188065dynamic 키워드를 사용한 객체 핸들링에 대해서 여쭤보려고 합니다. [1]
4987포플러5/14/20189080닥터왓슨 로그 - c0000005 (액세스 위반) 분석 부탁드려도 될까요? [2]
4986익명5/14/201810784비주얼 스튜디오 wpf 프로젝트에서 어떻게 하면 exe파일과 실행에 필요한 파일들을 분리해서 정리해서 디렉토리로 묶을 수 있을까요? [4]
... 31  32  33  [34]  35  36  37  38  39  40  41  42  43  44  45  ...