CoreCLR 이 와도 모바일은 그대로다
Esc

    CoreCLR 이 와도 모바일은 그대로다

    –Unity최적화GC

    앞의 두 편은 Boehm 을 그대로 두고 할 수 있는 일이었습니다. 쪼개고, 미루고, 잠깐 끄는 것입니다. GC 자체가 나아지지는 않았습니다.

    유니티가 지금 하는 것은 다른 쪽입니다. 런타임을 통째로 갈아 끼웁니다.

    Mono 를 걷어내고 CoreCLR 을 넣습니다. 요즘 .NET 이 쓰는 그 런타임입니다. 6.7 에서 실험적으로 들어왔고 6.8 에서 Mono 가 없어집니다.

    그런데 이 글에서 제일 중요한 문장은 좋아지는 쪽이 아닙니다.

    IL2CPP 는 이 변화의 영향을 받지 않습니다. 계속 Boehm 을 씁니다.

    무엇이 달라지나

    CoreCLR 의 GC 는 앞에서 본 Boehm 의 “안 하는 것” 세 개를 전부 합니다.

    Boehm (Mono·IL2CPP) CoreCLR
    주소를 아는가 보수적(conservative). 주소처럼 생기면 주소로 침 정밀(precise). 무엇이 참조인지 정확히 앎
    객체를 옮기는가 비압축(non-compacting). 빈틈이 남음 압축(compacting). 살아남은 것을 붙여서 빈틈을 없앰
    나이를 따지는가 비세대(non-generational). 매번 전체를 훑음 세대(generational). 방금 만든 것만 자주, 싸게

    셋이 서로 물려 있습니다. 정확히 아니까 옮길 수 있고, 옮길 수 있으니 세대로 나눌 수 있습니다. Boehm 이 셋을 다 못 한 것도 같은 이유입니다. 첫 단추가 없으면 나머지가 안 됩니다.

    유니티는 이걸 “much less work” 라고 표현합니다. GC 가 할 일이 줄어서 프레임에 남는 시간이 늘어난다고요.

    실제로 체감되는 것은 이쯤입니다.

    • 짧게 살다 죽는 객체가 싸집니다. 세대 덕입니다. 매 프레임 만들고 버리는 것들이 대부분 여기 해당합니다
    • 조각이 안 쌓입니다. 옮기니까요. 빈 메모리가 있는데 힙이 커지는 일이 줄어듭니다
    • 잘못된 보존이 사라집니다. 정수를 주소로 착각할 일이 없습니다

    그래서 이 시리즈가 필요 없어지나

    아닙니다. 두 가지 때문입니다.

    첫째, 바닥은 안 바뀝니다. 할당이 GC 를 부르는 것도, 뿌리에서 손이 닿는 것만 사는 것도 그대로입니다. 세대 GC 도 결국 할당이 임계치를 넘기면 돕니다. 덜 아플 뿐입니다. 그리고 오래 사는 객체가 많으면 2세대 수집이 돌고, 그건 여전히 비쌉니다.

    둘째, 지금 만드는 게임이 모바일이라면 이 글은 남 얘기입니다.

    IL2CPP 는 빠져 있다

    유니티 문서가 명시합니다. IL2CPP 는 이 변화의 영향을 받지 않고, IL2CPP 가 쓰는 Boehm GC 의 동작에는 변경이 없다고요.

    CoreCLR 은 JIT 입니다. 실행하면서 기계어로 바꿉니다. 그런데 iOS 같은 플랫폼은 실행 중에 코드를 만들어 내는 것을 허용하지 않습니다. 그래서 미리 다 컴파일해 두는 IL2CPP 가 필요했고, 그 사정은 CoreCLR 이 와도 그대로입니다.

    정리하면 이렇게 갈립니다.

    • 데스크톱 · 콘솔 일부 → CoreCLR. 세대 GC 가 옵니다
    • 모바일 → IL2CPP. 계속 Boehm 입니다. 세대 GC 는 안 옵니다

    모바일 게임을 만들고 있다면 앞의 두 편이 앞으로도 계속 내 이야기입니다. “곧 좋아진다” 는 글을 보고 대응을 미루면 안 됩니다.

    같은 프로젝트를 데스크톱과 모바일로 둘 다 빌드한다면 한쪽에서만 재는 것도 위험합니다. 에디터에서 멀쩡한 것이 기기에서 튈 수 있습니다. 지금도 그렇지만 앞으로는 GC 종류까지 달라집니다.

    지금 무엇을 하면 되나

    바꿀 것이 별로 없다는 게 답입니다. 어느 GC 든 할당을 줄이는 것이 이득이기 때문입니다.

    앞 시리즈에서 정리한 것이 그대로 유효합니다. 박싱을 안 하고, 문자열을 반복문에서 잇지 않고, 매 프레임 도는 자리에 LINQ 를 안 쓰고, 만든 것을 다시 쓰는 것입니다.

    CoreCLR 로 옮길 준비로 지금 할 만한 것은 따로 있습니다.

    • 재 두기. 지금 프레임당 할당이 얼마인지 기록해 두면 옮긴 뒤에 비교할 수 있습니다
    • Reflection.Emit 이나 dynamic 을 쓰고 있다면 확인. IL2CPP 에서는 원래 안 되는 것들입니다
    • 부동소수점 결과가 미세하게 달라질 수 있다는 것. 리플레이나 결정론적 로직을 쓴다면 확인이 필요합니다

    세대 GC 자체가 궁금하다면

    CoreCLR 의 GC 는 일반 .NET 의 그것과 같은 것입니다. 0세대·1세대·2세대가 어떻게 나뉘고 왜 그게 싸게 먹히는지, 큰 객체는 왜 따로 관리되는지는 C# 쪽 글에 정리해 뒀습니다.

    그 글을 쓸 때는 “유니티는 다르다” 는 단서를 달아야 했는데, CoreCLR 로 가면 그 단서가 없어집니다. 데스크톱 한정으로요.

    정리

    • 유니티가 Mono 를 걷어내고 CoreCLR 을 넣습니다. 6.8 에서 Mono 가 없어집니다
    • CoreCLR 의 GC 는 정밀하고, 옮기고, 세대를 나눕니다. Boehm 이 안 하던 셋을 전부 합니다
    • 짧게 살다 죽는 객체가 싸지고, 조각이 안 쌓입니다
    • IL2CPP 는 이 변화에서 빠져 있습니다. 계속 Boehm 입니다
    • 그래서 모바일에는 세대 GC 가 오지 않습니다. 앞의 두 편이 계속 유효합니다
    • 어느 쪽이든 할 일은 같습니다. 할당을 줄이는 것입니다