Godot 4.7 렌더링 아키텍처 CPU 병목 현상의 이론적 및 기술적 분석
Godot

Godot 4.7 렌더링 아키텍처 CPU 병목 현상의 이론적 및 기술적 분석

저자GameDev Base
날짜2026년 10월 28일
21분 읽기

Godot 4.7 렌더링 아키텍처: CPU 병목 현상의 이론적 및 기술적 분석

일반 아키텍처 프레임워크 및 CPU-GPU 동기화 지점

Godot 게임 엔진의 현대적인 렌더링 아키텍처는 특히 3D 및 고급 2D 그래픽을 지원하는 버전에서 플랫폼 독립성과 저수준 그래픽 API(Vulkan, Direct3D 12, Metal)에 대한 접근성을 제공하기 위해 복잡한 계층 구조를 기반으로 합니다. 이 아키텍처의 초석은 모든 현대적인 렌더러가 사용하는 RenderingDevice라는 추상화 계층입니다. RenderingDevice는 서로 다른 그래픽 백엔드의 API를 공통 인터페이스로 통합하여 동일한 코드 베이스로 여러 플랫폼에 컴파일할 수 있도록 합니다. 이 계층은 RenderingServer가 관리하는 고수준 시각적 자산(메시, 재질, 조명)을 GPU로 효율적으로 전송할 수 있게 하는 명령 목록(draw_list, compute_list, raytracing_list)과 같은 핵심 GPU 리소스 및 작업을 관리합니다. 이러한 아키텍처 접근 방식은 코드 중복을 줄이고 새로운 백엔드 통합을 용이하게 하는 것을 목표로 하지만, 동시에 각 API 호출 뒤에 숨겨진 상당한 CPU 부하를 동반합니다. 각 렌더 명령, 셰이더 변경 또는 상태 변경은 GPU가 현재 구성을 검증하기 위해 상당한 양의 CPU 작업을 필요로 하며, 이는 렌더링 최적화의 핵심 목표인 비용이 많이 드는 API 호출을 최소화하고 유사한 객체를 단일 작업으로 렌더링하기 위한 "배칭(batching)" 기술 사용을 강제합니다.

CPU와 GPU 간의 동기화는 렌더링 프로세스가 원활하게 작동하기 위해 매우 중요하지만, 동시에 잠재적인 CPU 병목 현상의 주요 원인이기도 합니다. Godot은 동기식 및 비동기식 동기화 메커니즘을 모두 지원합니다. RenderingDevice 클래스는 CPU를 대기시키는 명시적인 동기화 지점을 제공합니다. 예를 들어, sync() 메서드는 호출하는 스레드를 GPU가 모든 작업을 완료할 때까지 차단하며, 이는 성능 측면에서 비용이 많이 듭니다. 마찬가지로, force_sync() 메서드 또한 강제적인 CPU-GPU 동기화 지점을 생성하여 성능 저하를 유발할 수 있습니다. 반면 비동기 작업은, 예를 들어 GPU에서 텍스처를 읽기 위해 사용되는 texture_get_data_async와 같은 함수는 CPU가 525 7 533 1619 533 21

GPU가 작업을 마칠 때까지 기다리며 유휴 시간을 보내는 것을 방지합니다. 그러나 이러한 작업도 지연(frame delay)이 발생하며, 이 지연은 rendering/rendering_device/vsync/frame_queue_size 프로젝트 설정으로 결정되는 값만큼 CPU 시간을 소비할 수 있습니다. 또한, 각 texture_get_data_async 호출은 약 4ms의 CPU 시간을 소요할 수 있으며, 다수의 요청이 동시에 시작될 경우 전체 CPU 부하가 증가하여 오히려 성능 저하를 초래할 수 있습니다. 이는 비동기 프로그래밍에서 신중한 균형을 잡는 것이 중요함을 보여줍니다.

가장 주목할 만한 발견 중 하나는 Vulkan 백엔드에 존재하는 동기화 로직에 근본적인 결함이 있다는 점입니다. 언급된 자료에 따르면, RenderingDeviceVulkan::swap_buffers 함수가 올바른 순서를 따르지 않아, 새로운 프레임이 준비될 때 GPU가 이전 프레임의 리소스를 해제하기를 기다리지 않음으로써 잠재적인 "use-after-free" 오류와 DEVICE_LOST와 같은 심각한 충돌 상황을 초래할 수 있음이 밝혀졌습니다. 이 결함은 단순한 성능 문제가 아니라 엔진의 안정성과 신뢰성을 위협할 수 있는 아키텍처적 결함으로 부각됩니다. 결론적으로, RenderingDevice 계층이 플랫폼 독립성을 제공하더라도 그를 통과하는 모든 명령은 CPU 부하이며, 동기화 로직의 오류와 비동기 API의 사용은 CPU 성능을 결정짓는 섬세한 균형 게임입니다.

동기화 메서드 / 지점

설명

잠재적 CPU 병목 현상

RenderingDevice.submit() (로컬 장치용)

기본 렌더 스레드 외에서 작동하는 워커 스레드로부터 명령 목록을 GPU로 전송합니다.

스레드 동기화 및 명령 전송 비용.

RenderingDevice.sync() (로컬 장치용)

기본 렌더 스레드 외에서 작동하는 워커 스레드의 완료를 기다립니다.

GPU가 작업을 마칠 때까지 CPU가 차단되어 발생하는 CPU 시간 손실.

RenderingDevice.force_sync()

강제적인 CPU-GPU 동기화 지점을 트리거합니다.

CPU가 GPU의 현재 작업을 완료할 때까지 대기(stalling). 비용이 매우 높음.

RenderingDevice.texture_get_data_async()

GPU의 텍스처 데이터를 비동기적으로 CPU로 가져옵니다.

지연 시간, GPU가 데이터를 준비할 때까지 CPU가 활성화되지 않음.

RenderingDevice.buffer_get_data_async()

GPU의 버퍼 데이터를 비동기적으로 CPU로 가져옵니다.

데이터가 올 때까지 CPU가 대기하는 시간.

Vulkan 백엔드 동기화 오류

swap_buffers가 올바른 순서로 대기하지 않고 새 프레임을 시작할 수 있습니다.

GPU가 아직 완료하지 않은 리소스를 해제하려 시도하여 발생하는 "use-after-free" 위험 및 DEVICE_LOST.

아키텍처의 또 다른 중요한 계층은 RenderingServer입니다. 이 서버는 노드 기반의 씬 트리와 독립적으로 작동하며 모든 시각적 콘텐츠의 생성 및 관리에 사용됩니다. RenderingServer의 API는 RID(Resource ID)라고 하는 32 33 32 4 533 21 32 33 5 4 821

고유 식별자를 기반으로 합니다. 이 ID들은 실제 메모리 참조 대신 불투명 포인터이며, 리소스의 생성, 업데이트 및 삭제 작업을 가능하게 합니다. 이러한 설계는 씬 트리를 거치지 않고 RenderingServer를 직접 사용하는 개발자들이 트리 시스템의 비용(노드 수명 주기 알림, 변환 전파 등)에서 벗어나 더 효율적인 시스템으로 작업할 수 있게 합니다. 예를 들어, instance_set_transform()으로 RID 기반 객체의 변환을 업데이트하는 것은 Node3D.set_transform()으로 동일한 작업을 수행하는 것보다 훨씬 빠릅니다. 이러한 구분은 CPU 성능을 극대화하려는 애플리케이션에 중요한 도구를 제공합니다.

RenderingServer는 또한 call_on_render_thread(Callable)와 같은 메커니즘을 통해 메인 애플리케이션 스레드에서 안전하게 호출될 수 있도록 합니다. 이는 메인 스레드가 렌더링 작업을 위해 할당된 스레드로 작업을 보낼 수 있게 하지만, 이 전송 작업 또한 동기화 지점이며 CPU 시간을 소비합니다. 따라서 RenderingServer가 더 효율적인 경로를 제공하는 동시에, 이를 사용하기 위한 메커니즘 자체가 CPU 비용을 수반한다는 점을 기억해야 합니다. CPU 시간 측정은 get_frame_setup_time_cpu()와 같은 함수로 수행할 수 있으며, 이는 프레임 준비에 소요된 총 CPU 시간을 제공하므로 RenderingServer 내부 작업의 전반적인 영향을 이해하는 데 가치 있는 지표입니다. 결론적으로, Godot의 렌더링 아키텍처는 플랫폼 독립성과 유연성을 제공하는 만큼, CPU-GPU 동기화에서 정밀함이 요구되고 잠재적인 오류가 존재하며 올바른 최적화 전략이 필요한 복잡한 구조입니다.

2D 렌더링 프로세스의 CPU 병목 현상 및 최적화 트레이드오프

Godot의 2D 렌더링 프로세스는 일반적으로 더 단순한 지오메트리 세트와 더 적은 수의 재질로 작업하기 때문에 3D 렌더링과 비교하여 다른 성능 역학을 보여줍니다. 2D 게임은 특히 수천 개의 스프라이트가 동시에 화면에 나타나는 탑다운 또는 사이드 스크롤 방식의 프로젝트에서 CPU 측면에서 병목 현상이 발생하는 경향이 있습니다. 이는 2D 렌더링 프로세스의 가장 근본적인 CPU 병목 현상이 수백 또는 수천 개의 개별 객체에 대해 개별적으로 GPU Draw Call을 보내는 것임을 보여줍니다. 각 Draw Call은 API 인터페이스(OpenGL, Vulkan 등)를 통과할 때 GPU의 현재 상태(활성 셰이더, 재질, 정점 형식 등)를 검증하기 위해 상당한 양의 CPU 작업을 필요로 합니다. 이러한 검증 작업은 API 뒤에서 실행되는 백그라운드 오버헤드로 간주되며, 이 비용은 객체당 발생하는 상태 변경(state switching)으로 인해 기하급수적으로 증가합니다. 따라서 2D 렌더링 최적화의 핵심 목표는 이러한 비용이 많이 드는 721 22 22 8 8 11 1619 19

API 호출을 최소화하고 유사한 객체를 단일 작업으로 렌더링하기 위해 "배칭(batching)" 기술을 사용하는 것입니다. Godot은 이 문제에 대한 해결책으로 Use Batching 기능을 제공합니다. 이 기능은 유사한 재질을 가진 스프라이트, Polygon2D 또는 CollisionPolygon2D와 같은 다른 2D 요소를 결합하여 단일 정점 버퍼로 모아서 작동합니다. 이를 통해 많은 개별 스프라이트를 단일 Draw Call로 렌더링할 수 있으며, 결과적으로 Draw Call 수와 그에 따른 API 검증 부하가 크게 감소합니다. 이 작업은 매 프레임마다 스프라이트의 변환을 가져오고, 정점 데이터를 준비하며, 단일 버퍼에 배치해야 하므로 즉각적인 CPU 비용이 발생합니다. 그러나 이 즉각적인 CPU 비용은 훨씬 적은 Draw Call을 수행함으로써 얻는 이득보다 훨씬 낮으며, 일반적으로 전체적인 CPU 성능을 향상시킵니다.

그러나 배칭 기술 자체도 CPU 측면에서 새로운 잠재적 병목 현상을 일으킬 위험이 있습니다. 특히 "버퍼 오퍼닝(buffer orphaning)"이라고 불리는 방법이 사용됩니다. 이 방법에서는 동일한 정점 버퍼를 한 프레임 내에서 수백 번 재사용할 수 있습니다. 이는 동적 콘텐츠가 있는 씬에서 메모리 할당/할당 해제 비용을 줄이는 효과적인 방법입니다. 그러나 이 기술은 동적 버퍼 관리에 대한 OpenGL 표준의 불확실성으로 인해 플랫폼과 그래픽 드라이버에 따라 일관되지 않은 결과를 초래할 수 있습니다. 일부 드라이버는 이러한 지속적인 재사용을 최적화하지 못해 GPU 드라이버 스톨(stall)을 유발할 수 있으며, 이는 성능을 저하시킬 수 있습니다. 이는 최적화가 플랫폼별로 다르게 동작할 수 있으며 각 프로젝트에 대해 구체적인 테스트가 수행되어야 함을 강조합니다.

예를 들어, 한 사용자 보고서에 따르면 Intel UHD 620 통합 GPU를 사용하는 Windows 운영 체제에서 배칭이 성능을 60 FPS에서 45 FPS로 떨어뜨렸지만, 동일한 하드웨어의 Linux에서는 부정적인 영향이 전혀 없었습니다. 이는 2D 배칭이 강력한 최적화 도구이기는 하지만, 개발자가 이 기능의 영향과 잠재적 위험을 신중하게 평가해야 함을 보여줍니다. 배칭의 CPU 비용은 수만 개의 항목이 처리되지 않는 한 일반적으로 작게 유지되지만, 병목 현상은 대개 정점 버퍼 업로드 단계에서 그래픽 드라이버 수준으로 이동합니다.

가장 눈에 띄고 개발자가 쉽게 간과할 수 있는 2D CPU 병목 현상의 예는 Polygon2D 클래스에서 애니메이션 도중 매 프레임마다 메시를 다시 생성하는 것입니다. 이 상황을 "메시 처닝(mesh churn)"이라고 하며 막대한 CPU 비용을 발생시킬 수 있습니다. 한 테스트 시나리오에서 AnimationPlayer로 애니메이션되는 Polygon2D 객체에서 매 프레임마다 기존 메시를 해제(free)하고 새로운 정점 데이터와 함께 다시 할당(생성)하는 작업은 15.7밀리초의 CPU 비용을 발생시켰으며, FPS를 28에서 83으로 올린 최적화 이전에는 35ms/frame의 프레임 시간을 초래했습니다. 이는 성능을 최적화하는 것이 때로는 직관적이지 않지만, 근본적인 데이터 구조 조작에 집중해야 하기 때문에 매우 중요하다는 것을 보여주는 사례입니다.(116 1 2 1 11 1 1 1 1 1 1130)

최적화가 기존 정점 데이터를 업데이트하는 대신 새로운 메시를 생성하는 방식으로 수행되었을 때, 성능이 200%를 초과하는 향상을 보였습니다. 이는 데이터 구조가 관리되는 방식이 CPU 성능에 미치는 영향이 얼마나 결정적인지를 증명합니다. 캐시 친화적인 데이터 구조와 가비지를 생성하지 않는 데이터 업데이트는 특히 2D 렌더링에서 CPU 병목 현상을 방지하기 위한 기본 원칙입니다.

배칭은 메시 처닝과 같은 근본적인 구조적 오류를 해결할 수 없습니다. 배칭은 이미 준비된 정점 데이터를 그룹화하는 것을 목표로 하지만, 메시 처닝은 매 프레임마다 이 데이터가 생성되는 방식을 잘못 처리하고 있기 때문입니다. 결론적으로, 2D 렌더링 최적화는 Draw Call 수를 줄이는 것을 목표로 하는 트레이드오프입니다. 배칭은 이 트레이드오프의 가장 일반적인 도구이지만, 플랫폼별 비용과 위험이 따릅니다. 개발자가 직면하는 가장 큰 CPU 위험은 데이터 구조를 잘못 관리하여 "메시 처닝"과 같은 확실한 병목 현상을 만드는 것입니다. 이러한 오류는 일반적으로 직관적이지 않고 복잡해 보이는 최적화 대신, 기본적인 데이터 조작 관행에 집중함으로써 예방해야 합니다.

최적화 기술

목적

CPU 비용 (즉각적)

CPU 비용 (장기적)

위험 및 플랫폼 종속성

Draw Call 수 감소

API 인터페이스 검증 및 상태 변경 비용 절감.

낮음 (일반적으로 무시할 수 있음)

매우 높음 (성능 향상)

정보 없음

2D 배칭

유사한 재질을 가진 스프라이트를 단일 정점 버퍼로 모아 단일 Draw Call로 렌더링.

중간 (정점 데이터 준비, 수집)

매우 높음 (Draw Call 수 대폭 감소)

GPU 드라이버에 따른 성능 변동, 버퍼 오퍼닝으로 인한 드라이버 스톨.

스프라이트 아틀라스

여러 스프라이트를 하나의 큰 텍스처(아틀라스)에 배치.

낮음 (아틀라스 좌표 계산)

높음 (재질 변경 횟수를 줄여 Draw Call 수 감소).

아틀라스 크기 제한, 아틀라스 관리의 복잡성.

메시 처닝 (나쁜 관행)

애니메이션 또는 동적 변경으로 인해 매 프레임마다 메시를 해제하고 다시 생성.

매우 높음 (정점 배열의 지속적인 해제/할당)

부정적 (막대한 CPU 비용, FPS 저하)

씬의 2D 객체에 애니메이션을 적용할 경우 발생 가능.

3D 렌더링 과정에서의 CPU 병목 현상: 컬링, 물리 및 변환 관리

3D 렌더링 과정은 2D에 비해 훨씬 복잡한 CPU 집약적 작업 세트를 포함하며, 병목 현상도 이러한 복잡성에서 비롯됩니다. 이러한 병목 현상은 일반적으로 세 가지 주요 범주로 나눌 수 있습니다: 보이지 않는 객체 필터링(컬링), 물리 시뮬레이션 및 동기화, 그리고 장면 내 객체와 조명 관리입니다. 컬링은 보이지 않는 객체가 GPU로 전송되는 것을 방지하여 성능을 크게 향상시키는 과정입니다.

그러나 이러한 컬링 작업은 특히 3D 장면에 수십만 개의 객체가 포함된 경우 상당한 CPU 부하를 발생시킬 수 있습니다. 컬링 작업은 일반적으로 CPU에 의해 수행됩니다. 첫째, 카메라의 시야(Frustum) 밖에 있는 객체들이 필터링됩니다. 이 작업은 일반적으로 매우 빠르지만, 대규모 장면에서는 수십만 개의 객체에 적용하는 데 비용이 많이 듭니다. 더 정교한 기술인 오클루전 컬링(Occlusion Culling)은 가장 효과적이지만 가장 비용이 많이 드는 컬링 기술입니다.

Godot는 Embree라는 소프트웨어 레이트레이싱 라이브러리를 사용하여 CPU에서 이 작업을 수행합니다. 엔진은 OccluderInstance3D 노드로 표시된 오클루더(occluder) 메시를 CPU 측에서 저해상도 깊이(depth) 버퍼로 래스터화합니다. 그런 다음 이 버퍼를 확인하여 다른 객체(occludees)가 완전히 가려졌는지 확인합니다. 객체의 AABB(축 정렬 경계 상자)가 오클루더에 의해 완전히 가려지면 렌더링되지 않습니다. 이 작업은 특히 복잡한 오클루더 메시와 장면 내 움직이는 오클루더의 경우 매우 CPU 집약적인 작업입니다.

오클루더 베이킹(baking)과 움직이는 오클루더의 BVH(Bounding Volume Hierarchy) 구조를 매 프레임 업데이트하는 것은 상당한 CPU 부하를 발생시킵니다. 따라서 오클루전 컬링은 실내나 여러 방이 있는 환경에서 가장 효과적이며, 개방된 장면에서는 덜 유용합니다. 또한 오클루전 컬링의 이점은 사용하는 렌더러에 따라 다릅니다. Forward+ 렌더러는 오버드로우를 줄이기 위해 이미 깊이 프리패스(depth prepass)를 수행합니다. 따라서 오클루전 컬링이 추가적인 이점을 제공하긴 하지만, 깊이 프리패스를 수행하지 않는 모바일 렌더러에서 더 큰 성능 차이를 만들어냅니다. 11 14 1619 111 1 2 1130

두 번째 주요 CPU 병목 현상은 물리 시뮬레이션과 이를 장면 트리와 동기화하는 것으로, 특히 동적 장면에서 매우 중요합니다. Godot 4.6에서 Jolt Physics가 기본값이 된 것은 속도와 안정성 측면에서 큰 개선이었습니다. Jolt는 더 발전된 충돌 감지 알고리즘(GJK/EPA), 더 안정적인 제약 조건 솔버, 그리고 내장된 멀티스레딩 지원을 제공합니다.

이는 5,000개의 활성 물리 객체가 포함된 장면에서 4.7배 더 높은 FPS를 달성하게 했습니다. 그러나 Jolt가 가져온 성능 향상에도 불구하고, PhysicsServer와 장면 트리 사이에서 발생하는 상태 동기화(state sync) 메커니즘 자체가 병목 현상을 일으킬 수 있습니다. flush_queries라고 불리는 이 과정은 각 활성 물리 객체에 대해 콜백 함수(_sync_body_state)를 트리거하여 GDScript/C++ 측의 노드(예: RigidBody3D)의 변환 정보를 업데이트합니다.

이는 객체당 개별 Callable 호출로 인해, 특히 3,000개 이상의 객체가 포함된 대규모 장면에서 물리 단계 함수의 비용과 맞먹는 CPU 부하를 생성할 수 있습니다. (23 2428 24 29 2428 24 2428 12 12 15) 이 문제의 해결책은 개별 쿼리를 위한 API 대신, 모든 활성 객체의 변환, 속도 등을 단일 원시 데이터로 가져올 수 있는 "벌크 쿼리(bulk query)" 함수를 추가하는 것입니다. 이는 콜백 비용과 API 인터페이스의 복잡성을 제거하여 상당한 CPU 개선을 제공할 것입니다. 개발자는 body_set_state_sync_callback(Callable())을 사용하여 이 자동 상태 동기화를 비활성화하고 수동으로 제어할 수 있습니다.

세 번째 주요 CPU 병목 현상은 동적 장면에서 각 객체의 월드 변환을 GPU로 전송하고 각 객체를 조명하기 위해 필요한 램프와 매칭하는 것입니다. 동적 객체의 변환을 업데이트하는 것은 instance_set_transform()과 같은 RenderingServer 호출을 포함합니다. 이는 특히 6,500개 이상의 동적 메시가 포함된 장면에서 'Preparing to Render 3D scene' 단계의 CPU 사용량을 50%까지 줄일 수 있습니다.

이는 노드 시스템 하위의 RID 기반 시스템과 비교했을 때 더 효율적이어야 함을 의미합니다. 객체와 램프를 매칭하는(Object-Light Pairing) 작업도 CPU에서 수행됩니다. 각 객체가 장면 내의 모든 램프와 겹침(overlap) 및 가시성 마스크 검사를 수행하는 것은 특히 많은 램프와 동적 객체가 있는 장면에서 비용이 많이 드는 작업입니다. 이 계산은 최종적으로 렌더링되지 않더라도 CPU 시간을 소비합니다. 이러한 병목 현상을 극복하는 핵심은 MultiMeshInstance와 같은 더 효율적인 API를 사용하는 것입니다.

MultiMeshInstance는 수천 개의 동일한 메시를 서로 다른 변환으로 렌더링하는 작업을 훨씬 더 효율적으로 수행하며, 이를 통해 6,500개의 동적 메시를 단일 MMI로 표현하면 평면을 렌더링하는 것과 같은 성능을 얻을 수 있습니다. 이는 SceneTree를 거치지 않고 RenderingServer를 직접 사용하는 것이 제공하는 성능 향상과 일치합니다.

결론적으로, 3D CPU 병목 현상은 컬링, 물리, 동적 객체/조명 관리와 같은 더 광범위하고 복잡한 시스템으로 구성됩니다. 오클루전 컬링은 강력한 최적화이지만 CPU를 많이 사용합니다. 물리 및 변환 관리는 특히 flush_queries와 같은 API 제한으로 인해 잠재적인 병목 현상을 포함합니다. 이러한 병목 현상을 극복하는 핵심은 MultiMeshInstance와 같은 더 효율적인 API를 사용하고, 가능하다면 장면 트리 하위로 내려가 RenderingServer를 직접 사용하는 것입니다. 15 15 15 27 22 27 27 22 2227

병목 현상 범주

특정 프로세스

CPU 비용 원인

최적화 방법

컬링

가시 영역 컬링

객체의 AABB를 프러스텀과 비교.

간단하고 빠르며 대규모 장면에서도 효과적임.

컬링

오클루전 컬링

CPU에서 Embree를 사용한 래스터화, 오클루더 BVH 생성 및 업데이트.

가장 비용이 많이 드는 컬링 기술이며, 복잡한 오클루더 메시에서 성능 저하.

물리 및 동기화

물리 시뮬레이션 (Jolt)

Jolt의 알고리즘 복잡성 및 멀티스레드 관리.

일반적으로 효율적이지만 대규모 장면에서는 여전히 상당한 CPU 사용 가능.

물리 및 동기화

상태 동기화 (flush_queries)

각 활성 물리 객체에 대한 개별 Callable 콜백 비용.

벌크 쿼리 함수(제안), 자동 동기화 비활성화.

객체 및 조명 관리

변환 업데이트

각 동적 객체의 instance_set_transform()과 같은 서버 호출.

MultiMeshInstance 사용, RID 기반 RenderingServer 직접 사용.

객체 및 조명 관리

객체-조명 매칭

각 객체가 장면 내의 모든 램프와 겹침 및 마스크 검사를 수행.

램프 수 감소, 거리/그림자 투영 램프 제한, LOD 사용.

데이터 구조, 스크립트 성능 및 핵심 최적화 전략

Godot 엔진의 CPU 성능은 렌더링 파이프라인 내의 단계뿐만 아니라, 개발자가 씬 구조와 스크립트를 작성할 때 따르는 기본 원칙에도 깊이 연관되어 있습니다. CPU 성능의 기초는 알고리즘뿐만 아니라, 사용된 데이터 구조가 CPU 캐시(cache)에 얼마나 적합한지에 의해 결정됩니다. 무작위 메모리 액세스 패턴은 "캐시 미스(cache misses)"라고 알려진 상황을 초래하며, 이는 CPU가 느린 RAM에서 데이터를 가져오기를 기다리는 유휴 시간(idle time)을 발생시킵니다. 따라서 데이터를 메모리에 순차적이고 지역적으로 유지하는 것은 캐시 적중률을 높이고 결과적으로 성능을 크게 향상시킵니다.

Godot의 자체 서비스 API(예: RenderingServer)는 이러한 최적화를 기능적 계층에서 처리하지만, 개발자가 자신의 코드(특히 GDExtension이나 복잡한 GDScript 구조)에서 캐시에 적합한 데이터 구조를 선택하는 것은 매우 중요합니다. 가장 대표적인 2D CPU 병목 현상의 예는 Polygon2D 클래스에서 애니메이션 도중 매 프레임마다 메시를 다시 생성하는 것입니다. 이는 성능 최적화가 때로는 직관적이지 않더라도, 핵심 데이터 구조 조작에 집중해야 한다는 점을 보여주는 중요한 사례입니다. 최적화를 기존 정점 데이터를 업데이트하는 방식이 아닌 새로운 메시를 생성하는 방식으로 수행했을 때, 성능이 200%를 초과하는 향상을 보였습니다. 23 2428 12 15 2227 27 14 14 14 1130 11

이는 데이터 구조가 어떻게 관리되는지가 CPU 성능에 얼마나 결정적인 영향을 미치는지 증명합니다. 마찬가지로, 씬에 수많은 노드(수천에서 수만 개)가 존재할 때, SceneTree가 각 노드에 _process() 및 _physics_process() 업데이트 알림을 전파하는 것 또한 CPU 비용입니다. 이러한 노드를 씬 외부로 관리(Node.remove_child)하거나 일시 정지(set_process(false))함으로써 이 비용을 줄일 수 있습니다. 스크립트 언어 선택 또한 CPU 성능에 큰 영향을 미치는 요소입니다. GDScript는 속도 우선 언어라기보다 사용 편의성 우선 언어입니다. 이는 VM(가상 머신)을 통해 해석되며 직접 기계어로 컴파일되지 않음을 의미합니다.

이는 C# 및 특히 C++ 모듈(GDExtensions)에 비해 더 높은 CPU 비용을 의미합니다. C#은 .NET 기반이므로 JIT 컴파일 덕분에 좋은 성능을 제공하지만, GC(가비지 컬렉션) 간격 동안 대기 시간이 발생할 수 있습니다. C++는 가장 빠른 옵션이지만 더 복잡한 배포 과정이 필요합니다. 이러한 성능 차이는 특히 자주 호출되는 루프나 핵심 알고리즘에서 올바른 스크립트 언어 선택을 요구합니다. GDScript의 성능을 향상시키기 위해 오프라인 컴파일러 사용이 제안되었습니다. 이 접근 방식은 GDScript 코드를 내보내기(export) 시 C ABI와 통신할 수 있는 공유 라이브러리(예: .dll, .so)로 컴파일하여 VM의 추가 비용을 제거합니다. 그러나 이는 iOS, WebAssembly 및 콘솔 플랫폼과 같이 동적 코드 생성을 지원하지 않는 플랫폼에서는 필수적인 장애물입니다. 이는 스크립트 기반 게임 개발과 고성능을 요구하는 엔진 내부 코딩 사이의 근본적인 대조를 보여줍니다.

이러한 핵심 요소들을 고려할 때, 개발자가 CPU 성능을 개선하기 위해 따라야 할 기본 최적화 전략이 명확해집니다. 첫 번째는 RenderingServer와 PhysicsServer를 직접 사용하여 SceneTree의 비용을 피하는 것입니다. SceneTree가 각 노드에 대해 수행하는 하우스키핑(생명 주기 알림, 트랜스폼 전파 등)은 수천 개의 객체가 포함된 대규모 씬에서 상당한 CPU 부하를 발생시킬 수 있습니다. RenderingServer를 사용하여 RID 기반의 더 가벼운 에셋을 생성하면 이 비용을 크게 제거할 수 있습니다. 이 접근 방식은 특히 AI 캐릭터, 파티클 또는 기타 고밀도 객체에 이상적입니다.

두 번째 전략은 MultiMeshInstance와 같은 내장 GPU 가속 최적화를 극대화하는 것입니다. 수천 개의 동일한 모델(풀, 나무, 발사된 탄환 등)을 렌더링해야 할 때, MultiMeshInstance는 이러한 객체를 단일 드로우 콜(draw call)로 렌더링할 수 있게 하며, 이는 동적 씬에서의 트랜스폼 업데이트 및 객체-조명 페어링과 같은 CPU 병목 현상을 해결하는 가장 효과적인 방법 중 하나입니다. 세 번째 전략은 데이터 구조와 알고리즘을 캐시 친화적으로 만드는 것입니다. 정점 데이터를 지속적으로 다시 생성하지 않고, 데이터를 메모리에 선형으로 배치하며, 불필요한 메모리 할당/변환을 피함으로써 CPU의 유휴 시간을 줄일 수 있습니다. 14 14 14 14 14 14 17 17 17 1422 22 2327

마지막으로, 프로파일링은 이러한 최적화가 어디에서 이루어져야 하는지 결정하는 유일한 신뢰할 수 있는 방법입니다. Godot의 내장 프로파일러, 특히 Frame Time 내의 Rendering 섹션은 렌더링 관련 CPU 비용을 분석하는 데 사용할 수 있습니다. 그러나 이 프로파일러가 일부 서비스(물리, 오디오)의 대기 시간을 정확하게 측정하지 못한다는 보고가 있습니다. 더 심층적인 분석을 위해서는 Time.get_ticks_usec()과 같은 고해상도 타이머를 사용하여 자체 코드 블록의 시간을 측정하거나, Valgrind(Callgrind)와 같은 외부 C++ 프로파일러를 사용하여 CPU 핫스팟(hotspots)을 식별하는 것이 더 정확한 결과를 제공할 수 있습니다.

프로파일링은 "최적화하기 전에 측정하라"는 원칙의 가장 구체적인 적용이며, 개발자가 시간을 가장 효율적으로 사용하도록 돕습니다. 이러한 전략은 CPU 성능을 향상시키기 위한 예산 관리처럼 생각할 수 있습니다. 씬 트리의 비용을 절약하고, RenderingServer를 사용하여 더 효율적인 자원을 구매하며, 캐시 친화적인 코드를 작성하여 프로세서의 모든 클럭 사이클을 최적으로 활용하는 것입니다.

최적화 전략

목적

적용 분야

예상 CPU 이득

SceneTree 비용 회피

노드 생명 주기 알림, 트랜스폼 전파 및 기타 씬 트리 오버헤드 제거.

고밀도 객체 (AI, 파티클, 탄환).

씬 트리 비용 절감, 수천 개의 노드에서 특히 중요.

MultiMeshInstance 사용

수천 개의 동일한 메시를 서로 다른 트랜스폼으로 렌더링하도록 최적화.

풀, 나무, 발사된 탄환, 군중.

트랜스폼 업데이트 및 객체-조명 페어링 비용 제거, 드로우 콜 수 급격히 감소.

데이터 구조를 캐시 친화적으로 최적화

캐시 미스를 줄여 CPU 유휴 시간 최소화.

전체 애플리케이션, 특히 정점 조작, 알고리즘.

CPU 클럭 사이클의 효율적 활용, 전반적인 성능 향상.

스크립트 언어 최적화

VM 또는 GC와 같은 추가 계층으로 인한 CPU 비용 제거.

핵심 알고리즘, 자주 호출되는 함수.

C++ (GDExtension) > C# > GDScript 성능 순서.

프로파일링을 통한 집중

성능에 가장 큰 영향을 미치는 영역(핫스팟)을 식별하여 최적화 노력 집중.

전체 애플리케이션.

시간과 노력을 가장 큰 CPU 병목 현상에 집중, 불필요한 최적화 방지.

종합: CPU 성능을 결정하는 복잡한 상호작용과 미래 전망

이 심층 분석의 결과로, Godot 4.7의 CPU 측 렌더링 아키텍처는 다양한 요인의 복잡한 상호작용으로 구성된 동적 시스템으로 나타납니다. 11 14 10 14 1014 1422 2327 1114 1417 1014. 연구의 핵심 목표인 특정 CPU 병목 현상을 이해하고 일반적인 종합과 함께 평가할 때, 개발자가 엔진의 동작을 파악하는 데 도움이 될 몇 가지 핵심 결론을 도출할 수 있습니다. 첫째, CPU와 GPU 사이의 기본적인 균형이 렌더링 최적화의 중심에 있습니다.

2D 렌더링에서는 일반적으로 CPU가 프레임을 준비하는 데 필요한 시간(드로우 콜 준비, 컬링 등)이 GPU의 픽셀 셰이딩 시간보다 길기 때문에 CPU 병목 현상이 더 흔합니다. 이러한 맥락에서 2D 배칭(batching)과 같은 기술은 CPU 비용을 GPU 비용으로 전환하는 효과적인 거래입니다. 3D 렌더링에서는 상황이 더 복잡하며, CPU와 GPU 병목 현상이 번갈아 나타날 수 있습니다. CPU는 객체와 조명의 렌더링 여부를 결정하는 컬링 작업으로 바쁘고, GPU는 가시적인 폴리곤의 깊이와 조명을 처리하는 데 바쁩니다. 이러한 이중 관계는 모든 최적화가 다른 곳에 영향을 미친다는 것을 보여줍니다. 예를 들어, 오클루전 컬링(occlusion culling)은 CPU를 압박하여 GPU의 셰이딩 부하를 줄일 수 있지만, 이는 Polygon2D와 같은 상황에서 메시 갱신(mesh churn)과 같은 확실한 CPU 병목 현상을 초래할 수 있습니다. 둘째, 모든 최적화는 거래이며 개발자가 자신의 씬 요구 사항에 따라 올바른 균형을 찾는 것이 중요합니다. 2D 배칭은 CPU 비용을 GPU 비용으로 바꾸지만, 높은 LOD(Level of Detail)는 CPU를 압박하는 대신 GPU를 압박합니다. 투명 객체는 뒤에서 앞으로(back-to-front) 렌더링 순서가 필요하므로 불투명 객체처럼 재질별로 배칭할 수 없으며, 이는 본질적으로 더 많은 비용을 발생시킵니다.

이러한 거래는 프로젝트마다 다른 결과를 낳을 수 있으며, 최선의 전략은 일반적으로 씬의 콘텐츠, 대상 플랫폼의 용량, 사용자의 미적 요구 사항을 고려하여 실험적으로 결정되어야 함을 의미합니다. RenderingDevice 및 RenderingServer와 같은 추상화 계층은 개발자에게 더 많은 제어권과 성능을 제공하지만, 실제로는 또 다른 CPU 비용을 발생시킵니다. 이러한 계층은 SceneTree를 덮어쓰며, 더 효율적인 RID 기반 접근 방식으로의 전환 필요성과 이 접근 방식이 가져오는 복잡성(예: 월드 트랜스폼을 수동으로 관리)을 드러냅니다. 이는 추상화가 제공하는 편리함이 성능이 중요할 때 더 깊은 아키텍처 이해를 요구함을 보여줍니다.

셋째, CPU 성능은 알고리즘뿐만 아니라 사용된 데이터 구조의 기본 특성에도 의존합니다. Polygon2D 예시는 올바른 데이터 조작이 얼마나 중요한지 증명합니다. 데이터는 반복적으로 생성되는 대신 업데이트되어야 하며, 메모리에 순차적으로 정렬되어야 하고, CPU 캐시의 효율적인 사용을 보장해야 합니다. 이러한 원칙은 특히 GDExtension과 같은 C++ 기반의 고성능 코드를 작성할 때 매우 중요합니다. 또한 GDScript와 같은 스크립트 언어의 VM 기반 구조는 C++ 모듈에 비해 자연스러운 CPU 비용을 발생시키며, 이는 특히 성능이 중요한 알고리즘의 경우 GDExtension 사용을 강제할 수 있습니다. 이러한 기본적인 구조적 요소들은 렌더링 파이프라인 자체보다 먼저 또는 동시에 최적화되어야 합니다.

미래 전망을 살펴보면, 렌더링 기술의 발전은 CPU의 렌더링 파이프라인 내 역할을 변경하는 것을 목표로 합니다. GPU 기반 렌더링(GPU-driven rendering)과 같은 고급 최적화 연구는 CPU가 프레임을 준비하는 데 필요한 모든 디스패치(객체, 조명, 패스 등)를 GPU로 보내고, GPU가 작업을 완료할 때까지 기다리는 것을 목표로 합니다. 이 아키텍처는 CPU의 유휴 시간을 최소화하여 잠재적으로 큰 성능 향상을 제공할 수 있습니다. 그러나 이 접근 방식은 현재의 CPU 기반 투명 렌더링 패스와 같은 일부 작업을 CPU에서 계속 수행해야 하는 강제성을 만들며, 이는 아키텍처의 새로운 병목 현상을 형성할 수 있습니다. 이러한 아키텍처 변경은 대규모 재구조화와 기존 게임 흐름의 변경을 초래할 것입니다.

결론적으로, 제공된 자료에 비추어 볼 때 Godot의 CPU 측 렌더링 아키텍처는 플랫폼 독립성을 보장하기 위해 설계된 유연한 구조를 나타내지만, 동시에 동기화 오류, 플랫폼별 동작 및 복잡한 최적화 거래와 같은 상당한 양의 CPU 성능 위험을 내포하고 있습니다. 개발자는 이러한 아키텍처와 병목 현상을 이해함으로써 더 효율적이고 원활하며 고성능인 게임을 만들기 위한 올바른 전략을 따를 수 있습니다. 211 2327 1124 16 1619 522 22 1130 14 14

*정보 : 이미지는 Google Gemini에 의해 생성되었습니다.

Godot4.7GDScriptDrawCallJoltPhysicsOptimizationVulkan
딥 리서치 도구
Qwen (Alibaba Cloud)
Qwen (Alibaba Cloud)
번역 도구
Google Gemini
Google Gemini

공유