제네릭 컬렉션의 초기 용량 설정을 통한 최적화

제네릭 컬렉션의 초기 용량 설정을 통한 최적화

닷넷에서 제공하는 제네릭 컬렉션들은 효율적인 데이터 관리와 접근을 위해 설계되어 있습니다. 그러나 많은 양의 데이터를 추가할 때는 내부적으로 배열의 재할당과 복사가 발생하여 성능 저하를 초래할 수 있습니다. 이러한 문제를 완화하기 위해 초기 용량을 설정하면 재할당 횟수를 줄여 성능을 향상시킬 수 있습니다.

제네릭 컬렉션의 초기 용량 설정

제네릭 컬렉션들은 내부적으로 동적 배열을 사용하여 데이터를 저장합니다. 기본적으로 용량은 필요에 따라 자동으로 증가하지만, 이 과정에서 배열의 재할당과 복사가 발생하여 성능 저하를 유발할 수 있습니다. 특히 많은 양의 데이터를 한꺼번에 추가하는 경우, 초기 용량을 적절히 설정하면 이러한 재할당 비용을 최소화하여 성능을 향상시킬 수 있습니다.

동작 원리

  • 기본 생성자로 초기화: 초기 용량은 0으로 설정됩니다.
  • 첫 번째 요소 추가 시: 첫 번째 요소가 추가될 때 내부적으로 크기가 4인 동적 배열이 생성됩니다.
  • 동적 배열 크기 조정: 현재 용량을 초과하는 요소가 추가되면 크기가 일정한 비율로 증가한 새로운 배열을 할당합니다.
  • 재할당과 복사: 기존의 데이터를 새로운 배열로 복사해야 하므로, 많은 요소를 추가할수록 복사 비용이 누적됩니다. 예를 들어, List<T>를 기본 생성자로 초기화한 후 10개의 요소를 추가하면 다음과 같은 방식으로 동작합니다:
  • 초기 용량은 0 입니다.
  • 첫 번째 요소를 추가하면 크기가 4인 배열이 할당됩니다.
  • 용량이 초과될 때마다 크기가 2배 증가하며, 10개의 요소를 추가하는 동안 총 3번의 재할당이 발생합니다. ( 4 → 8 → 16 으로 증가) 이러한 이유로 많은 요소를 추가할 것으로 예상될 경우, 초기 용량을 설정하는 것이 성능 최적화에 큰 도움이 됩니다. 이는 List, Dictionary, Queue, Stack, HashSetLinkedList와 SortedDictionary를 제외한 모든 제네릭 컬렉션에 적용됩니다.

테스트 코드

using System;
using System.Diagnostics;
using System.Collections;
using System.Collections.Generic;
namespace MyCompiler {
    class Program {
        public static void Main(string[] args) {
		    GC.Collect(); 
		    GC.WaitForPendingFinalizers(); 
		    GC.Collect();
		    
            // 초기 용량 설정 없이
			Stopwatch sw = Stopwatch.StartNew();
			List<int> numbers = new List<int>();
			for (int i = 0; i < 10000000; i++)
			{
				numbers.Add(i);
			}
			sw.Stop();
			Console.WriteLine($"초기 용량 미설정: {sw.ElapsedMilliseconds}ms");
			GC.Collect();
			GC.WaitForPendingFinalizers(); 
			GC.Collect();
			
			// 초기 용량 설정
			sw.Restart();
			List<int> numbersWithCapacity = new List<int>(10000000);
			for (int i = 0; i < 10000000; i++)
			{
				numbersWithCapacity.Add(i);
			}
			sw.Stop();
			Console.WriteLine($"초기 용량 설정: {sw.ElapsedMilliseconds}ms");
			// 출력: 
			// 초기 용량 미설정: 34ms
			// 초기 용량 설정: 15ms			
        }
    }
}
  • 초기 용량을 설정한 경우와 설정하지 않은 경우를 비교하면, 초기 용량을 설정한 경우가 메모리 할당 및 배열 복사 횟수가 줄어들어 성능이 향상되는 것을 확인할 수 있습니다.

초기 용량 설정 예시

List<T>

List<int> numbers = new List<int>(capacity: 100);

Dictionary<TKey, TValue>

Dictionary<int, string> people = new Dictionary<int, string>(capacity: 50);
  • 해시 테이블의 크기를 미리 설정하여 재해시rehashing 비용을 줄일 수 있습니다.

미적용 제네릭 컬렉션

LinkedList<T>

LinkedList<T>는 다른 제네릭 컬렉션들과는 달리 용량Capacity의 개념이 없습니다. 이는 LinkedList가 이중 연결 리스트로 구현되어 있기 때문입니다. 연결 리스트는 각 노드가 임의의 메모리 공간에 동적으로 할당되며, 노드 간의 포인터로 다른 노드를 연결합니다. 이러한 방식은 연속적인 메모리 공간을 필요로 하지 않으며, 용량을 미리 설정하거나 제한할 필요가 없습니다.

SortedDictionary<TKey, TValue>

이 컬렉션은 내부적으로 이진 트리 구조를 사용하기 때문에 용량 설정의 개념이 적용되지 않습니다. 요소를 삽입할 때 트리의 균형을 유지하는 방식으로 동작합니다.

동시성 컬렉션과 불변 컬렉션

동시성 컬렉션

동시성 컬렉션에서도 초기 용량을 설정하여 성능을 최적화할 수 있습니다. 다만, 동시성 컬렉션은 스레드 안전성을 보장하기 위해 내부적으로 동기화 메커니즘lock 또는 lock-free을 사용하기 때문에, 일반적인 단일 스레드 컬렉션에 비해 초기 용량 설정의 효과가 상대적으로 제한적일 수 있습니다. 즉, 초기 용량 설정을 통해 재할당과 복사 비용을 줄이는 것은 여전히 유효하지만, 동기화 비용이 더 큰 경우 그 효과가 다소 상쇄될 수 있습니다. 동시성 컬렉션에서 주로 성능을 좌우하는 요소는 동시 접근 시의 동기화 비용이기 때문에, 초기 용량 설정의 최적화 효과가 일반 컬렉션만큼 명확하게 나타나지 않습니다.

using System;
using System.Diagnostics;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Concurrent;
namespace MyCompiler {
    class Program {
        public static void Main(string[] args) {
	        GC.Collect(); 
		    GC.WaitForPendingFinalizers(); 
		    GC.Collect();
            // 초기 용량 설정 없이
			Stopwatch sw = Stopwatch.StartNew();
			ConcurrentDictionary<int, string> concurrentDict = new ConcurrentDictionary<int, string>();
			for (int i = 0; i < 10000000; i++)
			{
				concurrentDict.TryAdd(i, i.ToString());
			}
			sw.Stop();
			Console.WriteLine($"초기 용량 미설정: {sw.ElapsedMilliseconds}ms");
			
			GC.Collect(); 
		    GC.WaitForPendingFinalizers(); 
		    GC.Collect();
			// 초기 용량 설정
			sw.Restart();
			concurrentDict = new ConcurrentDictionary<int, string>(concurrencyLevel: 4, capacity: 10000000);
			for (int i = 0; i < 10000000; i++)
			{
				concurrentDict.TryAdd(i, i.ToString());
			}
			sw.Stop();
			Console.WriteLine($"초기 용량 설정: {sw.ElapsedMilliseconds}ms");
			// 출력: 
			// 초기 용량 미설정: 1997ms
			// 초기 용량 설정: 1213ms			
        }
    }
}

불변 컬렉션

불변 컬렉션은 불변성Immutable을 보장하기 위해 설계되었습니다. 불변 컬렉션은 생성된 이후 변경할 수 없으며, 변경 작업이 필요할 경우 새로운 복사본을 생성합니다. 따라서 Capacity와 같은 용량 개념은 크게 의미가 없습니다. 새로운 요소를 추가하거나 삭제할 때마다 새로운 인스턴스가 생성되므로, 초기 용량 설정을 통한 최적화보다는 메모리 관리 측면에서 신중한 사용이 필요합니다.