5,330만 삼각형이 118 드로우 콜로 접힌다
Esc

    5,330만 삼각형이 118 드로우 콜로 접힌다

    –UnityDOTS최적화렌더링

    프레임타임 전체만 재면 “빨라졌다” 는 알아도 어디가 빨라졌는지는 모릅니다. 그래서 벤치 하니스에 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 의 차이로 말할 수 있지만, 그 안에서 무엇이 비싼지는 이 데이터로 말할 수 없습니다.