ValueTuple은 구조체, Tuple은 클래스입니다. 이 한 줄이 성능 차이의 대부분을 설명합니다. 구조체인 ValueTuple은 스택 또는 포함하는 객체 내부에 저장되어 할당과 GC 비용이 적고, 클래스인 Tuple은 힙 할당과 GC 압력이 발생합니다. 실용적인 관점에서 둘의 속도, 메모리 차이를 간단한 코드와 함께 정리합니다.
1. 핵심 차이 요약
- ValueTuple: 구조체(struct), 필드가 공개이며(mutability) 이름 지정과 분해 할당 지원, 일반적으로 할당 없음(escape 시 제외), 빠름.
- Tuple: 참조형(class), 불변(immutable), 항상 힙 할당, GC 대상, Item1/Item2 등만 제공, 느림.
- 성능 포인트: 작은 데이터(예: 2~3개 값) 왕복 시 ValueTuple이 유리합니다. 단, 큰 값을 여러 개 담아 빈번히 복사하면 구조체 복사 비용이 생길 수 있습니다.
2. Stopwatch로 빠른 감 잡기
아래 코드는 500만 번 생성/접근을 반복하여 대략적인 경향을 봅니다. 환경에 따라 수치는 달라지지만 ValueTuple이 일반적으로 더 빠르고 GC 할당이 적습니다.
using System;
using System.Diagnostics;
class Program
{
const int N = 5_000_000;
static void Main()
{
Warmup();
var sw = Stopwatch.StartNew();
long sum1 = 0;
for (int i = 0; i < N; i++)
{
var t = (i, i + 1); // ValueTuple (struct)
sum1 += t.Item1 + t.Item2;
}
sw.Stop();
Console.WriteLine($"ValueTuple: {sw.ElapsedMilliseconds} ms, sum={sum1}");
GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect();
sw.Restart();
long sum2 = 0;
for (int i = 0; i < N; i++)
{
var t = Tuple.Create(i, i + 1); // Tuple (class)
sum2 += t.Item1 + t.Item2;
}
sw.Stop();
Console.WriteLine($"Tuple: {sw.ElapsedMilliseconds} ms, sum={sum2}");
}
static void Warmup()
{
for (int i = 0; i < 1_000; i++)
{
var a = (i, i + 1);
var b = Tuple.Create(i, i + 1);
_ = a.Item1 + a.Item2 + b.Item1 + b.Item2;
}
}
}
- 일반적 관찰: ValueTuple 구문은 힙 할당이 없거나 적고, Tuple은 반복마다 객체가 생성되어 GC가 늘어납니다. 긴 루프에서 체감 차이가 큽니다.
3. 제대로 측정하기: BenchmarkDotNet
정확한 비교는 BenchmarkDotNet을 권장합니다. 패키지(BenchmarkDotNet) 추가 후 아래를 실행합니다.
using System;
using System.Linq;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
[MemoryDiagnoser]
public class TupleBench
{
private int[] data = Array.Empty<int>();
[GlobalSetup]
public void Setup()
{
data = Enumerable.Range(0, 1000).ToArray();
}
[Benchmark]
public long ValueTuple_CreateAccess()
{
long sum = 0;
for (int i = 0; i < data.Length; i++)
{
var t = (data[i], data[i] + 1);
sum += t.Item1 + t.Item2;
}
return sum;
}
[Benchmark]
public long Tuple_CreateAccess()
{
long sum = 0;
for (int i = 0; i < data.Length; i++)
{
var t = Tuple.Create(data[i], data[i] + 1);
sum += t.Item1 + t.Item2;
}
return sum;
}
}
public class Program
{
public static void Main() => BenchmarkRunner.Run<TupleBench>();
}
- 예상 결과: ValueTuple은 할당(Allocated) 0B 또는 매우 적게, Tuple은 데이터 길이에 비례한 힙 할당이 보고됩니다. 평균 실행 시간도 ValueTuple이 앞서는 경향입니다.
4. 결과 해석과 메모리 관점
- ValueTuple은 구조체라서 지역 사용과 반환에 유리하며, escape 하지 않으면 힙 할당이 없습니다. 반복적인 생성/접근 패턴에서 이점이 큽니다.
- Tuple은 참조형이라 생성 시마다 힙에 객체가 생기고, GC 대상이 됩니다. 짧은 루프나 고빈도 경로에서는 성능 병목이 됩니다.
- 단, ValueTuple은 값 복사가 발생합니다. 포함 필드가 크거나 중첩되어 크기가 커질수록 복사 비용이 커질 수 있습니다.
5. 언제 무엇을 쓸까
- ValueTuple 권장: 여러 값을 간단히 반환/전달, 고빈도 경로(루프/핫패스), 분해 할당과 이름 지정으로 가독성 향상을 원할 때.
- Tuple 고려: 참조 동일성(Identity)이 필요하거나, 딕셔너리 키로 넣은 뒤 값 변경 위험을 피하고 싶을 때(불변성), 매우 큰 페이로드를 자주 전달하여 값 복사 비용이 커질 때(전용 클래스로 모델링도 고려)입니다.
6. 주의사항과 최적화 팁
- ValueTuple은 가변입니다. 딕셔너리 키로 사용 후 필드를 바꾸면 해시가 바뀌어 문제를 일으킬 수 있습니다. 이 경우 불변 타입을 사용하세요.
- 큰 ValueTuple을 빈번히 전달하면 복사 비용이 증가합니다. 필요한 경우 메서드 매개변수에 in 키워드를 사용해 읽기 전용 참조로 전달을 검토합니다.
- 7개를 초과하는 요소로 ValueTuple/Tuple을 구성하면 중첩이 발생해 가독성과 비용이 나빠집니다. 명시적인 레코드/클래스/구조체를 정의하는 것이 좋습니다.
- 인터페이스나 object로 박싱하면(ValueTuple -> object) 힙 할당이 생깁니다. 제네릭을 활용해 박싱을 피하세요.
- async/iterator 캡처 시에도 일반적으로 ValueTuple이 유리하지만, 불필요한 큰 값 복사를 피하도록 구조를 단순화하는 것이 좋습니다.
7. 결론
대부분의 실무 시나리오에서 ValueTuple이 Tuple보다 빠르고 메모리 효율적입니다. 짧고 빈번한 경로라면 ValueTuple을 기본 선택으로, 불변 참조형이 필요한 경우나 큰 데이터 모델링이 필요할 때는 전용 타입(레코드/클래스)로 명확히 표현하는 것을 권장합니다.
'C#' 카테고리의 다른 글
| C# 타입 변환 연산자와 Custom TypeConverter 정의하기 (0) | 2026.05.13 |
|---|---|
| C# 중첩 스위치 패턴과 복잡한 조건 매칭 구현 (0) | 2026.05.12 |
| C# Immutable Collections 사용과 장점 (0) | 2026.05.11 |
| C# Expression Tree로 동적 코드 생성하기 (0) | 2026.05.11 |
| C# AssemblyLoadContext로 플러그인 아키텍처 만들기 (0) | 2026.05.08 |