C# 기초부터 마스터까지 — 9화
GC, 값 형식, 할당 비용으로 성능 이해하기
본 블로그 작성 글들은, 강의를 목표로 하고 제작되었습니다. 또한 작성된 글의 정보도 틀렸을 수 있으니, 주의해주세요.
C#은 GC가 메모리를 관리하지만 할당 비용이 사라지는 것은 아니다. 특히 게임처럼 일정한 프레임 시간이 중요한 프로그램에서는 언제 객체가 만들어지는지 알아야 한다.
값 형식과 참조 형식
public readonly struct Damage
{
public int Value { get; }
public Damage(int value) => Value = value;
}
Damage first = new(10);
Damage second = first;
값 형식은 변수에 값 자체가 들어가고 대입할 때 복사된다. 참조 형식은 관리 힙에 있는 객체를 가리키는 참조가 복사된다.
값 형식이 항상 스택에 있고 참조 형식이 항상 느리다고 외우면 안 된다. 실제 저장 위치와 비용은 사용 문맥, 크기, 런타임 최적화에 따라 달라진다.
boxing 피하기
int score = 100;
object boxed = score;
int restored = (int)boxed;
값 형식을 object나 구현한 인터페이스로 다루면 boxing이 발생할 수 있다. 제네릭 컬렉션을 사용하면 값 타입을 object로 바꾸지 않고 저장할 수 있다.
var scores = new List<int>();
scores.Add(100);
반복 할당 줄이기
private readonly List<Monster> visibleMonsters = new(128);
void CollectVisible()
{
visibleMonsters.Clear();
foreach (Monster monster in monsters)
{
if (monster.IsVisible)
{
visibleMonsters.Add(monster);
}
}
}
목록을 매번 새로 만들지 않고 용량을 가진 버퍼를 재사용하면 할당 횟수를 줄일 수 있다. 문자열 연결, 캡처 람다, LINQ 결과 생성도 반복 경로에서 할당 원인이 될 수 있다.
먼저 측정하기
GC 세대와 수집 방식은 런타임이 관리하며, 짧게 사는 객체가 무조건 문제인 것은 아니다. 가독성을 희생해 모든 코드를 미리 최적화하면 유지보수 비용만 커질 수 있다.
프로파일러로 할당량과 멈춤이 실제 병목인지 확인한 뒤 풀링, 버퍼 재사용, 값 타입을 적용해야 한다. 성능 최적화는 추측으로 문법을 바꾸는 작업이 아니라 측정된 비용의 원인을 줄이는 작업이다.