제로 할당 (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();
.AsValueEnumerable 만 붙이면 기존 Linq 문법을 그대로 사용할 수 있다.
기존 Linq는 연산자를 하나 붙일 때 마다 힙 할당이 늘어나지만, zLinq는 늘지 않는다.
기존 linq가 어떻게 힙에 할당 되는지 예시를 들어 설명하겠다.
list.Where(pred).Select(f).ToList()
이 한줄에서 힙에 생성되는 건
쿼리 체인을 힙의 객체 그래프가 아니라 스택 위의 중첩 구조체로 만든다
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>
타입들이 마트료시카처럼 중첩되는데, 이 전체가 값 타입이라 스택에만 존재한다.
// 변경 전: 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 를 압축했다.
public List<int> ActiveQuestIds()
{
return _questStates.AsValueEnumerable() // ← 이 한 줄만 추가
.Where(entry => { ... })
.Select(entry => entry.Key)
.ToList();
}
가장 정석적인 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 를 사용하지 못하는 경우의 예시다.
그러면 결국 열거자 박싱으로 ZLinq 를 사용하는 의미가 없어진다.
그래서 결국 foreach + if + Add 를 통해서 하나씩 순회하면서 더하는게 할당도 추가로 생기지 않고 위 구조에서 최적의 로직이다.