· 11 min read
Unity 6.7 vs Unity 7: IL2CPP와 CoreCLR Unity 6.7 vs Unity 7: IL2CPP와 CoreCLR
MatterKit과 Spill로 잰 Editor, Player, 빌드 MatterKit과 Spill로 잰 Editor, Player, 빌드
같은 프로젝트를 Unity 6.7(6000.7.0b2)과 Unity 7 알파 9(7000.0.0a9)로 열고 같은 작업을 시켰다. 대상은 Tessel Works의 물리·파단 라이브러리 MatterKit이다. 연산과 할당이 많고 여러 스레드에서 돌아서 런타임 차이가 잘 드러난다. 실제 게임 Spill로도 확인했다.
Unity 7 alpha 9 vs Unity 6.7 at a glance
Time ratios: below 1× means Unity 7 is faster. Windows, MatterKit. Purple: Unity 7 wins
- Editor C#
- 0.47–0.63× (Unity 7 wins)
- time vs 6.7, Release and Debug
- Script edit to reload
- 0.94–1.10×
- unchanged (Mac 1.34–1.67×)
- Editor memory
- +283 MB
- per Play entry, Entities
- CoreCLR Player, long runs
- 0.82–1.02× (Unity 7 wins)
- time vs Unity 7 IL2CPP
- CoreCLR Player, first run
- 1.37–1.70×
- slower: JIT compiles first
- CoreCLR clean build
- 40 s (Unity 7 wins)
- same as 6.7 Mono; IL2CPP takes 168–178 s
Editor 안 C#은 2배 가까이 빨라졌다. Player는 오래 돌면 CoreCLR이, 처음 한 번은 IL2CPP가 앞선다. 이 글의 막대는 모두 시간 비다. 1보다 작으면 Unity 7 쪽이 빠르다.
어떻게 쟀나
직접 만든 unity-bench(v0.7.0, MIT)로 쟀다. 비교 대상마다 새 프로세스를 띄우고 순서를 번갈아 섞었다. 프로세스별 중앙값을 낸 뒤 그 값들의 중앙값과 범위를 비교했다. 핵심 항목은 프로세스 6개, 나머지는 2~4개다.
| Windows (CI 러너 PC) | Mac | |
|---|---|---|
| CPU | AMD Ryzen 9 7900X3D, 12코어 24스레드 | Apple M4 Max, 16코어(성능 12, 효율 4) |
| 메모리 | 64 GB DDR5-4800 | 48 GB |
| 저장 장치 | NVMe SSD (Samsung PM9A1 2 TB) | 내장 SSD |
| GPU | GeForce RTX 4080 | M4 Max 내장 |
| OS | Windows 11 Pro 25H2 (26200), 고성능 전원 | macOS 26.5.2 |
| Unity | 6000.7.0b2, 7000.0.0a9 | 같음 |
- Player 숫자는 Windows PC 한 대의 값이다. Mac에서는 Editor와 빈 프로젝트 측정만 했다.
- 기술 통계이지 유의성 검정이 아니다. 5% 안팎의 차이는 방향만 본다.
- Editor는 batchmode로 쟀다. 창이 있는 Editor의 체감은 재지 않았다.
- 6.7과 7은 기본 라이브러리가 달라 같은 입력에서도 물리 결과가 2~4% 갈린다.
Editor: C#은 2배 가까이 빨라졌다
Unity 7 Editor는 Mono가 아니라 CoreCLR(.NET 10) 위에서 돈다.
- MatterKit의 순수 C# 시나리오 60개 중 55개에서 Unity 7의 가장 느린 프로세스가 6.7의 가장 빠른 프로세스보다 빨랐다. 7의 Debug가 6.7의 Release와 거의 같은 속도다.
- Play 진입이 빨라진 것은 대부분 샘플이 Play마다 카탈로그를 굽는 C# 코드 몫이다.
- Mac(M4 Max)은 C#이 더 빨라지고, reload는 오히려 1.3~1.7배 느리다.
reload가 그대로인 이유는 이전 코드를 내리는 단계다. Mono는 AppDomain을 통째로 버리고, CoreCLR은 collectible AssemblyLoadContext를 내린다. 이 단계가 484 ms에서 862 ms로 1.8배가 되어 어셈블리 로드에서 번 시간을 먹는다. TypeCache 스캔과 [InitializeOnLoad] 처리도 2배 안팎 느려졌다.
Player: 오래 돌면 CoreCLR, 처음 한 번은 IL2CPP
6.7 IL2CPP, 7 IL2CPP, 7 CoreCLR 세 가지로 빌드했다.
- 7 IL2CPP는 6.7과 거의 같다. 첫 자르기 2.07배와 catalog 1.17배는 아래 함정의
Vector3문제다. - 7 CoreCLR은 오래 도는 작업에서 대체로 빠르고, 병을 여러 개 동시에 깰 때 차이가 가장 크다. 예외도 있다. 저금통 하나는 1.8배, 집 모형 자르기는 3분을 돌려도 1.5~1.7배 느리다.
- 처음 한 번은 CoreCLR이 확실히 느리다. 처음 실행되는 코드를 그때 JIT로 컴파일한다. Unity 7은 .NET 기본값(tiered compilation, Dynamic PGO)을 그대로 쓰고, ReadyToRun 어셈블리는
System.Private.CoreLib하나뿐이다. 출시 빌드라면 로딩 중에 주요 코드 경로를 한 번씩 돌려 두면 줄일 수 있다. 자세한 것은 CoreCLR은 왜 오래 돌면 빠른가에 적는다.
여러 스레드가 함께 할당하면
IL2CPP의 Boehm GC는 Unity 빌드에서 스레드별 할당 버퍼가 없다. 그래서 객체 하나를 만들 때마다 프로세스 전역 잠금을 잡는다. CoreCLR은 스레드마다 버퍼가 있어서 포인터만 민다.
Allocation throughput by thread count
Each thread allocates 32-byte objects. Million per second. Mac M4 Max, Release players, median of 7 processes
7 CoreCLR381 M/s, 1.9× of 1 thread
6.7 and 7 IL2CPP36 to 39 M/s, 0.84×
| Threads | Unity 6.7 IL2CPP | Unity 7 IL2CPP | Unity 7 CoreCLR |
|---|---|---|---|
| 1 | 42.8 | 46.3 | 196.9 |
| 2 | 40.9 | 42.9 | 297.6 |
| 4 | 38.6 | 40.8 | 396.6 |
| 8 | 36.0 | 38.9 | 380.9 |
- 할당을 32번에 한 번으로 줄여도 IL2CPP는 8스레드가 1스레드보다 느렸다(0.42배). 할당 없는 계산은 세 런타임 모두 7.5배 안팎으로 늘었다.
- 병 깨짐에서도 보인다. 향수병 하나에서 넷으로 갈 때 시간이 IL2CPP는 2.7배, CoreCLR은 2.0배 늘었다.
- 다만 MatterKit은 커터 스레드를 IL2CPP에서 4개, CoreCLR에서 8개 쓴다. 각자의 기본값 비교다.
- 한 스레드에서만 할당하거나 Burst job처럼 관리 객체를 만들지 않는 코드에서는 차이가 거의 없다.
두 GC의 구조는 Boehm과 CoreCLR, 두 GC는 어떻게 다른가에 적는다.
빌드: IL2CPP만 느리다
클린 빌드는 CoreCLR이 40초, IL2CPP가 168~178초다. 그런데 6.7 Mono도 43초로 같다(번갈아 잰 따로 측정에서 42.6초 대 41.3초, 스크립트 수정 뒤 21.5초 대 20.7초). 빌드가 빠른 건 CoreCLR이라서가 아니라 C++로 변환하고 컴파일하는 단계가 없어서다.
Editor 메모리: Unity 누수는 고쳐졌고, Entities 경로는 남았다
Editor memory over repeated reloads
30 script edits, each followed by a reload. App Resident memory, Mac, small example project (not the MatterKit sample). Unity's own leak, fixed in alpha 9
Alpha 7 4.2 GB+122 MB per reload
| Reloads | Unity 7 alpha 7 (CoreCLR) | Unity 7 alpha 9 (25 reloads) | Unity 7 alpha 7 + two caches cleared (15 reloads) | Unity 6.7 (Mono) |
|---|---|---|---|---|
| 0 | 0.58 GB | 0.59 GB | 0.47 GB | 0.40 GB |
| 1 | 0.70 GB | 0.61 GB | 0.54 GB | 0.42 GB |
| 2 | 0.81 GB | 0.62 GB | 0.55 GB | 0.44 GB |
| 3 | 0.93 GB | 0.62 GB | 0.55 GB | 0.44 GB |
| 4 | 1.05 GB | 0.62 GB | 0.56 GB | 0.44 GB |
| 5 | 1.16 GB | 0.62 GB | 0.56 GB | 0.44 GB |
| 6 | 1.28 GB | 0.62 GB | 0.56 GB | 0.44 GB |
| 7 | 1.40 GB | 0.62 GB | 0.56 GB | 0.44 GB |
| 8 | 1.54 GB | 0.63 GB | 0.56 GB | 0.44 GB |
| 9 | 1.67 GB | 0.63 GB | 0.56 GB | 0.44 GB |
| 10 | 1.80 GB | 0.63 GB | 0.56 GB | 0.44 GB |
| 11 | 1.90 GB | 0.63 GB | 0.56 GB | 0.44 GB |
| 12 | 2.03 GB | 0.63 GB | 0.56 GB | 0.44 GB |
| 13 | 2.14 GB | 0.63 GB | 0.56 GB | 0.44 GB |
| 14 | 2.25 GB | 0.63 GB | 0.57 GB | 0.44 GB |
| 15 | 2.37 GB | 0.64 GB | 0.57 GB | 0.44 GB |
| 16 | 2.49 GB | 0.64 GB | – | 0.44 GB |
| 17 | 2.62 GB | 0.64 GB | – | 0.44 GB |
| 18 | 2.72 GB | 0.64 GB | – | 0.44 GB |
| 19 | 2.85 GB | 0.64 GB | – | 0.44 GB |
| 20 | 2.95 GB | 0.64 GB | – | 0.44 GB |
| 21 | 3.09 GB | 0.66 GB | – | 0.44 GB |
| 22 | 3.18 GB | 0.57 GB | – | 0.45 GB |
| 23 | 3.32 GB | 0.57 GB | – | 0.45 GB |
| 24 | 3.44 GB | 0.57 GB | – | 0.45 GB |
| 25 | 3.55 GB | 0.57 GB | – | 0.45 GB |
| 26 | 3.67 GB | – | – | 0.45 GB |
| 27 | 3.80 GB | – | – | 0.45 GB |
| 28 | 3.92 GB | – | – | 0.45 GB |
| 29 | 4.04 GB | – | – | 0.45 GB |
| 30 | 4.16 GB | – | – | 0.45 GB |
그림은 더 작은 예제 프로젝트의 값이다. 알파 7만 reload마다 늘고(+122 MB), 알파 9는 6.7처럼 평평하다.
- 알파 7은 reload마다 이전 코드를 붙잡았다. MatterKit 샘플은 한 번에 230 MB씩 늘어 30번 만에 8.2 GB가 됐다. 원인은 Unity 내부 캐시 두 곳이었고, 알파 9에서 고쳐졌다. 릴리스 노트에는 없는 수정이다.
- Entities를 쓰는 프로젝트는 알파 9에서도 domain reload를 켠 Play 진입마다 커진다. MatterKit 샘플은 한 번에 managed 힙 +102 MB, 프로세스 +283 MB다(Windows). Play 진입 때 domain reload를 끄면 늘지 않는다.
원인을 찾아간 과정은 Unity 7 Editor 메모리 누수를 쫓아서에 적는다.
실제 게임에서도 같았나: Spill
Spill은 유리병이 계단을 굴러 깨지는 물리 게임이다. Entities, Unity Physics, URP, 녹화 테이프 재생을 다 쓴다. 같은 PC, 같은 두 Editor로 하룻밤 6시간을 쟀다.
- Editor는 같은 모양이다. 어셈블리 19개를 다시 컴파일하는 수정만 18% 느렸다. 이전 코드를 내리는 단계가 5.3배 걸렸다.
- 메모리는 더 나빴다. Play 한 번에 +269 MB(MatterKit +283 MB)이고, 스크립트 reload마다 110 MB가 더 붙는다(MatterKit -5 MB). Mac에서는 패키지 두 곳(
com.unity.pipeline,com.unity.ide.visualstudio)이 이전 코드를 붙잡았다. Windows 원인은 확인 중이다. - 7 IL2CPP가 43% 빨랐다. GC가 아니었다. MatterKit이 소리를 내려고 접촉을 샘플링하는 job 하나가 managed 코드로 돌았고, 6.7 IL2CPP에서 특히 느렸다. Burst로 바꾸면 차이가 거의 사라진다. Spill main은 이미 Burst가 기본이다.
- CoreCLR은 오래 돌면 p99가 7 IL2CPP의 0.39배다. 실행에서 첫 프레임까지는 1.67배 걸렸다. 클린 빌드는 6.7 IL2CPP 225초, 7 IL2CPP 250초, CoreCLR 90초다.
알아 둘 함정
재현과 원인은 Unity 7 IL2CPP에서 밟은 함정들에 적는다.
- Unity 7 IL2CPP의
System.Numerics.Vector3: 같은 계산이 6.7 IL2CPP의 4.7배 걸린다. 알파 9에서도 그대로다. - 6.7 Editor(Mono)의 float: double로 계산해서
Math.Floor(-0.17f / 0.005f)가 Player와 한 칸 다르게 나온다. - 알파 9 IL2CPP 링커: 빌드에서 뺀 어셈블리도
[AlwaysLinkAssembly]가 붙어 있으면 링크한다. Spill의 7 IL2CPP 빌드가 이것으로 실패했다. - IL2CPP 종료 crash: 스레드 풀 작업이 도는 중에 끝내면 6.7과 알파 9 모두 죽는다(Spill 20번 중 6번).
그래서 무엇을 쓸까
PC 게임에서 IL2CPP를 고른 가장 큰 이유는 구형 Mono보다 빨라서였다. Unity 7의 CoreCLR은 그 Mono 자리를 현대 .NET으로 바꾼다. PC 출시 백엔드로 CoreCLR을 진지하게 볼 이유가 생겼다.
CoreCLR or IL2CPP for a PC release
Concept diagram of where each backend wins in this benchmark. Unity 7 alpha 9, so a direction, not a verdict
CoreCLR
- Long game loops mostly faster
- Clean builds about 4× faster than IL2CPP (same as Mono)
- Scales when many threads allocate
IL2CPP
- First run of new code is faster
- Native code, harder to decompile
- Required on consoles and iOS (no JIT)
Either way, physics results differ slightly by runtime. Bake replays and data with the one you ship.
알파 9 기준이라 결론이라기보다 방향이다. Spill은 출시까지 반년 이상 남아 Unity 7으로 올린다. 백엔드는 출시 전에 같은 방법으로 다시 재서 정한다.