Microsoft MVP성태의 닷넷 이야기
제네릭과 배열 관련 기초 질문입니다. [링크 복사], [링크+제목 복사]
조회: 11439
글쓴 사람
이성환 (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)
4907심너울10/29/20179504선생님이 쓰신 "시작하세요! C# 7.1~"으로 공부하고 있는데요~ [2]
4906guest10/25/20179076.net core, .net standard 관련해서 궁금합니다 [2]
490510/25/20178348watermark 관련문의 [1]
4903Ho10/25/20178746간단한 서버를 구현해 보고 싶습니다. [2]
4902황재승10/22/20179780C# 7.1 프로그래밍 왕초보자도 이해할 수 있는 수준인가요? [1]
4901김레오10/17/201714477c# 프로그램 난독화 도구인 confuserex라는 도구를 사용하다 여쭙습니다. [2]
4900황준범10/12/201710246ClickOnce 배포관련 질문드립니다. [1]
4899Ques...9/28/201712673C# 프로그램이 "응답 없음" 시에도 계속 독립적으로 돌아가는 타이머 생성법 [3]
4898ssdrm9/27/20179456Clickonce 실행시 보안에 막힙니다 ㅠ [1]
4897김태진9/23/20179634윈도우7 작업관리자의 상세(details)탭 생성에 대해 여쭙습니다. [1]
4896장진국9/19/201710995안녕하세요 WPF 에서 Window객체가 가비지 콜렉션에 의해 수집되지 않는거 같아서 문의드립니다. [1]
4895Ques...9/18/201710654Generic 에 관하여 질문드립니다. [5]
4894얼마전6...9/14/201712552C# 7.1에서 보강된 부분만 PDF로 제공하는 건 아니되나요? [2]
4893BigII9/14/201711583타 언어(JAVA, PHP 등)에서 받은 RSA 개인키 문자열을 이용하여 내용 복호화 가능 여부 [4]
4892Ques...9/13/201710608서브 폼에서는 무거운 작업을해도 속도가 빠를까요 ?? [1]
4891윤현수9/11/201711254Taskbar에 관한 질문입니다. [5]
4890제발9/11/20179784 시작하세요! C# 6.0 프로그래밍 책이 절판됐나요? [5]파일 다운로드1
4889낙낙이9/7/20179213안녕하세요! xsl관련 이야기입니다. [1]
4888heyh...9/6/20179038클릭원스를 수동으로 배포 시 업데이트 할 때 [1]
4887이경현9/4/20179599Windows server 2012 파일 없어짐 현상... [1]
4886질문자9/1/201711477disconnecteditem에 대하여 아시나요? [4]파일 다운로드1
4883솔솔8/30/20179958dataview에서 select한 index 가져오기 [1]
4882user8/30/201712612UI 변경 작업 여러개를 동시에 사용하려면 어떻게 해야되나요 ?? [7]
4884user8/31/20179260    답변글 [답변]: UI 변경 작업 여러개를 동시에 사용하려면 어떻게 해야되나요 ??파일 다운로드1
4881kmi8/30/20179443진행속도가 중간에 더뎌지는 문제가 있는데 해결할 수 있는 방법이 있나 궁금합니다. [3]
4880김호종8/29/20179852HTTP JSON POST 관련 질문 드립니다. [1]
... 31  32  33  34  35  36  37  [38]  39  40  41  42  43  44  45  ...