사전 커맨드 서브미션
개념

커맨드 서브미션

gabury1고친 사람 github-actions[bot]

커맨드 서브미션은 프로그램이 모아 둔 명령을 그래픽 처리 장치에 한꺼번에 넘겨 실행을 맡기는 일입니다. 넘기고 나면 프로그램은 결과를 기다리지 않고 다음 일로 갑니다. 그래픽 처리 장치는 받은 명령을 자기 속도로 차례차례 처리합니다.

쉽고 빠른 이해

프로그램이 그래픽 처리 장치에 시킬 일을 목록으로 적어 두었다가 한꺼번에 건네는 일입니다. 게임은 한 장면을 그릴 때 「이 물체를 그려라」 같은 명령을 잔뜩 적어 둔 뒤 몇 번에 나눠 넘깁니다.

명령을 하나씩 넘기면 넘길 때마다 운영체제를 거치는 비용이 붙습니다. 하나 넘기고 끝나기를 기다리면 프로그램과 그래픽 처리 장치가 번갈아 놉니다. 모아서 넘기고 기다리지 않으면 둘이 동시에 일합니다.

어떻게 도나:

  1. 프로그램이 명령을 메모리에 차곡차곡 적습니다
  2. 다 적은 묶음을 그래픽 처리 장치의 대기열에 넣습니다
  3. 그래픽 처리 장치가 묶음을 꺼내 처리합니다. 다 끝나면 표시를 남깁니다

누가 하나:

Vulkan 같은 새 그래픽 도구 묶음에서는 프로그램이 이 일을 직접 합니다. OpenGL 같은 옛 도구 묶음에서는 그래픽 처리 장치를 다루는 드라이버가 뒤에서 대신 합니다.

무엇이 나빠지나:

넘긴 명령이 언제 끝날지 프로그램은 바로 모릅니다. 결과를 쓰려면 끝났다는 표시를 따로 확인해야 합니다. 묶음을 너무 크게 모으면 그래픽 처리 장치가 그만큼 늦게 일을 시작합니다.

상세

식당 홀 직원은 주문을 하나 받을 때마다 주방으로 뛰어가지 않습니다. 한 테이블의 주문을 주문서 한 장에 다 적어 주방 창구에 꽂아 둡니다. 주방은 꽂힌 순서대로 요리합니다. 홀 직원은 그사이 다른 테이블로 갑니다.

프로그램은 CPU(Central Processing Unit, 중앙 처리 장치)에서 돕니다. 화면을 그리는 계산은 GPU(Graphics Processing Unit, 그래픽 처리 장치)가 맡습니다. GPU 는 같은 계산을 수많은 점에 한꺼번에 하는 데 특화된 처리기입니다. CPU 와 GPU 는 따로 돌아갑니다.

CPU 는 GPU 를 함수 부르듯 바로 부리지 못합니다. 할 일을 GPU 가 알아듣는 명령으로 적어 건넵니다. 「이 삼각형들을 그려라」 · 「이 메모리를 저기로 복사하라」 · 「화면을 이 색으로 지워라」 같은 것이 GPU 명령입니다.

명령을 GPU 가 알아듣는 형식으로 바꾸는 일은 그래픽 드라이버가 합니다. GPU 마다 명령 형식이 다릅니다. 그래서 GPU 를 만든 회사가 자기 GPU 에 맞는 드라이버를 함께 내놓습니다.

명령은 하나씩 넘기지 않습니다. 메모리 한 구역에 차곡차곡 적어 묶음을 만듭니다. 이렇게 명령을 적어 두는 메모리 구역이 커맨드 버퍼입니다. 식당의 주문서 한 장이 커맨드 버퍼 하나에 해당합니다.

다 적은 커맨드 버퍼를 GPU 에 건네는 일이 커맨드 서브미션입니다. 우리말로 「명령 제출」이라고도 부릅니다. 건넨 뒤에 CPU 는 기다리지 않고 다음 일을 합니다. GPU 는 받은 묶음을 자기 속도로 처리합니다.

명령을 모아서 넘기는 까닭

명령을 하나씩 넘기지 않는 데에는 두 가지 비용이 걸려 있습니다. 넘기는 비용과 기다리는 비용입니다.

GPU 에 명령을 넘기려면 대개 커널을 거칩니다. 커널은 운영체제의 핵심입니다. 하드웨어를 직접 다룰 권한을 쥔 부분입니다.

시스템 콜은 일반 프로그램이 커널에 일을 부탁하는 통로입니다. 시스템 콜 한 번은 보통 함수 호출보다 훨씬 비쌉니다. 프로그램이 멈추고 커널로 넘어갔다가 돌아오는 절차가 붙기 때문입니다.

명령마다 시스템 콜을 하면 이 비용이 명령 수만큼 쌓입니다. 명령 여럿을 한 묶음으로 넘기면 비용을 한 번만 냅니다. 데이터베이스에 행을 하나씩 넣지 않고 여러 행을 한 번에 넣는 일괄 삽입과 같은 계산입니다.

둘째 비용은 기다림입니다. CPU 가 명령 하나를 주고 끝나기를 기다리면 그동안 CPU 가 놉니다. GPU 가 일을 끝내고 다음 명령을 기다리는 동안에는 GPU 가 놉니다. 둘이 번갈아 쉬면 어느 쪽도 제 속도를 못 냅니다.

묶음을 넘기고 기다리지 않으면 둘이 겹쳐서 일합니다. 넘긴 쪽이 결과를 기다리지 않고 제 일을 이어 가는 방식을 비동기라고 합니다. 아래 표는 시간을 세 구간으로 잘라 두 처리기가 각각 무엇을 하는지 보입니다.

구간 CPU GPU
1 1번 장면의 명령을 적는다 쉰다
2 2번 장면의 명령을 적는다 1번 장면을 그린다
3 3번 장면의 명령을 적는다 2번 장면을 그린다

첫 구간만 지나면 둘 다 쉬지 않습니다. GPU 가 한 장면을 그리는 동안 CPU 는 다음 장면을 준비합니다.

기록과 제출

커맨드 버퍼를 다루는 일은 두 단계로 나뉩니다. 커맨드 서브미션은 그 가운데 뒤 단계입니다.

명령을 커맨드 버퍼에 적는 단계를 기록이라고 부릅니다. 기록은 메모리에 글을 쓰는 일이라 프로그램 안에서 끝납니다. 이 단계에서는 커널도 GPU 도 아직 아무것도 모릅니다.

다 적은 커맨드 버퍼를 GPU 쪽으로 넘기는 단계가 제출입니다. 비싼 일은 이쪽에 몰려 있습니다. 제출할 때는 커널을 거쳐야 합니다.

커맨드 버퍼가 쓰는 메모리를 GPU 가 읽을 수 있는지도 이때 확인합니다. 예를 들어 커맨드 버퍼가 가리키는 그림 데이터가 GPU 가 닿을 수 있는 메모리에 실제로 올라와 있어야 합니다. 확인할 메모리가 많을수록 제출이 오래 걸립니다.

두 단계를 나누면 기록을 여러 스레드가 나눠 맡을 수 있습니다. 스레드는 한 프로그램 안에서 동시에 도는 실행 흐름입니다. 스레드마다 커맨드 버퍼를 하나씩 채웁니다. 제출은 한 곳에서 모아서 합니다.

나눠 맡기는 까닭은 기록에도 CPU 시간이 들기 때문입니다. 장면이 복잡할수록 적을 명령이 많아집니다. 스레드 하나로 다 적으면 그 스레드가 장면 전체의 속도를 묶습니다.

프로그램이 GPU 를 부리는 함수 묶음을 그래픽 API(Application Programming Interface, 응용 프로그래밍 인터페이스)라고 합니다. OpenGL 같은 옛 그래픽 API 는 기록과 제출을 프로그램에 드러내지 않았습니다. 프로그램이 그리기 함수를 부르면 드라이버가 뒤에서 모아 두었습니다. 언제 제출할지도 드라이버가 정했습니다.

Vulkan · Direct3D 12 · Metal 같은 새 그래픽 API 는 이 둘을 프로그램에 맡깁니다. 프로그램이 커맨드 버퍼를 직접 만들어 채웁니다. 언제 제출할지도 프로그램이 정합니다. 제출 함수는 Vulkan 에서는 vkQueueSubmit, Direct3D 12 에서는 ExecuteCommandLists, Metal 에서는 커맨드 버퍼의 commit 입니다.

제출이 지나가는 길

이 소절은 커맨드 버퍼 하나가 프로그램을 떠나 GPU 에 닿기까지를 따라갑니다. 끝났다는 알림이 돌아오는 길도 봅니다. 그 길에는 그래픽 드라이버의 두 부분이 차례로 섭니다.

사용자 공간은 일반 프로그램이 도는 영역입니다. 이 영역의 코드는 하드웨어에 직접 손대지 못합니다. 하드웨어를 만지는 일은 커널이 맡습니다.

그래서 그래픽 드라이버도 두 부분으로 갈립니다. 한 부분은 프로그램과 함께 사용자 공간에서 돕니다. 다른 부분은 커널 안에서 돕니다.

사용자 공간 쪽 드라이버는 프로그램의 그리기 요청을 GPU 명령으로 바꿔 커맨드 버퍼에 적습니다. 제출할 때가 되면 시스템 콜로 커널 쪽 드라이버에 커맨드 버퍼를 넘깁니다.

커널 쪽 드라이버는 그 버퍼가 쓰는 메모리가 GPU 가 닿을 수 있는 메모리에 올라와 있는지 확인합니다. 그런 다음 GPU 에 명령을 읽어 가라고 알립니다. GPU 는 커맨드 버퍼를 읽어 명령을 하나씩 처리합니다.

인터럽트는 장치가 CPU 에 「일이 생겼다」고 끼어들어 알리는 신호입니다. GPU 는 묶음을 다 처리하면 인터럽트로 커널 쪽 드라이버에 알립니다.

커널 쪽 드라이버는 이 알림을 받고 그 묶음의 완료 표시를 바꿔 둡니다. 프로그램은 이 표시를 들여다보고 묶음이 끝났음을 압니다. 이 표시가 뒤 소절에서 다룰 펜스입니다. 아래 그림은 이 순서를 위에서 아래로 보입니다.

sequenceDiagram
    participant P as 프로그램
    participant U as 사용자 공간 쪽 드라이버
    participant K as 커널 쪽 드라이버
    participant G as GPU
    P->>U: 그리기 함수 호출
    U->>U: GPU 명령으로 바꿔 커맨드 버퍼에 적는다
    U->>K: 시스템 콜로 커맨드 버퍼를 넘긴다
    K->>G: 명령을 읽어 가라고 알린다
    Note over P,U: 프로그램은 기다리지 않고 다음 장면을 적는다
    G-->>K: 다 끝냈다는 인터럽트
    K->>K: 그 묶음의 펜스를 끝남으로 바꾼다
    P->>K: 펜스 상태를 확인한다

제출하는 쪽은 하나가 아닙니다. 브라우저와 게임이 한 GPU 에 동시에 묶음을 넘깁니다. 누구의 묶음을 먼저 돌릴지는 커널 쪽 드라이버가 정합니다. 이렇게 순서를 정하는 일이 GPU 스케줄링입니다.

제출한 묶음이 기다리는 큐

제출한 커맨드 버퍼는 곧바로 실행되지 않고 큐에 들어갑니다. 큐는 먼저 넣은 것을 먼저 꺼내는 대기열입니다.

GPU 에 딸린 이 큐가 커맨드 큐입니다. GPU 는 커맨드 큐에서 묶음을 하나씩 꺼내 처리합니다. 그래서 같은 큐에 넣은 묶음은 넣은 순서대로 처리가 시작됩니다. 식당 창구에 꽂힌 주문서가 꽂힌 순서대로 주방에 들어가는 것과 같습니다.

GPU 는 큐를 여럿 가질 수 있습니다. 그리기용 · 계산용 · 메모리 복사용처럼 하는 일에 따라 나뉘기도 합니다. 큐가 다르면 묶음끼리 서로 기다리지 않고 나란히 돌 수 있습니다. 큰 그림 파일을 GPU 메모리로 옮기는 동안에도 그리기가 멈추지 않습니다.

끝났음을 알리는 펜스

제출은 비동기라서 CPU 는 그 묶음이 언제 끝날지 모릅니다. 그런데 그린 결과를 화면에 내거나 다시 읽으려면 끝났는지 알아야 합니다. 끝나기 전에 그 메모리를 고쳐 쓰면 GPU 가 아직 읽고 있는 데이터가 망가집니다.

그래서 제출할 때 펜스를 함께 넘깁니다. 펜스는 「이 묶음까지 끝났다」를 알리는 표시입니다. GPU 가 묶음을 다 처리하면 펜스의 상태가 바뀝니다. CPU 는 펜스를 들여다보거나 상태가 바뀔 때까지 기다립니다.

펜스를 언제 기다리느냐가 성능을 가릅니다. 제출하자마자 기다리면 CPU 와 GPU 가 다시 번갈아 놉니다.

그래서 보통 앞서 제출한 장면의 펜스만 확인합니다. 이번 장면을 넘길 때 몇 장면 전 묶음이 끝났는지를 봅니다. 그 묶음이 쓰던 메모리를 이번 장면에 다시 쓰려는 것입니다.

묶음 크기를 정하는 기준

묶음이 작고 제출이 잦으면 제출 비용이 쌓입니다. CPU 시간이 제출에 새어 나갑니다. 정작 다음 장면의 명령을 적을 시간이 줄어듭니다.

묶음이 너무 크면 반대 문제가 생깁니다. GPU 는 묶음이 제출된 뒤에야 일을 시작합니다. 한 장면을 다 적고 나서야 넘기면 그동안 GPU 가 비어 있을 수 있습니다.

그래서 한 장면을 몇 묶음으로 나눠 앞부분부터 먼저 넘기기도 합니다. 제출 비용과 GPU 의 빈 시간 사이에서 묶음 크기를 고르는 셈입니다.

관련 항목

커맨드 서브미션을 이루는 구성 요소

커맨드 버퍼 · 커맨드 큐 · 펜스 · 세마포어 · 타임라인 세마포어 · 링 버퍼

커맨드 버퍼에 적히는 명령

드로 콜 · 컴퓨트 셰이더 · 파이프라인 배리어 · 렌더 패스 · DMA

제출이 거치는 소프트웨어 계층

그래픽 드라이버 · 사용자 공간 · 커널 · 시스템 콜 · 인터럽트 · 다이렉트 렌더링 매니저 · GPU 스케줄링

기록과 제출을 프로그램에 맡기는 그래픽 API

Vulkan · Direct3D · Metal · WebGPU · OpenGL

제출 횟수를 줄이는 기법

드로 콜 배칭 · 인스턴싱 · 간접 드로 · 멀티스레드 렌더링

제출 비용이 깎아 먹는 프레임 지표

프레임 시간 · 프레임 예산 · 프레임 드롭 · 프레임률

커맨드 서브미션이 기대는 바탕 개념

GPU · CPU · 비동기 · 큐 · 스레드 · 생산자-소비자 문제

커맨드 서브미션이 속하는 상위 분야

그래픽스 · 렌더링 · GPU 프로그래밍 · GPGPU

다른 이름: command submission · 명령 제출 · 커맨드 버퍼 제출 · 큐 제출 · queue submission