Playdate C 최적화 비법: 게임보이 에뮬레이터 코어를 20KB에서 2KB로
- Playdate CPU는 레지스터 연산이 매우 빠르지만 캐시 밖 메모리 접근이 느림. Rev A의 명령어 캐시가 4KB뿐이라 코어 코드가 여기에 들어가야 성능이 나옴(Rev B는 16KB로 추정)
- -O3보다 -Os가 더 빠를 수 있음. 20KB였던 게임보이 에뮬레이터 코어를 거대한 switch 테이블을 걷어내 2KB로 줄였더니 동작은 그대로고 속도는 훨씬 빨라짐
- 코어 함수는
__attribute__((section(".text.섹션이름")))로 연속 주소에 묶고,C_API/buildsupport의link_map.ld를 복사한 뒤 makefile에서override LDSCRIPT=./link_map.ld를 지정해 커스텀 링커 스크립트를 씀 - 거의 실행되지 않는 opcode나 시뮬레이션에서 드문 예외 처리는 4KB 코어 밖으로 빼서 캐시 압박을 줄임. 에뮬레이터, 대형 시뮬레이션, 3D 렌더러, 코덱처럼 극한 성능이 필요한 코드에 해당하는 기법임
Hacker News opinions
이 글 읽으면 RISC 논쟁이 다시 떠오름. 메모리 속도가 CPU 속도를 따라잡았다는 'Case for RISC'의 첫 번째 전제가 여기선 그냥 깨지잖아. 코드 밀도도 여전히 중요하고. 그리고 그 논문은 사실 제목이 잘못 붙은 거임. Patterson과 Ditzel은 RISC가 뭔지 끝까지 정의 안 하고, CISC 설계 방식을 비판하는 데 대부분을 씀.
내가 보기엔 8비트, 16비트 시절 홈컴퓨터 게임들이 최고 성능 노리고 전부 어셈블리로 짠 이유가 바로 이거더라.
플스1로 복셀 렌더링 데모 만지작거릴 때랑 비슷함. 거긴 메인 RAM 읽을 때마다 6사이클 스톨이 걸리고 데이터 캐시도 없어서, 핫 루프 전체를 함수 하나에 몰아넣어 4KiB 직접매핑 명령어 캐시에 맞췄음. 1024개 명령어 안쪽으로.
C++ 템플릿 하나 짜서 함수 호출을 2KiB 스크래치패드로 트램폴린시켰고, 남은 스크래치패드는 CLUT 테이블로 썼음. 스톨이 안 걸리니까. GTE로 N번째 좌표를 변환하는 동안 CPU는 N+1번째 높이맵 데이터를 기다리게 겹쳐 먹였고, 실제 3D 투영 대신 단순화한 식으로 GTE를 이론상 한계보다 더 혹사시켰음.
사각형 프리미티브는 디스플레이 리스트 안 거치고 GPU 레지스터에 바로 썼는데, 그렇게까지 플스를 갈궈서 PCSX-Redux랑 DuckStation이 성능 예측을 서로 다르게 내더라.
코드 배치랑 세그먼트 나누는 팁은 완전 옛날 생각 나네. 그 시절로 돌아간 기분임.