매번 기본적인 질문만 드려 죄송스럽게 생각하고 있습니다.
이번에도 도움을 얻고자 이렇게 또 질문을 올리게 되었습니다.
코드가 포함되어 있어 내용 약간 깁니다.
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]