ZLinq

박지예·2026년 8월 6일

공부2026

목록 보기
14/20

제로 할당 (zero - allocation) Linq 라이브러리
링크

내부 구현을 인터페이스 + 클래스 기반에서 -> 구조체(struct) 기반으로 전면 재설계해서 쿼리 체인 자체가 힙 할당을 일어나지 않게 만들었다.

사용법은 컬렉션 앞에 AsValueEnumerable() 한줄을 붙이면 된다.

// 기존 LINQ
var result = list.Where(x => x > 0).Select(x => x * 2).ToList();

// ZLinq
var result = list.AsValueEnumerable().Where(x => x > 0).Select(x => x * 2).ToList();

핵심

기존 Linq와 99% 호환

.AsValueEnumerable 만 붙이면 기존 Linq 문법을 그대로 사용할 수 있다.

체인이 아무리 길어져도 중간 할당이 0이다

기존 Linq는 연산자를 하나 붙일 때 마다 힙 할당이 늘어나지만, zLinq는 늘지 않는다.

기존 Linq

기존 linq가 어떻게 힙에 할당 되는지 예시를 들어 설명하겠다.

list.Where(pred).Select(f).ToList()

이 한줄에서 힙에 생성되는 건

  1. Iterator 객체 (연산자당 한 개)
  • Where 은 WhereIterator 클래스를, Select 는 또 하나를 힙에 생성
  1. 델리게이트
  • 람다는 Func<T,bool> 델리게이트 객체로 변환된다. 외부 객체를 캡쳐하면 display class가 힙에 생성된다.
  1. Enumerator 박싱
  • Enumerator<T> 인터페이스를 경유하면 List<T>의 struct 열거자가 박싱되어 힙으로 간다.

ZLinq의 핵심 - 어떻게 할당을 없앴는가

쿼리 체인을 힙의 객체 그래프가 아니라 스택 위의 중첩 구조체로 만든다

AsValueEnumerable()을 호출하면 IEnumerable<T> 가 아니라 ValueEnumerable<TEnumerable,T>라는 구조체가 반환된다.

이후 Where절을 붙이면 그 구조체를 감싸는 또 다른 구조체가 되고, Select 를 보내면 또 감싼다.

list.AsValueEnumerable()   // ValueEnumerable<FromList<int>, int>
    .Where(x => x > 0)     // ValueEnumerable<Where<FromList<int>, int>, int>
    .Select(x => x * 2)    // ValueEnumerable<Select<Where<FromList<int>, int>, int, int>, int>

타입들이 마트료시카처럼 중첩되는데, 이 전체가 값 타입이라 스택에만 존재한다.

실제 적용 예시

1) Quest 에서 DailyQuest 찾기

// 변경 전: ActiveQuestIds()가 List<int>를 만들고, 그걸 다시 LINQ로 순회 (2회 순회 + 중간 List)
private List<DailyQuest> ActiveDailies => ActiveQuestIds()
    .Select(id => QuestTable.Find(id) as DailyQuest)
    .Where(quest => quest != null)
    .Cast<DailyQuest>()
    .ToList();

// 변경 후: 퀘스트 상태를 한 번만 순회, 중간 List<int> 소멸
private List<DailyQuest> ActiveDailies => _questStates.AsValueEnumerable()
    .Where(entry => entry.Value)
    .Select(entry => QuestTable.Find(entry.Key))
    .OfType<DailyQuest>()
    .Where(quest => quest.RemainingCount > 0)
    .ToList();

이 코드는 단순히 ZLinq 만 사용한 것이 아니라 구조적인 개선도 같이 진행되었다.

기존 DailyQuest 를 이미 Select 하고 있었는데,
Cast를 또 진행하고 있다. (Cast<T>는 실제로 T타입으로 전환한다.)

그럴 필요 없이 OfType을 통해 필요한 Type check 와 null check 를 압축했다.

2) 활성 QuestID

public List<int> ActiveQuestIds()
{
    return _questStates.AsValueEnumerable()   // ← 이 한 줄만 추가
        .Where(entry => { ... })
        .Select(entry => entry.Key)
        .ToList();
}

가장 정석적인 ZLINQ 사용

3) ZLinq 를 사용하지 않는 경우!!

// 변경 전
result.AddRange(TraitCatalog.Values.Where(trait => trait.Matches(context)));

// 변경 후 — ZLinq가 아니라 foreach
foreach (var trait in TraitCatalog.Values)
    if (trait.Matches(context))
        result.Add(trait);

ZLinq 를 사용하지 못하는 경우의 예시다.

  1. TraitCatalog 는 딕셔너리인데, Dictionary.Values 에는 ZLinq 전용 오버로드가 없다 (1.5.6 기준)

그러면 결국 열거자 박싱으로 ZLinq 를 사용하는 의미가 없어진다.

  1. 열거자 반환이 아니기 때문에 list.AddRange() 로 넘길 수 없다.

그래서 결국 foreach + if + Add 를 통해서 하나씩 순회하면서 더하는게 할당도 추가로 생기지 않고 위 구조에서 최적의 로직이다.

profile
게임 클라이언트 개발자

0개의 댓글