std::unordered_map
std::unordered_map
- 키 - 값 (Key - Value) 쌍을 저장하는 컨테이너
(내부 원소 타입은 pair<>)
- 일반적으로 ‘유일 키’ 방식
- 일반적으로 ‘유일 키’ 방식
- 해시 테이블을 기반으로 작동
- Key를 Hash 함수에 넣어 Hash 값을 만든 후,
bucket_count 만큼 나누어 지정된 위치에 요소를 저장 - ‘임의의 위치’에 저장하기에
Iterator 기반 탐색 시, ‘정렬되지 않은’ 상태로 순회
(‘unordered’라 말하는 이유)
- Key를 Hash 함수에 넣어 Hash 값을 만든 후,
- 삽입/삭제/접근 이 모두 O(1)!
- 다만 ‘해시 충돌’이 발생하지 않는 가정!
- 다만 ‘해시 충돌’이 발생하지 않는 가정!
-
- 해시 충돌?
- ‘다른 key’임에도 해시 함수가 내놓은 Hash 값이 같은 경우
- 이때, 동일한 ‘위치’에 요소를 저장해야 하는 상황이 발생
- unordered_map은 내부적으로 ‘연결 리스트’를 사용하는 ‘체이닝’ 방식으로
해당 문제를 해결
- 다만, 해시 충돌이 너무 잦게 발생하면 ‘연결 리스트’ 탐색으로 인하여
점점 탐색이 느려질 수 있음
- 다만, 해시 충돌이 너무 잦게 발생하면 ‘연결 리스트’ 탐색으로 인하여
- 해시 충돌?
- 내부적으로 vector의 capacity와 비슷하게
bucket_count 라는 요소가 존재
- 실제 요소의 개수가 버켓 카운트 이상이 되면
(실제로는 (저장 원소 개수) / (버캣 개수) > 1.0)
충돌 확률이 높아지기에 ‘rehash’가 발생함! - reserve로 초기에 설정 가능
(임의의 size 예상이 가능하다면, reserve를 통해
rehash 빈도를 줄일 수 있음)
- 실제 요소의 개수가 버켓 카운트 이상이 되면
-
rehash?- bucket_count의 개수를 늘리고
기존의 데이터를 다시 해싱하며
‘새로운’ 버켓 위치에 배치하는 과정
모든 요소를 전부 건드리기에 비용이 큰 작업(O(N))
- unordered_map 사용시, 어느 순간 성능이 저하되는 가장 큰 이유!
#include <iostream>
#include <unordered_map>
#include <string>
int main() {
// 선언: Key는 string, Value는 int
std::unordered_map<std::string, int> scores;
// 1. 데이터 삽입
scores["Alice"] = 90;
scores["Bob"] = 85;
scores.insert({"Charlie", 95});
// 2. 데이터 수정 (Key가 이미 존재하면 Value 업데이트)
scores["Alice"] = 92;
// 3. 데이터 검색 (안전한 방법)
std::string target = "Bob";
// find()는 데이터를 못 찾으면 end() iterator를 반환함
if (scores.find(target) != scores.end()) {
std::cout << target << "의 점수: " << scores[target] << std::endl;
} else {
std::cout << target << "을(를) 찾을 수 없습니다." << std::endl;
}
// 4. 전체 순회 (Iterator 사용)
// 주의: 출력 순서는 입력 순서와 다르며, 뒤죽박죽일 수 있음
std::cout << "\n--- 전체 명단 ---" << std::endl;
for (const auto& pair : scores) {
std::cout << "이름: " << pair.first << ", 점수: " << pair.second << std::endl;
}
// 5. 대괄호 [] 연산자의 주의점
// 존재하지 않는 키를 []로 접근하면, 기본값(0)으로 자동 생성해버림
std::cout << "\nDavid 점수 확인: " << scores["David"] << std::endl; // David가 0으로 생성됨
std::cout << "현재 크기: " << scores.size() << std::endl; // 크기가 1 늘어남
return 0;
}
Hash 에 대하여
- Hash 함수와 Hash 값
-
- Hash Function
- 임의의 데이터를 특정한 타입의 ‘값’으로 반환하는 함수
- 기초적으론 나눗셈이나 곱셈을 기반으로 버킷 위치를 정하거나
속도와 ‘충돌 방지’를 위한 것(MurMurHash, FNV 등)
아니면 역추적을 방지위한 해시 함수 등(MD5, SHA 시리즈)
매우 다양한 종류의 해시 함수가 존재
- Hash Function
-
- Hash Value
- 해시 함수에서 내놓은 결과물
- Hash Value
-
-
- Hash Collision
- 서로 다른 ‘데이터’를 ‘해시 함수’에 집어넣었는데
그 결과값(해시 값)이 같은 현상
- 함수는 ‘다른 입력’으로 ‘같은 결과’를 충분히 내놓을 수 있음
(모든 입력에 대하여 결과가 1:1로 매핑되는 것이 아니기에)
- Hash Collision
- 해시 충돌의 해결법들
-
- Chaining
- 데이터를 집어넣는 ‘같은 위치’에
여러 값을 집어넣을 수 있게 하는 방식
- 연결 리스트를 통해 값들을 이어 붙인 방식
- 충돌이 너무 잦을 시, 접근이 ‘연결 리스트’ 탐색과 비슷하게 됨
- Chaining
-
- Open Addressing(개방 주소법)
- 충돌이 나면 ‘다른 빈칸’을 찾아
그 위치에 값을 집어넣는 방식
- ‘다른 빈칸’을 정하는 방식이 다양함
+1, 제곱, 나누기 등등
- Open Addressing(개방 주소법)
-
unordered_map vs TMap(Unreal)?
-
- 해시 충돌 처리 기법의 차이
- unordered_map은 ‘체이닝’
TMap은 ‘개방 주소법’을 사용 (속도를 위한 ‘배열 기반’)
- 배열을 기반으로 하기에
‘캐시 히트율’이 높아짐
- 해시 충돌 처리 기법의 차이
- 또한 TMap은 Unreal 최적화가 되어 있음
- Reflection
- GC
- Reflection
정리 표
| 비교 항목 | std::unordered_map (C++ STL) | TMap (Unreal Engine) |
|---|---|---|
| 기반 자료구조 | 해시 테이블 (Hash Table) | 해시 테이블 (Hash Table) |
| 충돌 해결 (Collision) | 체이닝 (Chaining) (연결 리스트 사용) | 개방 주소법 (Open Addressing) (선형 조사 등) |
| 메모리 구조 | 노드 기반 (비연속적, 파편화 발생) | 배열(TArray) 기반 (연속적, 메모리 친화적) |
| 캐시 효율 (Cache) | 낮음 (포인터 점프 발생, Cache Miss 증가) | 높음 (데이터가 모여있어 Cache Hit 유리) |
| 데이터 순회 속도 | 상대적으로 느림 | 매우 빠름 (인덱스 접근) |
| 메모리 할당 횟수 | 데이터 추가 시마다 발생 (new 호출 잦음) |
배열 크기가 찰 때만 재할당 (상대적으로 적음) |
| 언리얼 GC 연동 | 불가능 (UObject* 보호 못함, 댕글링 포인터 위험) |
가능 (UPROPERTY 지정 시 객체 참조 관리) |
| 직렬화/리플렉션 | 별도 변환 필요 | 언리얼 시스템 완벽 지원 |
| 삭제 비용 | O(1) (단순 링크 해제) | 상대적으로 복잡 (구멍(Hole) 관리 및 데이터 이동 가능성) |
댓글남기기