오늘 공부한 내용
1. 오클루전 컬링, 프러스텀 컬링
2. dynamic_cast와 static_cast의 차이 (다운캐스팅)
3. 컴파일 타임과 런타임
4. RTTI
5. shared_ptr, 순환참조
6. 뮤텍스와 세마포어
7. 디퍼드 렌더링의 처리순서와 장단
8. 언리얼 GC
9. LOD
10. atomic과 mutex
11. 데이터 레이스
12. 레이스 컨디션
13. 스레드 스케줄링
오클루전 컬링이란 시야안에 있지만 다른 불투명 객체에 완전히 가려진 객체를 렌더링 하지 않는 기법을 말함
프러스텀 컬링은 카메라 뒤쪽 뿐아니라 상하좌우 near/far범위를 포함한 시야밖의 객체를 렌더링하지 않는것
static_cast, dynamic_cast 모두 명시적 형변환
static_cast는 컴파일 타임에 형식상 가능한지만 검사하고, 다운캐스팅 대상이 실제로 그 파생 타입인지 런타임에 확인하지 않음, 잘못된 다운캐스팅은 오류가 아니라 정의되지 않은 동작이 될 수 있다.
dynamic_cast는 다형적 타입에서 RTTI를 이용해 런타임 타입 검사를 한다. 포인터 변환 실패 시 nullptr, 레퍼런스 변환 실패시 std::bad_cast예외를 던진다.
부모 타입의 포인터/레퍼런스를 자식 타입으로 변환하는것
Animal* animal = new Dog();
Dog* dog = dynamic_cast<Dog*>(animal)
Animal -> Dog가 다운캐스팅이다. 실제 객체가 정말 Dog일때만 안전하다.
업캐스팅도 있는데 (자식->부모) 이건 항상 안전해서 보통 암묵적으로 가능하다.
컴파일 타임은 어제 공부한대로 .cpp 코드가 기계어로 변환되는 과정, 이때 문법검사, 선언된 변수 타입, 함수 인자타입처럼 코드만 보고 판단 가능한 문제를 검사한다.
런타임은 컴파일된 프로그램이 실제 실행되는 시점이다. 사용자 입력, 파일 존재 여부, 실제 객체가 Dog인지 Cat인지 같은 실행 중 데이터에 따라 달라지는 것은 이때 알 수 있다.
Run-Time Type Information, 즉 런타임 타입 정보다. 다형적 클래스의 객체가 실행 중 실제로 어떤 타입인지 알아낼 수 있게하는 C++기능
dynamic_cast가 실제로 RTTI를 사용해 객체 타입을 검사한다.
설명 잘못한 부분이 있다. 몇개의 shared_ptr이 소유중인지를 센다고 하면 정확
struct Node{
std::shared_ptr<Node> neighbor;
}
int main(){
auto a = std::make_shared<Node>(); // 노드A의 레퍼런스카운트1
auto b = std::make_shared<Node>(); // 노드B의 레퍼런스카운트1
a->neighbor = b;
b->neighbor = a;
return 0;
}
뮤텍스는 한번에 하나의 스레드만 임계영역에 들어가게 하는 락이고, 락을 획득한 스레드가 해제한다.
세마포어 공유자원의 사용가능개수를 세는 카운터, 예를들어 세마포어값이 3이면 최대 3개의 스레드까지 동시에 진입할 수 있고, 0이면 다음 스레드는 대기한다.
디퍼드 렌더링은 물체정보와 조명 계산을 분리하는 렌더링 방식
장점 : 조명이 많아도 물체별로 모든 조명을 계산하지 않고 화면 픽셀 기준으로 처리하기 쉬워서 다수의 동적 광원에 유리
단점 : G-buffer때문에 메모리,메모리 대역폭 비용이 크고, 반투명 물체는 처리하기 어려워 별도 포워드 렌더링을 쓰는 경우가 많다.
UPROPERTY가 붙은 UObject 참조는 GC가 추적하는 강한 참조가 되고, ,GC는 루트 객체에서 시작해 이런 참조를 따라가며 도달가능한 Uobejct를 살아있는것으로 판단한다 그래서 UPROPERTY로 참조되는 객체는 GC가 수집하지 않는다 허나 raw UObject 멤버는 GC가 그 참조를 모를수 있어 대상 객체가 수집된 뒤 댕글링 포인터 위험이 있다.
거리에 따라 그릴 품질을 낮추는것, 가까운 객체는 고폴리곤 메시와 고해상도 텍스처를 쓰고 멀어질수록 저폴리곤 메시, 낮은 해상도의 텍스처 또는 단순한 머티리얼로 바꿈, 화면에 작게 보이는 먼 객체에 같은 비용을 쓰지않아 렌더링 성능을 높임
atomic은 정수 증가, 플래그 설정처럼 단일 값에 대해 단순하고 독립적인 연산을 여러 스레드가 안전하게 처리할때 씀
std::atomic<int> killCount = 0;
++killCount;
mutex는 여러 변수, 컨테이너를 함께 읽고 수정하는 코드 구간 전체를 보호할 때 씀
std::mutex mutex; // 미리 선언된 뮤텍스
{
std::lock_guard<std::mutex> lock(mutex);
// 여기부터 mutex 잠김
inventory.push_back(item);
gold -= item.price;
} // 여기서 lock 객체가 소멸하며 mutex 자동 해제
데이터 레이스 : 여러 스레드가 같은 메모리를 동시에 접근하고, 그중 하나 이상이 쓰기인데 동기화가 없는 경우
원인 : 동기화 도구 (Mutex, Atomic등)없이 같은 메모리를 동시에 읽고 쓰려고 할때 발생
결과 : 정의되지 않은 동작, 메모리 오염
두 스레드가 일반 int score에 동시에 score++를 하면 둘 다 같은 기존 값을 읽고 같은값으로 써서, 2가 증가해야할것이 1만증가할 수 있다. 이를 막기위해 atomic이나 mutex를 쓴다.
레이스 컨디션 : 경쟁상태, 여러 스레드의 실행순서에 따라 결과가 달라져 버그가 생기는 넓은 개념
원인 : 작업들의 실행 순서가 보장되어야 하는데, 쓰레드 스케줄링 등으로 인해 순서가 꼬일때 발생
if (gold >= 100){
gold -= 100;
BuyItem();
}
골드가 100일때 두 스레드가 동시에 실행하면
둘중 하나만 구매되어야 하는데 실행 순서때문에 둘다 구매되는 경우가 생김 이를 레이스 컨디션이라고 함, 이때도 뮤텍스나 atomic compare-and-swap등으로 해결
| 데이터 레이스는 메모리 접근 자체의 동기화 문제이고, 레이스 컨디션은 여러 단계 로직의 실행 순서 문제
지금 CPU를 어느 스레드에게 얼마나 실행시킬지를 결정하는 작업
CPU코어가 4개면 같은 순간에 실제로 실행되는 스레드는 최대 4개정도다. 실행할 스레드가 100개라면 운영체제가 빠르게 번갈아 CPU시간을 나눠준다. Ready, Running, Waiting/Blocking으로 나뉨
운영체제 스케줄러는 보통 우선순위와 공정성을 고려해 Ready 상태의 스레드중 하나를 고른다. 한 스레드가 너무 오래 CPU를 점유하면 일정 시간 단위인 타임 슬라이스가 끝난 뒤 다른 스레드에게 넘김 이를 선점형 스케줄링이라고 함
스레드를 바꿀때는 컨텍스트 스위칭이 발생한다. 운영체제는 기존 스레드의 레지스터 값, 실행 위치, 스택 관련 상태를 저장하고, 다음 스레드의 상태를 복원한다. 이 비용과 캐시 효율 저하 때문에 스레드를 과도하게 만들면 느려짐
프로세스 : 독립된 주소 공간과 자원을 가진 실행 중인 프로그램
스레드 : 그 프로세스 안에서 실제 코드를 실행하는 작업 단위
한 프로세스는 최소 한 개의 스레드를 가지며, 여러 스레드는 프로세스의 힙,전역변수,코드를 공유한다.
스레드마다 스택과 레지스터는 따로 가짐