5,330만 삼각형이 118 드로우 콜로 접힌다
프레임타임 전체만 재면 “빨라졌다” 는 알아도 어디가 빨라졌는지는 모릅니다.
그래서 벤치 하니스에 ProfilerRecorder 를 붙여서 CPU 메인 스레드, GPU,
시스템별 마커, 드로우 콜을 같이 누적하게 만들었습니다.
시뮬레이션이 프로파일에서 사라졌다
| 적 수 | avg_ms | main_ms | SpatialHash_ms | Movement_ms | brg_draws | 삼각형 |
|---|---|---|---|---|---|---|
| 1,000 | 6.45 | 2.54 | 0.023 | 0.014 | 22 | 0.72M |
| 2,500 | 6.37 | 2.48 | 0.023 | 0.014 | 44 | 1.42M |
| 5,000 | 6.80 | 2.74 | 0.023 | 0.014 | 69 | 2.59M |
| 7,500 | 6.56 | 2.64 | 0.023 | 0.014 | 94 | 3.89M |
| 10,000 | 6.77 | 2.92 | 0.023 | 0.014 | 118 | 5.33M |
읽을 것이 셋 있습니다.
하나. 시스템 마커가 바닥에 붙어 N 에 안 비례합니다. 공간 해시 0.023ms, 이동 0.014ms 로 적이 열 배가 돼도 고정입니다. 그리드와 Burst 를 쓰면 실연산이 나노초 수준이라, 이 마커는 사실상 잡을 띄우는 고정 오버헤드를 재고 있습니다.
지루한 표처럼 보이는데 그게 결과입니다. 최적화가 너무 잘 돼서 시뮬레이션이 프로파일에서 사라졌습니다.
둘. main_ms 가 전체의 절반이 안 됩니다. 2.9ms 대 6.77ms 입니다.
나머지 3.8ms 는 렌더와 GPU 대기입니다.
이제 병목은 시뮬레이션이 아니라 렌더 파이프라인입니다.
셋. brg_draws 만 선형으로 늡니다. 22에서 118 입니다.
그리고 그 118개가 삼각형 5,330만 개를 그립니다.
그 118개가 무엇인가
Entities Graphics 는 BatchRendererGroup 위에 얹혀 있습니다.
같은 메시와 머티리얼을 쓰는 엔티티를 배치로 묶고, 인스턴스별 데이터는
GPU 버퍼에 올려 두고, 드로우 콜 하나로 수천 개를 그립니다.
MeshRenderer 1만 개였다면 최소 1만 번의 렌더러 순회와 컬링이 CPU 에서 일어납니다.
BRG 는 그 목록을 GPU 쪽 버퍼로 들고 있으므로 CPU 가 할 일이 그만큼 없습니다.
드로우 콜이 118로 접히는 것보다, 그 118을 만드는 데 CPU 를 거의 안 쓴다는 쪽이 큽니다.
GPU Resident Drawer 는 같은 뿌리다
Unity 6 의 URP 에는 GPU Resident Drawer 가 있습니다. 일반 GameObject 를 같은 BRG 경로로 그려 주는 기능 입니다. ECS 를 안 써도 BRG 의 이득을 가져올 수 있게 만든 것입니다.
다만 그냥 켜면 되는 물건이 아닙니다.
- CPU 를 벌고 GPU 를 조금 더 씁니다. 이미 GPU 바운드면 프레임이 오히려 늘 수 있습니다. 저사양 모바일과 VR 에서 이 경향이 강합니다
- 합쳐 그리는 만큼 오버드로우가 늘어납니다. 문서가 Forward+ 에서 Depth Priming Mode 를 Auto 나 Forced 로 두라고 권합니다
- 호환 조건이 있습니다. 정적 배칭을 끄고, Fixed Lightmap Size 를 켜고, 라이팅의 Mipmap Limits 를 끄는 식입니다
그리고 에디터보다 빌드에서 이득이 큽니다. 씬 뷰와 게임 뷰는 조건이 달라서, 에디터에서 재고 “효과 없다” 고 판단하면 틀릴 수 있습니다.
프로파일러 카운터에서 두 번 데였다
이 표를 만들면서 걸린 것 둘입니다. 둘 다 가짜 숫자가 나왔습니다.
하나. 카운터 이름은 문서가 아니라 런타임에서 확인해야 합니다.
ProfilerCategory 상수만으로는 원하는 카운터를 못 잡았습니다.
ProfilerRecorderHandle.GetAvailable() 로 8,062개를 전부 훑어서
BRG Draw Calls Count, CPU Main Thread Frame Time,
그리고 각 시스템의 Default World Simulation.Systems.<Name> 마커를 찾았습니다.
같은 시스템 마커가 Scripts 와 Burst Jobs 두 카테고리에 걸쳐 있어서 둘 다 합산했습니다.
Burst 를 껐다 켜도 총비용이 일관되게 잡히게 하려는 것입니다.
둘. 병렬 잡 마커는 SumAllSamplesInFrame 이 없으면 고정값이 나옵니다.
처음에 MovementSystem 이 2,000마리든 10,000마리든 똑같이 0.008ms 였습니다.
당연히 이상합니다.
병렬 잡은 워커 스레드마다 마커가 여러 번 발생하는데,
ProfilerRecorder 기본값은 그중 하나만 잡습니다.
워커 한 명분을 보고 있었던 것입니다.
ProfilerRecorder.StartNew(category, name, capacity,
ProfilerRecorderOptions.SumAllSamplesInFrame);
이 옵션을 켜야 프레임 안의 전 샘플이 합산돼서 시스템 총비용이 나옵니다.
N 에 안 비례하는 숫자가 나오면 최적화를 의심하기 전에 계측을 의심해야 합니다. 위의 표에서 마커가 고정인 것은 진짜였지만, 처음 본 고정값은 가짜였습니다. 둘을 구분하려면 계측 자체를 한 번 검증해야 했습니다.
gpu_ms 는 에디터에서 0이다
표의 GPU 열이 전부 0에 가깝습니다. 정상입니다. 에디터에서는 GPU 프레임 타임 카운터가 제대로 안 잡힙니다.
GPU 실측은 빌드에서 따로 잡아야 합니다.
그래서 “병목이 렌더로 옮겨갔다” 까지는 main_ms 와 total 의 차이로 말할 수 있지만,
그 안에서 무엇이 비싼지는 이 데이터로 말할 수 없습니다.