파이프라인 캐시
고친 사람 github-actions[bot]
파이프라인 캐시는 그래픽 카드용 프로그램을 컴파일한 결과를 남겨 두었다가 다시 씁니다. 같은 프로그램을 다시 준비할 때 컴파일을 건너뛰게 해 줍니다. 빌드 도구가 실행 사이에 파일을 남겨 두는 기능도 같은 이름으로 부릅니다. 이 항목은 그래픽스 쪽 뜻을 다룹니다.
쉽고 빠른 이해
파이프라인 캐시는 그래픽 카드용 프로그램을 컴파일해 둔 결과를 보관합니다. 게임을 두 번째로 켤 때 첫 번째에 컴파일해 둔 결과를 꺼내 쓰는 식입니다.
이 컴파일은 느립니다. 화면을 그리는 도중에 하면 화면이 잠깐 멈칫합니다. 켤 때마다 다시 하면 로딩도 길어집니다.
어떻게 도나:
- 처음 실행할 때 드라이버(그래픽 카드를 움직이는 소프트웨어)가 컴파일하면서 결과를 캐시에 담습니다
- 앱이 끝날 때 캐시 내용을 파일로 저장합니다
- 다음 실행 때 그 파일로 캐시를 채워 두면 컴파일을 건너뜁니다
대가도 있습니다. 그래픽 카드나 드라이버가 바뀌면 저장한 파일을 못 씁니다. 그때는 처음처럼 다시 컴파일합니다. 첫 실행은 캐시가 있어도 빨라지지 않습니다. 컴파일할 프로그램이 몇 개 안 되는 짧은 앱은 얻는 게 적습니다.
상세
이 절은 파이프라인 캐시가 무엇을 건너뛰게 해 주는지, 그 결과를 어떻게 다음 실행까지 들고 가는지, 언제 못 쓰게 되는지를 봅니다. 파이프라인을 수백 개 만드는 게임 하나를 줄곧 예로 씁니다.
통역사가 처음 옮긴 문장을 수첩에 적어 둡니다. 같은 문장이 또 나오면 머리를 쓰지 않고 수첩을 봅니다. 수첩은 다음 출장에도 들고 갑니다. 파이프라인 캐시가 이 수첩입니다.
파이프라인과 컴파일
GPU(Graphics Processing Unit, 그래픽 처리 장치)는 같은 계산을 동시에 아주 많이 돌리는 칩입니다. 화면의 점 수백만 개의 색을 한꺼번에 정하려고 만들었습니다. 이 칩을 얹은 판을 흔히 그래픽 카드라고 부릅니다.
GPU 에서 도는 작은 프로그램을 셰이더라고 부릅니다. 점의 위치를 옮기는 셰이더, 점의 색을 정하는 셰이더가 따로 있습니다. 게임은 이런 셰이더를 수백 개 씁니다.
파이프라인은 셰이더 몇 개와 그리기 설정을 한 덩어리로 묶어 굳혀 둔 객체입니다. 색을 어떻게 섞을지, 앞뒤를 어떻게 가릴지 같은 설정이 함께 묶입니다. 그릴 때는 이 덩어리를 고르기만 합니다. 미리 묶어 두면 그릴 때마다 설정을 검사하는 일을 덜 수 있습니다.
드라이버는 GPU 를 움직이는 소프트웨어입니다. 앱이 내린 명령을 GPU 가 알아듣는 꼴로 바꿔 줍니다.
파이프라인을 만드는 순간 드라이버가 셰이더를 컴파일합니다. 컴파일 결과는 그 GPU 가 바로 돌리는 기계어입니다.
컴파일이 사용자 컴퓨터에서 일어나는 까닭
백엔드 코드는 빌드할 때 한 번 컴파일해서 배포합니다. 셰이더는 그렇게 못 합니다. 앱은 자기가 어느 회사의 어느 GPU 에서 돌지 모릅니다. GPU 마다 기계어가 다릅니다.
그래서 셰이더는 GPU 종류와 상관없는 중간 형태로 배포됩니다. 기계어로 옮기는 마지막 단계는 사용자의 컴퓨터에서 그 GPU 의 드라이버가 맡습니다.
앱이 GPU 에 일을 시킬 때 부르는 함수 모음을 API(Application Programming Interface, 응용 프로그램 인터페이스)라고 합니다. Vulkan은 GPU 를 다루는 API 가운데 하나입니다.
Vulkan 에서 쓰는 셰이더의 중간 형태는 SPIR-V(Standard Portable Intermediate Representation, 표준 이식 중간 표현)입니다. 앱은 셰이더를 이 꼴로 들고 있다가 파이프라인을 만들 때 드라이버에게 넘깁니다.
드라이버가 맡는 이 마지막 컴파일은 짧지 않습니다. 셰이더가 길고 파이프라인이 많으면 사람이 느낄 만큼 걸립니다. 게임이 켤 때마다 파이프라인 수백 개를 처음부터 컴파일하면 로딩 화면이 길어집니다.
더 아픈 경우는 그리는 도중입니다. 화면 한 장을 프레임이라고 부릅니다. 게임은 1초에 프레임을 수십 장 그립니다.
처음 보는 파이프라인이 필요해져 그 프레임 안에서 컴파일하면 그 한 장이 늦게 나옵니다. 사용자 눈에는 화면이 잠깐 멈칫한 것으로 보입니다. 이 현상을 셰이더 컴파일 끊김이라고 부릅니다.
캐시가 하는 일
파이프라인 캐시는 드라이버가 컴파일한 결과를 담아 두는 그릇입니다. 앱이 이 캐시를 먼저 하나 만들어 둡니다. 그리고 파이프라인을 만들 때마다 그것을 함께 넘깁니다.
드라이버는 넘겨받은 캐시에서 같은 입력으로 컴파일한 결과를 찾습니다. 있으면 그 결과를 꺼내 쓰고 컴파일을 건너뜁니다. 없으면 컴파일한 뒤 결과를 캐시에 담아 둡니다. 무엇이 같으면 같은 입력으로 칠지는 드라이버가 정합니다.
한 번 실행하는 동안에도 쓸모가 있습니다. 파이프라인 여럿이 같은 셰이더를 쓰면 두 번째부터는 캐시에서 꺼냅니다. 게임이 비슷한 파이프라인을 여러 벌 만들 때 컴파일이 그만큼 줄어듭니다.
하지만 캐시는 메모리에 있어서 앱이 끝나면 사라집니다. 다음 실행에서도 쓰려면 앱이 직접 파일로 남겨야 합니다.
파일로 남기고 다시 읽기
앱은 드라이버에게 캐시 내용을 달라고 해서 바이트 덩어리로 받습니다. 그리고 그 바이트를 디스크에 파일로 씁니다. 메모리에 있는 것을 이렇게 바이트로 꺼내 저장하는 일을 직렬화라고 부릅니다.
다음 실행 때 앱은 그 파일을 읽습니다. 캐시를 만들 때 읽은 바이트를 넘기면 드라이버가 그 내용으로 캐시를 미리 채웁니다. 이후 파이프라인을 만들면 지난번 결과를 꺼내 씁니다.
채우기 전에 드라이버는 그 바이트가 지금 GPU 에서 쓸 수 있는 것인지 확인합니다. 바이트의 앞머리를 봅니다.
sequenceDiagram
participant 앱
participant 드라이버
participant 디스크
Note over 앱,디스크: 첫 실행
앱->>드라이버: 캐시를 넘기며 파이프라인을 만든다
Note over 드라이버: 컴파일하고 결과를 캐시에 담는다
앱->>드라이버: 캐시 내용을 달라고 한다
드라이버-->>앱: 바이트 덩어리
앱->>디스크: 파일로 쓴다
Note over 앱,디스크: 다음 실행
디스크-->>앱: 지난번 파일
앱->>드라이버: 그 바이트로 캐시를 만든다
Note over 드라이버: 앞머리를 보고 같은 GPU 인지 확인한다
앱->>드라이버: 캐시를 넘기며 파이프라인을 만든다
Note over 드라이버: 캐시에 있으면 컴파일을 건너뛴다
위 절반이 첫 실행이고 아래 절반이 다음 실행입니다. 다음 실행에서는 지난번 파일 덕분에 컴파일이 빠집니다.
바이트 덩어리 안쪽은 앱이 읽을 수 없습니다. 형식은 드라이버마다 다릅니다. 그 형식은 드라이버만 압니다. 앱은 받은 바이트를 손대지 않고 파일에 썼다가 돌려주기만 합니다. 이런 데이터를 불투명한 데이터라고 부릅니다.
앞머리의 확인 값
바이트 덩어리 안쪽은 앱이 못 읽지만, Vulkan 에서는 맨 앞 32바이트만은 예외입니다. 이 앞머리는 꼴이 정해져 있어 앱도 읽을 수 있습니다. 여기에 캐시를 만든 GPU 를 가리키는 값이 담깁니다.
| 앞머리 값 | 무엇을 가리키나 |
|---|---|
| 앞머리 길이와 판 | 앞머리를 어떻게 읽을지 |
| 제조사 번호 | GPU 를 만든 회사 |
| 기기 번호 | 그 회사의 어느 GPU 인지 |
| 캐시 UUID(Universally Unique Identifier, 범용 고유 식별자) | 그 드라이버가 만든 캐시를 알아보는 값 |
UUID 는 겹칠 일이 없도록 만든 긴 식별 번호입니다. 드라이버는 캐시 앞머리의 이 값을 자기 값과 견주어 읽을 수 있는 캐시인지 가립니다. 드라이버 판이 바뀌어 기계어가 달라지면 이 값도 바뀝니다.
캐시를 만들 때 넘긴 바이트가 지금 GPU 와 맞지 않으면 캐시는 빈 채로 시작합니다. 오류가 나지는 않습니다. 컴파일을 처음부터 다시 할 뿐입니다.
그래서 드라이버를 업데이트한 다음 날 게임이 유난히 오래 켜지곤 합니다. 지난번 캐시 파일이 버려지고 첫 실행처럼 전부 다시 컴파일하기 때문입니다.
API 마다 부르는 이름
GPU 를 다루는 API 는 여럿입니다. 파이프라인 캐시를 다루는 방법도 API 마다 다릅니다.
| API | 캐시를 다루는 객체·호출 | 저장과 복원 |
|---|---|---|
| Vulkan | VkPipelineCache |
vkGetPipelineCacheData 로 꺼내 파일에 쓰고, 만들 때 그 바이트를 넘긴다 |
| Direct3D 12 | ID3D12PipelineLibrary |
파이프라인 여럿을 이름으로 담아 한 덩어리로 저장한다 |
| Metal | MTLBinaryArchive |
컴파일한 파이프라인을 담아 파일로 내보낸다 |
| OpenGL | glGetProgramBinary |
셰이더를 묶은 프로그램 하나의 기계어를 꺼낸다 |
| WebGPU | 없음 | 브라우저가 안에서 알아서 맡는다 |
Vulkan 은 캐시 여럿을 하나로 합치는 호출 vkMergePipelineCaches 도 둡니다. 스레드 여럿이 각자
캐시를 들고 파이프라인을 만든 뒤 마지막에 하나로 합쳐 저장할 수 있습니다.
WebGPU 에는 앱이 만지는 캐시가 없습니다. 웹 페이지가 GPU 의 기계어를 디스크에 쓰게 두지 않으려는 설계입니다. 컴파일 결과를 남겨 둘지는 브라우저가 정합니다.
드라이버가 따로 두는 캐시
앱이 아무것도 안 해도 드라이버가 자기 몫의 디스크 캐시를 두기도 합니다. 그래서 캐시를 직접 다루지 않는 게임도 두 번째 실행이 빨라지곤 합니다.
드라이버 쪽 캐시는 어디에 얼마나 남길지를 앱이 정하지 못합니다. 앱이 다루는 파이프라인 캐시는 그 반대입니다. 파일을 언제 쓰고 어디에 두고 언제 지울지를 앱이 정합니다.
쓸 때와 안 쓸 때
파이프라인 캐시는 같은 파이프라인을 여러 번 만드는 앱에 맞습니다. 켤 때마다 같은 파이프라인 수백 개를 만드는 게임이 대표입니다. 실행 횟수가 많을수록 첫 실행의 컴파일 시간이 묻힙니다.
파이프라인을 몇 개만 만들고 끝나는 짧은 프로그램에는 이득이 적습니다. 파일을 쓰고 읽는 수고가 아끼는 컴파일 시간보다 클 수 있습니다.
첫 실행은 빨라지지 않습니다. 담아 둔 것이 없기 때문입니다. 그래서 게임은 로딩 화면에서 파이프라인을 미리 만들어 둡니다. 그리는 도중에 처음 보는 파이프라인을 만나지 않게 하려는 것입니다.
캐시 파일은 앱이 관리해야 하는 짐입니다. 파이프라인 수만큼 파일이 커집니다. 저장하다 앱이 죽으면 반쯤 쓴 파일이 남을 수 있습니다. 그래서 길이를 확인하고 이상하면 버리는 처리를 함께 둡니다.
백엔드 개발자가 이 캐시를 손으로 다룰 일은 드뭅니다. 게임 엔진과 GPU 를 쓰는 라이브러리가 안쪽에서 대신 다룹니다. 다만 「배포한 뒤 사용자 기기에서 컴파일하고 그 결과를 남긴다」는 그림은 JIT 컴파일(Just-In-Time 컴파일, 실행 시점 컴파일)과 닮았습니다. 둘 다 첫 실행이 느립니다. 결과를 남겨 두면 다음 실행이 빨라집니다.
빌드 도구의 파이프라인 캐시
지속적 통합(CI, Continuous Integration) 도구는 코드가 올라올 때마다 빌드와 테스트를 돌립니다. 이 단계 묶음을 흔히 파이프라인이라고 부릅니다.
이 도구에서 파이프라인 캐시는 내려받은 의존성 폴더를 실행 사이에 남겨 두는 기능을 가리킵니다. 매번 같은 라이브러리를 다시 받지 않으려는 것입니다. 이름은 같아도 그래픽스의 파이프라인 캐시와는 상관이 없습니다. 이쪽 뜻은 빌드 캐시가 가깝습니다.
관련 항목
파이프라인 캐시가 결과를 담아 두는 파이프라인 객체
파이프라인 · 그래픽스 파이프라인 · 컴퓨트 파이프라인 · 레이 트레이싱 파이프라인 · 렌더 파이프라인 · 파이프라인 상태 객체 · 파이프라인 라이브러리
파이프라인 캐시가 건너뛰게 해 주는 처리 단계
셰이더 컴파일 · 컴파일 · 컴파일러 · 기계어 · SPIR-V · 중간 표현 · 셰이더 모듈 · 특수화 상수
파이프라인 캐시를 다루는 그래픽스 API
Vulkan · Direct3D · Direct3D 12 · Metal · OpenGL · WebGPU
파이프라인 캐시 데이터를 저장하고 확인하는 수단
직렬화 · UUID · 불투명 데이터 · 디스크 캐시 · 해시
파이프라인 캐시가 줄이려는 지연
셰이더 컴파일 끊김 · 프레임 · 프레임 시간 · 로딩 시간 · 지연
파이프라인 캐시와 같은 원리로 결과를 재사용하는 캐시
캐싱 · 빌드 캐시 · JIT 컴파일 · 코드 캐시 · 메모이제이션
파이프라인 캐시와 이름이 겹치는 빌드 도구의 기능
지속적 통합 · 빌드 파이프라인 · 의존성 캐시 · 빌드 캐시
파이프라인 캐시가 속하는 상위 분류
다른 이름: pipeline cache · VkPipelineCache · 파이프라인 상태 캐시 · PSO 캐시