개요
15편에서 좀비 수에 뚜껑을 씌워 최악의 경우를 예산 안에 넣었다. 그런데 그건 원래 하려던 게 아니었다.
원래 목표는 오브젝트를 매번 새로 만들고 부수는 대신 만들어둔 걸 돌려쓰는 것이었다. 14편에서 잰 값에는 그게 겨냥하는 비용이 안 나왔는데, 나는 그게 조건을 잘못 잡아서라고 생각했다. 그래서 조건을 다시 설계해서 한 번 더 갔다.
그리고 이 편에서 네 번째 가설까지 틀린다. 마지막엔 재고 있던 값이 게임과 아무 상관 없는 값이었다는 걸 알게 된다.
이번 글이 답할 질문. 값이 안 변한다는 것은 무엇을 뜻하는가?
1. 조건을 또 바꾼 이유
돌려쓰기가 줄이려는 비용이 뭔지부터 다시 봤다. 동시에 몇 마리가 살아 있느냐가 아니다. 줄이려는 건 만들고 부수는 왕복 횟수다.
- 만들 때 — 새 오브젝트를 찍어낸다
- 부술 때 — 쓰던 메모리가 쓰레기가 된다
- 돌려쓰면 — 둘 다 안 한다. 껐다 켜기만 한다
14편·15편의 조건은 좀비를 쌓는 조건이었다. 무적으로 가만히 서 있으니 좀비가 죽질 않는다. 왕복이 거의 안 일어난다.
그러니까 지금까지 나는 왕복 비용을 재겠다면서 왕복이 없는 상황을 재고 있었다.
새 조건의 목표는 반대다.
| 앞선 측정 | 이번 측정 | |
|---|---|---|
| 동시 존재 수 | 최대로 | 낮게 |
| 왕복 횟수 | 거의 없음 | 최대로 |

재려는 값이 무엇의 함수인지 먼저 정한다. 그걸 안 정하면 조건이 아무리 극단이어도 엉뚱한 걸 극단으로 만든다.
2. 측정 도구가 측정 대상을 오염시킨다
좀비를 계속 죽이려면 뭔가로 때려야 한다. 여기서 걸린 게 한둘이 아니었다.
무기를 끄면 절반만 재게 된다
처음엔 무기를 끄고 좀비만 계속 뽑을 생각이었다. 그러면 부수는 쪽이 한 번도 안 일어난다. 왕복의 절반만 재는 셈이다. 이건 짜기 전에 알아채서 다행이었다.
사거리를 늘리려다 철회했다
그럼 무기를 켜두고 사거리를 크게 늘려 태어나는 자리까지 덮으면 되겠다 싶었다. 코드를 보다가 접었다.
근접 무기는 매번 이렇게 돈다.
- 범위 안의 대상을 배열로 받아온다 — 이 배열이 매번 새로 만들어진다
- 받아온 전부에 대해 때릴 수 있는 대상인지 물어본다
이번 측정의 주 지표가 쓰레기 메모리 발생량인데, 재는 도구 쪽에서 쓰레기가 나온다. 게다가 사거리를 키우면 잡히는 대상이 늘어나 그 양도 같이 늘어난다.
측정 도구가 측정 대상과 같은 자원을 쓰면, 나온 숫자는 둘의 합이다.
그래서 임시 부품을 하나 만들었다
무기를 안 쓰기로 했다. 대신 1초 뒤에 자기가 붙은 좀비를 죽이는 임시 부품을 좀비마다 붙였다.
단, 죽이는 방법은 정상 경로 그대로 썼다. 체력을 깎아서 죽게 만들면 죽음 처리와 소멸 처리가 원래대로 다 돈다. 오브젝트만 슬쩍 지워버리면 그건 실제 게임에서 안 일어나는 일이다.
1초를 세는 방법에 후보가 셋 있었다.
| 방법 | 메모리 | CPU |
|---|---|---|
| 기다림 객체를 쓰는 것 | 좀비마다 새로 생김 | 없음 |
| 매 프레임 시간을 확인 | 없음 | 매 프레임 깨어난다 |
| 예약 호출 | 거의 없음 | 거의 없음 |
첫 번째는 재려는 값 자체를 늘린다. 두 번째는 좀비 수만큼 매 프레임 일이 는다. 예약 호출을 골랐다.
이 코드는 커밋하지 않는다
임시 부품과 바꿔둔 설정은 커밋하지 않았다.
커밋하면 다음 커밋이 반드시 “되돌림”이 된다. 기록에 왕복이 남고, 정작 성능 관련 커밋의 증거 가치가 떨어진다. 조건은 코드가 아니라 문서에 적는다. 이건 14편에서 정한 것과 같은 규칙이다.
3. 예상을 먼저 적는다
재기 전에 예상을 적어뒀다.
할당이 생기는 프레임에 1~2KB, 3프레임에 한 번쯤 생기니 평균 0.5KB쯤 나올 것이다.
실측은 464 B였다. 한 자릿수는 아니지만 예상의 10분의 1이다. 틀린 이유가 둘이었다.
첫째, 오브젝트를 찍어내는 일 대부분은 C#이 관리하는 메모리 밖에서 일어난다. 유니티 내부 영역에서 처리되고, 프로파일러의 그 칸에 잡히는 건 그중 일부다. 나는 만드는 비용 전부가 저 칸에 뜰 거라 생각했다.
둘째, 스폰 설정을 안 보고 예상했다.

“3프레임에 한 마리”라고 적었는데 실제 설정은 1초에 한 번 20마리가 한꺼번에였다. 전제부터 세팅과 안 맞았다.
예상은 맞히려고 적는 게 아니라 틀린 지점을 발견하려고 적는다. 재고 나서 적으면 “아 그때 얼마였더라”가 된다.
4. 값이 안 변한다
왕복률을 단계적으로 올렸다. 동시 존재 수가 같이 늘면 조건이 무너지니, 스폰 간격을 줄이는 만큼 좀비 수명도 같이 줄였다.
| 단계 | 스폰 간격 | 수명 | 설정상 왕복/초 | 좀비 수 | 쓰레기 메모리 |
|---|---|---|---|---|---|
| 0 | 1.0초 | 1.0초 | 20 | 약 20 | 464 B |
| 1 | 0.5초 | 0.75초 | 40 | 미기록 | 464 B |
| 2 | 0.25초 | 0.4초 | 80 | 40 | 464 B |
| 3 | 0.125초 | 0.2초 | 160 | 40 | 464 B |
한 바이트도 안 움직였다.
그리고 3단계는 조건이 성립하지 않았다. 2단계와 3단계의 최대 좀비 수가 둘 다 40이다. 설정만 두 배였을 뿐 실제로는 같은 상황이었다는 뜻이다.
왜 3단계에서 더 안 늘었는지는 모른다. 기다림 처리가 프레임 단위라 그보다 짧은 간격은 뭉개졌을 가능성을 의심하는데, 배포까지 해야하는 과정에서 조금 낮은 리스크의 문제라고 판단했다.
그래서 이 실험에서 실제로 검증된 왕복률의 상한은 초당 40회다.
네 배를 올렸는데 값이 그대로면, 그건 그 값이 왕복과 무관하다는 뜻이다.
이 문장을 적고 나서야 방향이 바뀌었다. 그전까지는 조건을 더 세게 밀 생각만 하고 있었다.
5. 도구를 의심한다
의심을 확인하는 방법은 간단했다. 좀비를 아예 안 만들어보는 것.
스포너를 껐다. 좀비 0마리. 다시 쟀다.
여전히 464 B였다.

어디서 나오는지 계통을 펼쳐서 따라갔다.
| 단계 | 항목 | 메모리 |
|---|---|---|
| 1 | 게임 루프 전체 | 464 B |
| 2 | 그 안의 씬 갱신 | 464 B |
| 3 | 뒤로 미뤄둔 작업 처리 | 464 B |
| 4 | 유니티 내부 동기화 작업 | 464 B (6회 호출) |
끝까지 내려가도 내 코드가 안 나온다. 유니티 에디터가 자기 일을 하면서 매 프레임 내는 상시 비용이었다.
그러니까 지금까지 “왕복 비용”이라고 보고 있던 값은 처음부터 끝까지 배경 잡음이었다. 0단계의 464 B도, 3단계의 464 B도 전부.
값이 안 변하면 대상을 의심하기 전에 자를 의심해야 한다. 나는 그 반대 순서로 한나절을 썼다.
6. 극단까지 밀어보고 접었다
마지막으로 조건을 한 번 더 극단으로 밀었다. 왕복을 초당 2000회 수준으로 올리고, 상한도 풀고, 8초만 돌렸다.

| 항목 | 값 |
|---|---|
| 쓰레기 메모리 | 70 B (오히려 더 작다) |
| 측정 프레임 CPU | 1.88ms |
렉은 분명히 체감됐다. 화면이 눈에 띄게 끊겼다. 그런데 내가 찍은 프레임은 안 끊긴 프레임이었던 모양이다. 상한을 풀어둬서 좀비가 쌓인 상태였으니 14편에서 확인한 물리 비용 때문일 가능성이 크기에 일단 보류해두었다..
세 가설이 전부 기각됐다
| 세웠던 가설 | 결과 |
|---|---|
| 돌려쓰기로 쓰레기 메모리 부담을 줄일 수 있다 | 기각 — 초당 2000회에서도 70 B, 수거는 아예 안 일어남 |
| 왕복률을 올리면 효과가 드러난다 | 기각 — 20~80회 구간에서 464 B 고정, 그마저 배경 잡음 |
| 더 극단으로 가면 보인다 | 기각 — 대신 다른 병목이 먼저 드러남 |
기각이 아니라 유보다
돌려쓰기를 영영 안 하겠다는 게 아니다. 지금 넣을 근거가 없다는 것이다.
지금은 화면에 나오는 게 좀비뿐이라 왕복이 적다. 그런데 터지는 효과나 날아가는 투사체를 넣으면 이야기가 달라진다. 그건 태어나서 몇 초 안에 사라지는 걸 반복하는 물건이라, 왕복이 좀비와 비교가 안 되게 많다.
그리고 마감 때문에 잘라낸 기능 목록에 샷건과 저격총이 있다. 둘 다 투사체가 필요한 무기다.
잘라낸 기능과 유보한 최적화가 같은 지점에서 다시 만난다. 그때 다시 재면 된다.
그럼 이번 이틀은 뭐였나
숫자로 남은 결론은 “안 해도 된다” 하나다. 처음엔 이게 실패처럼 느껴졌다.
그런데 반대로 생각하면, 안 쟀으면 이틀을 들여 돌려쓰기를 만들고 나서 “왜 안 빨라지지”를 하고 있었을 것이다. 그리고 진짜 병목인 물리 비용은 그때까지도 몰랐을 것이다.
최적화를 하는 것보다, 최적화가 필요 없다는 것을 데이터로 증명하는 쪽이 어려웠다.
정리
- 재려는 값이 무엇의 함수인지 먼저 정한다. 왕복 비용을 재겠다면서 왕복이 없는 조건에서 재고 있었다.
- 측정 도구가 측정 대상과 같은 자원을 쓰면 나온 숫자는 둘의 합이다. 무기 쪽 할당이 섞이려 해서 무기를 안 썼다.
- 죽이는 방법은 정상 경로를 쓴다. 오브젝트만 지우면 실제 게임에서 안 일어나는 일을 재게 된다.
- 측정용 임시 코드는 커밋하지 않는다. 되돌리는 커밋이 따라붙어 기록의 증거 가치가 떨어진다.
- 예상은 재기 전에 적는다. 맞히려는 게 아니라 어디서 틀렸는지 보려고 적는다.
- 오브젝트를 찍어내는 비용 대부분은 C# 관리 메모리 밖에서 처리된다.
- 네 배를 올렸는데 값이 그대로면 그 값은 그것과 무관하다.
- 값이 안 변하면 대상보다 자를 먼저 의심한다. 464 B는 좀비를 하나도 안 만들어도 나왔다.
- 한 번 본 값은 값이 아니다. 12.7 KB 스파이크는 재현이 안 돼서 뺐다.
- 모르는 건 모른다고 적는다. 2·3단계가 같아진 원인은 끝내 규명하지 못했다.
- 모든 수치는 에디터 기준이다. 배포용 빌드에서는 다시 재야 한다.
- 기각이 아니라 유보다. 투사체를 넣는 순간 조건이 성립한다.
참고 자료
- Unity 6000.0 Manual - Memory Profiler / GC.Alloc
- Unity 6000.0 Manual - Physics2D.OverlapCircleAll
- Unity 6000.0 Manual - MonoBehaviour.Invoke
- Unity 6000.0 Manual - WaitForSeconds
- 왕복 부하 실험 기록 - 0031
- 마감으로 잘라낸 범위 - 0017
- 지난 편 - 유니티 학습 프로젝트 < 15 >
한줄 평
- 왕복을 네 배로 올렸는데 값이 그대로였는데, 대상이 아니라 자를 의심하기까지 한나절이 걸렸다..