Araina’s Blog

Apply object-oriented principles: Profile code to identify issues 본문

Unity Learn 번역/Pathway: Junior Programmer

Apply object-oriented principles: Profile code to identify issues

Araina 2022. 2. 26. 14:52

출처

 

Profile code to identify issues - Unity Learn

In this tutorial, you’ll learn how to use the Profiler to analyze a scene and identify where optimization bottlenecks are occurring. By the end of this tutorial, you will be able to: Deduce the script method that uses the most CPU time (vs. a script meth

learn.unity.com


 

 

1. 서언

여러분의 프로젝트를 마무리하고 퍼블리싱을 준비할 때, 리펙토링 도중이나 리펙토링 후에 코드에 남아 있는 병목 현상들을 식별하고 해결하는 것이 중요합니다. 유니티의 프로파일링 도구는 애플리케이션의 프레임을 떨어트릴 수 있는 찾기 힘든 문제점들을 찾아내는 데 적합합니다. 프로파일러는 어떤 스크립트가 호출되었는지, 어떻게 메모리가 할당되었는지, 어떻게 화면에 시각적 개체들이 렌더링 되는지 등을 포함하여 프레임 단위로 애플리케이션 내에서 어떤 일이 벌어지는지에 대한 자세한 보고서를 생성해줍니다. 프로파일러를 사용하면 여러분의 프로젝트 성능에 영향을 미치는 모든 요소들을 확인하실 수 있습니다.

 

여러분의 프로젝트 성능은 주로 프로젝트를 실행하는 컴퓨터에 따라 달라지므로, 여러분의 프로젝트를 특정 타깃 디바이스에서 프로파일링 하는 것이 중요합니다. 또는 그 타깃 디바이스가 데스크톱처럼 지원하는 하드웨어 범위로 설정될 경우, 다양한 성능을 지닌 여러 디바이스에서 프로파일링을 진행해야 합니다. 만약 여러분의 프로젝트를 고사양 신형 게이밍 PC에서 구동할 경우, 매우 높은 프레임 수치를 얻으실 수 있을 것입니다. 하지만 5년이나 된 구식 내장형 그래픽 노트북에서는 전혀 다른 경험이 되겠죠.

 

 프로파일러를 처음 사용하시는 여러분들을 위해 일반적인 최적화 이슈들을 쉽게 살펴보실 수 있도록 최적화를 엉망으로 해둔 씬을 하나 준비했습니다. 이를 염두에 두시고, 이번 튜토리얼에서 볼 수 있는 수치들은 여러분의 컴퓨터에서 보이는 수치와 정확히 일치하지 않을 수 있습니다. 그러나, 여러분의 프레임 수치와 상관없이 스크립트를 최적화할 때 유의미한 결과를 보실 수 있으실 겁니다.

 


 

2. 씬 살펴보기

이제 프로파일러로 검사해볼 수 있는 성능 이슈 샘플 프로젝트를 살펴봅시다. 이전에 언급했듯이, 프로젝트의 전용 최적화 폴더 안에 있는 이번 튜토리얼을 위해 생성된 커스텀 씬을 사용할 것입니다.

1. 프로젝트에서 Optimization 폴더로 가신 뒤, Scenes 폴더에서 Optimization 씬을 열어주세요.

2. 씬을 처음 실행하시면, 회색 Plane 오브젝트만 덩그러니 놓인 것을 보실 수 있으실 겁니다. Hierarchy 창을 보시면, OptimManager 스크립트가 부착된 Manager 게임 오브젝트도 보이실 겁니다.

3. Play 버튼을 눌러 씬을 살펴보세요.

4. Play 모드에서 수천 대의 지게차가 Plane 오브젝트 위에 나타나며, 모두 설정된 경계면 안에서 주변을 돌아다니며 빙빙 회전할 것입니다. Stats 탭을 열어 컴퓨터의 현재 초당 프레임 수(FPS)를 기록해주세요.

5. Play 모드에서 나오셔서 Inspector 창에서 OptimManager 스크립트를 살펴보세요. 이 스크립트는 지게차 생성 역할을 담당하는 스크립트입니다. 지게차 프리팹의 변수, 인스턴스 개수 및 경계 사이즈에 유의해주세요.

6. Hierarchy 창에서 Manager 오브젝트를 선택하신 뒤, Optim Manager 컴포넌트에서 인스턴스 수를 200으로 변경해주세요. 다시 Play 모드에 들어가셔서 FPS가 어떻게 바뀌었는지 관찰해보세요.

씬에서 지게차 수를 줄이면 전체 FPS 수치가 증가합니다. 지금과 같이 애플리케이션 내에서 프레임 수치가 다른 것보다 가장 중요한 상황일 경우, 이 방안이 유효한 최적화 방법일 수도 있습니다. 하지만, 만약 애플리케이션이 지게차 2000대와 60 프레임을 동시에 요구한다면 어떻게 해야 할까요?

이러한 요구사항들을 염두에 두고, 중요한 것은 정확하게 어디서 병목 현상이 발생하는 지를 알아내는 것입니다. 첫 번째 예제에서 단순하게 10배 더 많은 메시(mesh)가 스크린에 있었기 때문에 프레임이 떨어졌던 걸까요? 아니면 지게차들이 모두 움직이고 있어서 그랬던 걸까요? 그것도 아니라면 또 다른 이유가 있는 걸까요? 이 질문에 대한 답을 찾으면, 우리가 정확이 무엇을 해결해야 하는지를 알 수 있을 것입니다.

프로파일링은 여러분에게 발생하는 모든 일들에 대한 스냅숏과 각각의 작업들이 정확히 얼마나 오래 걸리는지에 대한 정보를 제공하므로, 여러분은 어떻게 해야 프레임 수치를 증가시킬 수 있는지에 대해 정확한 결정을 내릴 수 있습니다.

 


 

3. 프로파일링 데이터 모으기

이제 유니티가 씬에서 처리하는 것이 무엇인지에 대해 자세히 살펴봅시다.

1. OptimManager의 인스턴스 수를 2000으로 되돌려주세요.

2. Window > Analysis > Profiler를 선택해 프로파일러를 실행해주세요.

3. Profiler 창을 Project 창 옆에 배치해주세요.

4. 현재 프로파일러는 씬에 대해 수집된 데이터가 아직 아무것도 없기 때문에 텅 비어있는 상태입니다. 여러분이 Play 모드에 진입하시면, 그때 프로파일러가 작업을 시작할 것입니다. Profiler 창에 있는 레코드 버튼(빨간색 점 모양)을 활성화한 뒤, Play 버튼을 눌러주세요.

5. 이제 프로파일러가 색상으로 구분된 성능 데이터 차트를 기록하기 시작할 것입니다. 1~2초가 지나면 Play 모드에서 나와주세요. 이제 시간이 흐르는 동안 애플리케이션 내에서 발생한 모든 요소들의 상세한 개요가 만들어졌습니다.

Profiler 창 상단의 절반은 CPU 사용량을 시작으로 여러 모듈로 나눠져 있습니다. 여기에는 튜토리얼 프로젝트 내에서 우리가 사용한 모듈들만 표시됩니다.

모듈에는 다양한 카테고리의 작업 차트와 어떻게 이 모듈들이 CPU를 사용하는지에 대한 정보가 담겨 있습니다. 모듈 좌측면에는 해당 카테고리를 나타내는 색상 키가 있습니다. 스파이크(Spikes)는 렌더링처럼 작업 카테고리들 중 하나가 특정 프레임에서 더 많은 작업 시간을 소모하는 경우를 나타내 줍니다.

파란색 배경은 그냥 배경화면 색상이 아니라, 실행되고 있는 스크립트들을 나타내 주는 것입니다! 이처럼 매우 높은 수준에서도 씬 자체를 렌더링 하는 데 사용되는 처리 자원보다 스크립트가 더 많은 처리 자원을 차지하기 때문에, 스크립트에서 문제가 발생할 확률이 매우 높다는 것을 알 수 있습니다. 이제 곧 스크립트를 검사해볼 것입니다.

 


 

4. 밀리초 예산 설정

Profiler 창 아래쪽 절반은 각각의 프래임에서 정확히 무슨 일이 벌어졌는지를 시각적으로 분석한 내용을 보여줍니다. 만약 여러분이 Profiler 창 아래쪽 절반에 막대그래프 시각화가 보이지 않는다면, Profiler Module 섹션 바로 아래에 있는 Timeline을 선택해주세요.

CPU 사용량 그래프의 흰색 세로선은 애플리케이션에서 실행된 1 프레임을 나타냅니다. 해당 세로선을 좌 클릭하시고 CPU 사용량 모듈 내에서 드래그하여 프레임 간에 어떻게 사용량이 변화하는지 확인해주세요. CPU 사용량이 급증하는 구간의 프레임을 선택해주세요.

위의 예시를 보면, CPU: 117.26ms (빨간색 네모로 강조 표시된 부분)라는 표기를 확인하실 수 있습니다. 이는 해당 프레임 내에서 모든 작업을 완료하는 데 걸린 전체 시간을 밀리초 단위로 나타내 줍니다.

프로파일링을 할 때, 최종 프레임 수치 목표를 달성하는 데 핵심이 되는 이 밀리초 작업 완료 속도에 특히 집중하고자 합니다. 목표 프레임 속도에 따라 프레임 당 특정 밀리초 자원이 존재합니다. 이를 계산하는 공식은 다음과 같습니다:

1000 ms / 초당 목표 프레임 = ms 자원

따라서, 목표로 삼은 60 FPS을 달성하려면, 매 프레임마다 16밀리 초 분량의 자원이 존재하는 것입니다. 이는 현재 프로파일링 중인 프레임 당 밀리초 자원의 7배 이상 정도 되는 수치입니다!

 


 

5. 프로파일러 타임라인(Profiler Timeline) 살펴보기

프로파일러 타임라인 상단에는 PlayerLoopEditorLoop라는 이름의 2개의 교대 레이블이 있습니다. 만약 이 항목이 표시되지 않는다면, 스크롤 휠이나 트랙패드를 사용해 확대해야 할 수도 있습니다. PlayerLoop는 게임 자체에서 실행되는 모든 것을 나타내는 것에 비하여 EditorLoop는 에디터 상에서 애플리케이션이 구동될 때 발생하는 모든 것을 나타냅니다. 지금과 같은 경우에는 최종 사용자가 유니티 에디터 상에서 애플리케이션을 사용하지는 않을 것이므로, EditorLoop는 무시하시고, PlayerLoop에서 무슨 일이 일어나는지에 집중하시는 게 좋습니다. 

PlayerLoop 아래에 나열된 막대들은 프레임 동안 애플리케이션 내에서 벌어진 모든 일들을 시간 길이에 따른 내림차순으로 나타냅니다. 프로파일러 타임라인은 CPU 사용량 그래프와 일치하도록 색상이 조정됩니다. 큰 파란색 막대는 해당 프레임 안에서 특정 스크립트 관련 작업이 많은 시간을 소모하고 있음을 나타내 줍니다.

막대를 클릭하여 해당 작업의 출처가 OptimUnit 스크립트, 그중에서 Update 메서드임을 확인해주세요. 작업을 완료하는 데 총 98.27ms가 소요되며, 이는 우리에게 주어진 밀리초 자원을 여러 번 초과하는 수치입니다. 또한 해당 프레임 내에서 2000개의 스크립트 인스턴스가 존재하고 있음을 기억해주세요. 이는 우리가 2000개의 지게차를 OptimManager 스크립트 내부에서 생성했다는 점을 알고 있기에 중요한 사항입니다. 어쨌든, 이 두 가지 요소는 필히 서로 관련이 있습니다.

왜 스크립트가 2000번이나 실행되는 걸까요? 이는 각각의 지게차들이 하나의 인스턴스이기 때문입니다. 만약 OptimUnit 프리팹을 OptimManager 상에서 선택한다면, OptimUnit 스크립트가 해당 프리팹에 부착되어 있음을 알 수 있습니다. 이는 즉, 2000개의 스크립트 복사본이 실행되고 있다는 것입니다.

이제 프로파일러를 사용하여 프로젝트를 느리게 만드는 정확한 코드를 구별해낼 수 있게 되었습니다.

 


 

6. 프로파일러 샘플 메서드 추가하기

OptimUnit 스크립트의 Update 메서드에는 많은 일들이 일어나고 있으므로, 프로파일러에서 도움을 좀 더 받도록 하겠습니다. Profiler.BeginSample과 Profiler.EndSample 메서드를 추가하면 코드의 특정 부분을 프로파일링 할 수 있습니다.

1. Optimization 폴더에서 Scripts 폴더를 찾으신 뒤, OptimUnit 스크립트를 열어주세요.

Update 메서드는 다음과 같이 핸들링 시간, 지게차 프리팹 회전, 지게차 프리팹 이동, Manager 경계 확인이라는 4가지 개별 작업들을 처리합니다. 이제 프로파일러가 추적할 수 있도록 해당 작업들에 플래그를 지정해주도록 하겠습니다.

2. BeginSample 메서드와 EndSample 메서드를 Update 메서드 상단에 있는 HandleTime 메서드 바로 위, 아래에 추가해주세요:

Profiler.BeginSample("Handling Time"); // begin profiling a piece of code with a custom label
HandleTime();
Profiler.EndSample(); // ends the current profiling sample

Profiler.BeginSample이 사용자 정의 태그 이름을 매개변수로 받고 있음을 기억해주세요. 이 레이블은 프로파일러에 "OptimUnit.Update"와 동일한 방식으로 표시될 것입니다.

3. Profiler.EndSample 메서드 바로 뒤에, 동일한 방법으로 Rotating 매개변수와 함께 BeginSample을 추가해주세요:

Profiler.BeginSample("Rotating"); // begin profiling

var t = transform;

if (transform.position.x <= 0)
    transform.Rotate(currentAngularVelocity * Time.deltaTime, 0, 0);
else if (transform.position.x > 0)
    transform.Rotate(-currentAngularVelocity * Time.deltaTime, 0, 0);

if (transform.position.z >= 0)
    transform.Rotate(0, 0, currentAngularVelocity * Time.deltaTime);
else if (transform.position.z < 0)
    transform.Rotate(0, 0, -currentAngularVelocity * Time.deltaTime);

Profiler.EndSample(); // end profiling

4. 다시 EndSample 마지막 부분에 방금 전과 동일한 방법으로 Moving 태그와 함께 BeginSample을 추가해주세요:

Profiler.BeginSample("Moving"); // begin profiling
        
Move();
        
Profiler.EndSample(); // end profiling

5. 마지막으로 이전 EndSample 아래에 BeginSample을 하나 더 추가합니다. 이번에는 Boundary Check 태그를 기입해주세요:

Profiler.BeginSample("Boundary Check"); // begin profiling

//check if we are moving away from the zone and invert velocity if this is the case
if (transform.position.x > areaSize.x && currentVelocity.x > 0)
{
    currentVelocity.x *= -1;
    PickNewVelocityChangeTime(); //we pick a new change time as we changed velocity
}
else if (transform.position.x < -areaSize.x && currentVelocity.x < 0)
{
    currentVelocity.x *= -1;
    PickNewVelocityChangeTime();
}
        
if (transform.position.z > areaSize.z && currentVelocity.z > 0)
{
    currentVelocity.z *= -1;
    PickNewVelocityChangeTime(); //we pick a new change time as we changed velocity
}
else if (transform.position.z < -areaSize.z && currentVelocity.z < 0)
{
    currentVelocity.z *= -1;
    PickNewVelocityChangeTime();
}

Profiler.EndSample(); // end profiling

6. 스크립트를 저장하고 에디터로 돌아와 주세요.

7. Profiler 창에서 Clear 버튼을 눌러 현재 캡처된 데이터를 모두 지워주세요.

8. Play 버튼을 눌러서 애플리케이션을 몇 초 간 실행하여 새로운 데이터를 캡처하신 뒤, Play 모드를 종료해주세요.

9. Profiler 창의 CPU Usage 모듈에서 CPU 사용량이 급증한 다른 프레임을 선택해주세요.

10. 플래그를 지정한 코드 섹션을 살펴보겠습니다. OptimUnit.Update 막대 바로 아래에 새로운 스크립팅 막대가 생겼습니다. 이 막대는 새로운 샘플 코드 전체를 나타냅니다. 해당 막대를 선택하시면, 새로운 샘플 라벨들 중에 하나가 나타날 것입니다.

11. Timeline 드롭다운 메뉴를 클릭하시고, Hierarchy를 클릭해주세요.

12. 프레임 데이터는 4개의 코드 섹션에 대한 레이블이 자동으로 선택된 리스트 보기로 변경될 것입니다.

이 보기 화면을 보면, Time ms 열을 통해 Moving 메서드에 문제가 있다는 것이 바로 명확하게 드러납니다! 이제 문제가 식별되었으므로, 코드에서 문제를 해결해볼 차례입니다.

 


 

7. 코드 최적화

1. OptimUnit 스크립트로 돌아가서 Update 메서드에서 호출되는 Move 메서드를 찾아주세요. Ctrl(윈도우) 또는 Cmd(맥) 키를 누른 상태로 Move 글씨를 좌 클릭하여 메서드가 있는 곳으로 바로 이동할 수도 있습니다.

void Move()
{
    Vector3 position = transform.position;

    float distanceToCenter = Vector3.Distance(Vector3.zero, position);
    float speed = 0.5f + distanceToCenter / areaSize.magnitude;

    int steps = Random.Range(1000, 2000);
    float increment = Time.deltaTime / steps;
    for (int i = 0; i < steps; ++i)
    {
        position += currentVelocity * increment * speed;
    }

    transform.position = position;

    transform.position = transform.position + currentVelocity * Time.deltaTime;
}

Move 메서드는 여러 변수들을 계산하며, for 반복문을 1000~2000번 반복하여 지게차의 새로운 위치를 계산합니다. 여기에 2000대의 지게차를 곱해야 하고(그러면 2~4백만 개의 반복문이군요!), 이 메서드가 프레임 당 1번 실행된다는 것을 고려해본다면, 왜 이 메서드가 이렇게나 느린 지 바로 감이 오실 겁니다. 이러한 연산들은 Boundary Check 코드 내에서 처리되는 currentVelocity와 중복되므로, 이 메서드가 단순히 지게차 이동에만 관여하도록 만드는 것은 간단합니다.

2. Move 메서드의 몸체 코드를 아래와 같이 바꿔주세요.

void Move()
{
    transform.position = transform.position + currentVelocity * Time.deltaTime;
}

3. 스크립트를 저장하시고, 에디터로 돌아와 주세요.

4. 프로파일러 데이터를 지워주시고, Play 모드에서 다시 한번 데이터를 캡처해주세요.

프로파일링 결과가 극적으로 개선되었습니다! 이전에는 Move 코드를 처리하는 데 99.74ms가 소요되었습니다. 최적화를 진행한 이후에는 고각 1.08ms가 소요되었네요. 전체 CPU ms 수치도 목표 수치였던 16ms 미만인 14.94까지 떨어졌습니다!

Game 뷰에서 Stats를 확인해보면, FPS 수치가 67까지 올랐습니다!

 


 

8. 정리

이번에 사용한 예제 프로젝트는 극단적인 예시였지만, 어떻게 간단한 스크립팅 실수가 때때로 애플리케이션의 성능에 지대한 영향을 미칠 수 있는 지를 보여줍니다. 상업적인 목적으로 개발되는 애플리케이션의 경우, 많은 스크립트들 사이에서 매우 다양한 병목 현상을 발견할 수 있겠지만, 프로세스는 항상 동일하게 유지됩니다. 프로파일러를 사용하여 문제가 있는 스크립트들을 알아내고, 프로파일러 샘플 메서드들을 사용해 문제가 될 수 있다고 의심되는 코드 블록을 분리하세요. 문제가 발생하는 특정 코드가 식별되었다면, 이를 리펙토링 하시고, 다시 프로파일링을 진행하는 과정을 모든 문제가 해결될 때까지 반복하세요. 이제 여러분은 프로젝트 안에서 동작하는 비효율적인 코드를 식별해낼 수 있는 도구를 손에 넣으신 겁니다.

 


 


수고하셨습니다!


Comments