버리기 위해 할당한 7.8MB의 키들
프로파일러를 통해 코드 내에 숨겨진 비용이 있다는 사실을 발견했습니다.
제 요청 핸들러는 거대한 문자열에서 토큰을 읽어 그 가중치를 합산합니다. 로직은 제대로 작동했지만, 메모리가 문제였습니다.
빈번하게 실행되는 루프(hot loop) 내부에서 7.8MB의 문자열을 할당했습니다. 딕셔너리 조회를 할 때마다 새로운 키를 생성하고, 사용 직후 바로 버리는 식이었죠.
원인은 바로 Substring이었습니다. 슬라이싱을 할 때마다 새로운 문자열 객체가 생성되었습니다.
200,000개 토큰
- Method A (Substring): 23.4ms, 7,812KB 할당됨.
- Method B (Span Lookup): 14.9ms, 0KB 할당됨.
Method B는 1.5배 더 빠르며 가비지(garbage)를 생성하지 않습니다.
.NET 9에서는 GetAlternateLookup을 호출할 수 있습니다. 새로운 문자열을 만드는 대신 ReadOnlySpan<char>를 전달하는데, 이는 원본 문자열을 가리키는 창(window) 역할을 할 뿐 복사가 일어나지 않습니다.
딕셔너리는 이 span을 기존 키들과 직접 해싱하여, 추가 할당 없이 동일한 결과를 반환합니다.
주의 사항
StringComparer.Ordinal및StringComparer.OrdinalIgnoreCase와 함께 작동합니다.- alternate lookup을 지원하지 않는 사용자 정의 비교자(custom comparer)를 사용하는 경우 런타임에 예외가 발생합니다.
- 파서, 로그 프로세서, CSV 스캐너와 같은 핫 패스(hot paths)에 이상적입니다.
- 키가 몇 개 없는 단순한 조회 작업에는 사용하지 마세요.
이제 저는 코드에서 Substring 다음에 TryGetValue가 이어지는 패턴을 찾아다닙니다. 그런 패턴은 엄청난 양의 메모리를 낭비하기 때문입니다.
Source: https://dev.to/ssukhpinder/78-mb-of-keys-i-allocated-just-to-throw-away-50mn
Optional learning community: https://github.com/ssukhpinder/dev-to-code-samples/tree/main/023-dictionary-alternate-lookup
