원문 : https://community.arm.com/developer/tools-software/graphics/b/blog/posts/mali-performance-1-checking-the-pipeline

주의 : 번역이 개판이므로 이상하면 원문을 참고하세요.

주의 : 허락받고 번역한 것이 아니므로 언제든 내려갈 수 있습니다.


이 시리즈에서 처음 몇 개의 블로그 동안, ARM Mali "Midgard" GPU 패밀리가 사용하는 고수준 렌더링 모델에 대해서 설명해 왔습니다. 이 시리즈들의 나머지 부분에서는 ARM 의 시스템 레벨 프로우파일링 툴인 DS-5 Streamline 을 사용하는 방법에 대해서 설명하고, 애플리케이션이 Mali 기반 시스템의 외부에서 최적의 성능을 획득하지 못하는 영역을 식별할 것입니다.

이 블로그에서, 매크로-스케일( macro-scale ) 파이프라이닝과 관련한 이슈들에 대한 디버깅, 항상 GPU 를 바쁘게 만드는 수단, 그리고 프레임 레벨 파이프라인이 멈추는 일반적인 원인에 대해서 살펴 볼 것입니다. 만약 이 시리즈가 처음이라면 첫 번째 블로그를 읽을 것을 권장합니다. 왜햐나면 그것은 여기에서 더 세부적으로 다루게 될 개념에 대해서 소개하고 있기 때문입니다.

노트( note ): 여러분이 이미 DS-5 Streamline 을 가지고 있고 실행중이라고 가정합니다. 만약 아직 실행하지 않았다면, 다양한 Mali 기반 소비자 디바이스에서 설정하는 법에 대한 몇 개의 가이드가 있습니다:

이 블로그의 예제들은 DS-5 v5.16 을 사용해서 수집되었습니다.

What does good content look like?

성능 문제를 분석하기 전에, 우리의 목표와 스트림라인에서 이것이 어떻게 보이는지를 이해하는 것이 좋습니다. 시스템의 성능과 칸텐트의 복잡도에 기반한 두 가지 "좋은" 행위( behavior )들이 존재합니다.

  • GPU 가 병목( bottleneck )인 곳의 칸텐트를 위한 행위.
  • 수직동기화( vsync )가 병목인 곳의 칸텐트를 위한 행위.

이 실험을 위해 필요한 카운터들은 다음과 같습니다:

  • Mali Job Manager Cycles: GPU cycles
    • 이 카운터는 GPU 가 뭔가를 수행하는 클락 사이클에 증가합니다.
  • Mali Job Manager Cycles: JS0 cycles
    • 이 카운터는 GPU 가 프래그먼트 셰이딩을 하는 클락 사이클에 증가합니다.
  • Mali Job Manager Cycles: JS1 cycles
    • 이 카운터는 GPU 가 버텍스 셰이딩을 하거나 타일링을 하는 사이클에 증가합니다.

The GPU is the bottleneck

만약 GPU 가 병목인 곳( 예를 들어 렌더링이 60 FPS 를 달성하기에는 너무 복잡함 )의 칸텐트를 위한 프레임 레벨 렌더링 파이프라인을 생성하고 유지하는 데 성공했다면, 우리는 GPU 워크로드( workload ) 유형( 버텍스나 프래그먼트 처리 )들인 작업들이 항상 전체 능력( capacity )을 이용해 실행되고 있기를 기대하게 됩니다.

거의 대부분의 칸텐트에서 프래그먼트 처리는 GPU 실행에 있어서 가장 지배적인 부분입니다; 애플리케이션들은 보통 버텍스다 한자리 혹은 두자리 수가 더 많은 프래그먼트를 가지고 있습니다. 그러므로 이 시나리오에서는 JS0 이 항상 활성화되어 있고 CPU 와 JS1 은 모든 프레임에서 적어도 일정 시간만큼 유휴상태가 될 것이라 예상할 수 있습니다.

스트림라인을 사용하여 이 카운터 집합들을 수집할 때, 세 가지 활동( activity ) 그래프들을 보게 될 것인데요, 이것들은 GPU 를 위한 날( raw ) 카운터 값들과 함께 툴에 의해 자동으로 생성됩니다. "GPU 프래그먼트" 처리가 완전하게 로드되어 있고, "CPU Activity" 와 "GPU Vertex-Tiling-Compute" 워크로드들은 각 프레임에서 일정시간 동안 유휴상태가 되는 것을 확인할 수 있습니다. 노트 -- 이를 보기 위해서는 1 ms 나 5 ms 의 줌( zoom ) 레벨에 가깝게 확대할 필요가 있습니다 -- 여기에서는 매우 짧은 기간에 대해서 이야기하고 있는 것입니다.

The vsync signal is the bottleneck

vsync 에 의해 조절( throttle )되는 시스템에서는 CPU 와 GPU 가 매 프레임 유휴상태가 되는 것을 기대할 수 있습니다. 왜냐하면 vsync 신호가 발생하고 윈도우 버퍼 교체( swap )이 발생하기 전까지는 다음 프레임을 렌더링하지 않기 때문입니다. 아래 그래프는 스트림 라인에서 이것이 어떻게 보이는지를 보여 줍니다:

만약 여러분이 애플리케이션 개발자가 아니라 플랫폼 통합자( integrator )라면, 60 FPS 로 실행하는 테스트 케이스들은 여러분의 시스템의 DVFS 주파수 선택의 효율성을 리뷰하는 데 있어 좋은 방법이 될 것입니다. 위의 예제에서는 활동들이 나타나는 위치 사이에 많은 시간 간격이 존재합니다. 이는 선택된 DVFS 주파수가 너무 높고 그 GPU 는 필요 이상으로 빠르게 실행중이라는 것을 의미합니다. 이는 플랫폼의 에너지 효율성을 전체적으로 감소시킵니다.

Content issue #1: Limited by vsync but not hitting 60 FPS

더블 버퍼링 시스템에서는 60 FPS 를 달성하지 못하지만 vsync 에는 여전히 제한되는 칸텐트가 존재할 수 있습니다. 이 칸텐트는 위의 그래프와 매우 유사해 보이는데요, 워크로드 사이의 시간 간격이 한 프레임의 길이의 몇 배가 될 것이며, 프레임율( frame rate )은 정확히 최대 스크린의 리프레시율( refresh rate )로 정확히 나눠질 것입니다( 예를 들어 60 PFS 패널은 30 PFS, 20 FPS, 15 FPS 등으로 실행될 수 있습니다 ).

60 FPS 로 실행중인 더블 버퍼링 시스템에서, GPU 는 vsync 버퍼 교체를 위한 시간 안쪽에서 성공적으로 프레임들을 생성하고 있습니다. 아래 그림에서는 두 개의 프레임버퍼( FB0, FB1 )들의 생명주기( lifetime )을 확인할 수 있습니다. 녹색으로 되어 있는 것은 on-screen 의 기간이며 파란색으로 되어 있는 것은 GPU 에 의해 렌더링되고 있는 기간입니다.

GPU 가 이를 수행할 수 있을 만큼 충분히 빠르게 실행되고 있지 않다면, 하나 이상의 vsync 데드라인( deadline )을 놓치게 될 것입니다. 그래서 현재의 프론트 버퍼( front-buffer ) 가 다른 vsync 기간 동안에 스크린상에 남아 있게 됩니다. 아래 다이어그램에서 오렌지 라인이 그어져 있는 곳에서는, 프론트 버퍼가 여전히 스크린에 렌덜이되고 있으며 백 버퍼( back-buffer )는 디스플레이를 위해 큐에 들어 가 있습니다. GPU 는 렌더링할 수 있는 버퍼를 가지고 있지 않기 때문에 대기상태에 빠집니다. GPU 가 45 FPS 이상으로 칸텐트를 실행할 수 있을만큼 충분히 빠름에도 불구하고, 성능은 30 FPS 로 떨어집니다.

안드로이드 윈도우 시스템은 보통 트리플 버퍼링( tripple buffering )을 사용합니다. 그래서 이 문제를 피합니다. 왜냐하면 GPU 가 렌더링하는 데 사용할 버퍼를 가지고 있기 때문입니다. 하지만 여전히 X11 기반 Mali 배포폰에서는 더블 버퍼링을 사용합니다. 만약 이런 이슈가 발생했다면, 성능 최적화를 수행하는 동안에는 vsync 를 끄는 것을 권장합니다; 발생하는 문제를 혼란스럽게 만드는 부차적인 요소들이 없는 상태에서 어떤 것을 최적화해야 할지 결정하는 것이 훨씬 쉽습니다.

Content issue #2: API Calls Break the Pipeline

여러분이 보게 될 두 번째 이슈는 파이프라인 휴식( break )입니다. 이 시나리오에서는 적어도 하나 이상의 CPU 처리 부분이나 GPU 처리 부분이 항상 바쁘지만 동시에 바쁘지는 않습니다; 일종의 직렬화( serialization ) 지점의 형식이 발생한 것입니다.

아래의 예제에서는, 칸텐트가 프래그먼트 중심( dominant )이며, 그래서 프래그먼트 처리가 항상 활성화되어 있을 것이라 기대하고 있지만, 활동이 진동하고 있음을 볼 수 있습니다. GPU 버텍스 처리와 프래그먼트 처리가 직렬화되고 있습니다.

이것의 가장 일반적인 원인은 API 의 동기화 행위를 강제하는 OpenGL ES API 함수를 사용하는 것이고, 이는 드라이버가 모든 대기중( pending )인 연산들을 플러싱( flushing )하게 강제하고, API 요구사항을 존중하기 위해서 렌더링 파이프라인을 비워( drain )버립니다. 가장 일반적인 범인들이 여기에 있습니다:

  • glFinish(): 명시적으로 파이프라인 비우기를 요구합니다.
  • glReadPixels(): 묵시적으로 현재 서피스를 위해 파이프라인 비우기를 요구합니다.
  • glMapBufferRange(): GL_MAP_UNSYNCHRONIZED_BIT 가 설정되지 않았다면; 명시적으로 매핑되고 있는 데이터 리소스를 사용하고 있는 모든 대기중인 서피스들을 위해 파이프라인 비우기를 요구합니다.

이런 API 호출들은 파이프라인 비우기 때문에 빠르게 만들어 지는 것이 거의 불가능합니다. 그러므로 이런 함수들은 가능한 한 피할 것을 제안합니다. OpenGL ES 3.0 이 glReadPixels 함수가 픽셀 복사를 비동기적으로 수행할 수 있는 Pixel Buffer Object( PBO )를 타깃으로 한다는 것은 언급할 가지가 있습니다. 이것은 더 이상 파이프라인 플러싱을 발생시키지 않습니다. 하지만 여러분의 데이터가 도착할 때까지 잠시동안 기다려야만 하며, 메모리 전송은 여전히 상대적으로 비쌉니다.

Content issue #3: NOT GPU limited at all

오늘의 마지막 이슈는 GPU 가 전혀 병목이 아닌 사황입니다. 하지만 종종 형편없는 그래픽스 성능을 보여줄 때가 있습니다.

CPU 가 GPU 가 소비하는 것보다 더 빠르게 새로운 프레임을 생성할 수 있다면, 우리는 그냥 프레임 파이프라인을 유지하기만 할 수 있습니다. 만약 CPU 가 렌더링하는데 5 ms 를 소비하는 프레임을 20 ms 에 만들었다면, 파이프라인은 각 프레임에서 비어 있는 상태로 실행될 것입니다. 아래의 예제에서 GPU 는 매 프레임 유휴상태입니다. 하지만 CPU 는 항상 실행중이고, 이는 CPU 가 GPU 를 따라가지 못한다는 것을 내포하고 있습니다.

"꽉 잡아! CPU 가 단지 25 % 만 로드되었어". 스트림라인은 시스템의 전체 용량을 100% 로 보여 줍니다. 만약 여러분이 4 개의 CPU 코어를 가지고 있다면, 여러분의 시스템에서 단일 프로세서는 하나의 스레드가 최대이므로, 그것은 20 % 로드로 보여질 것입니다. 만약 "CPU Activity" 그래프의 타이틀 박스의 우상단에 있는 화살표를 클릭한다면, 그것은 확장될 것이고 여러분에게 시스템의 CPU 코어 당 개별적인 로드 그래픽스를 제공할 것입니다.

예상했던 대로, 한 코어가 100 % 로드를 사용하고 있습니다. 그러므로 이 스레드는 시스템에서 병목이 되는데요, 이는 전체 성능을 제한하게 됩니다. 이런 문제가 발생하는 여러 가지 원인이 있을 수 있는데요, 그래픽스 동작의 관점에서는 애플리케이션 비효율성보다는 두 가지 주요 원인이 있습니다:

  • glDraw...() 호출이 과도하게 많음.
  • 동적 데이터 업로드가 과도하게 많음.

모든 드로 호출( draw call )은 드라이버에 대한 비용을 가집니다. 컨트롤 구조체들을 만들고 그것들을 GPU 에 제출해야 합니다. 프레임 당 드로 호출의 개수는 유사한 렌더 스테이트( render state )를 가진 오브젝트의 드로잉을 배칭함으로써 최소화되어야 합니다. 배치를 크게 할 것인지 컬링을 고려할 것인지에 대한 균형 문제가 있기는 합니다. 타깃에 따라 다르기도 합니다: 모바일에서 대부분의 고사양 3D 칸텐트는 렌더 타깃 당 대략 100 개의 드로 호출을 사용하며, 많은 2D 게임들은 20-30 개의 드로 호출을 사용합니다( 역주 : 2014 년 이야기입니다 ).

동적 데이터 업로드의 관점에서, 클라이언트 메모리에서 그래픽스 서버로 업로드되는 모든 데이터 버퍼는 드라이버가 그 데이터들을 클라이언트 버퍼에서 서버 버퍼로 복제하는 것을 요구한다는 점에 주의하시기 바랍니다. 만약 서브 버퍼 업데이트보다는 새로운 리소스를 사용한다면, 드라이버가 그 버퍼를 위한 메모리를 할당해야만 합니다. 가장 일반적인 나쁜 짓은 클라이언트측 버텍스 애트리뷰트를 사용하는 것입니다. 정적인 Vertex Buffer Objects( VBOs )를 사용해 그래픽스 메모리에 영구적으로 저장할 수 있으며, 그 버퍼를 다른 일련의 렌더링을 위해서 참조로 사용할 수도 있습니다. 이는 업로드 비용을 한 번만 지불하게 해 주며 여러 렌더링 프레임 동안 그 비용을 분할( amortize )합니다.

Mali 그래픽스 스택이 성능을 전혀 제한하지 않는 경우가 존재합니다. 애플리케이션 로직 자체가 16.6 ms 이상 걸리는 경우에는 OpenGL ES 호출이 무한하게 빠르더라도 60 FPS 를 달성할 수 없습니다. DS-5 스트림라인은 매우 능력있는 소프트웨어 프로우파일러를 포함하고 있으며, 그것은 코드 내에서 병목이 어느 부분인지를 정확하게 식별할 수 있도록 도움을 줄 수 있습니다. 또한 다중 스레드를 사용해서 소프트웨어를 병령화하기 원한다면, 시스템의 다중 CPU 코어 사이에서 균형있는 워크로드를 로드할 수 있도록 도움을 줄 수 있습니다. 하지만 이것은 Mali 의 동작과 직접적으로 연관된 것은 아니기 때문에 여기에서는 다루지 않겠습니다.

Next Time ...

다음 시간에는 Mali 드라이버의 렌더 타깃 유지를 위한 접근법에 대해서 리뷰하겠습니다. 그리고 Frame Buffer Objects( FBOs )를 사용해 이 모델을 멋지게 실행하기 위해서 애플리케이션을 구조화하는 방법에 대해서 리뷰하겠습니다.

댓글과 의견을 환영합니다.

Cheers,

Pete.

+ Recent posts