시작하기

어쩌다보니 당찮게도 모바일 서비스 스타트업에 CTO(라고 쓰고 ‘기술 잡부’라고 읽는다) 타이틀로 합류한지 몇 달 되었습니다. 그런데 함정은 제가 합류 당시 모바일 개발 경험이 전혀 없었다는 것입니다. 모바일 서비스 스타트업에 모바일 개발 경험이 없는 CTO라니… 게다가 전 Objective-C 혐오증을 심하게 앓고 있습니다. 하지만 미국 시장이 주 타겟이라 iPhone을 버릴 수는 없었으며 HTML5 성능은 부족하고 네이티브 UI 성능이 필요했기에 결국 선택은 Xamarin 밖에 없었습니다.

Xamarin 중에서도 크로스 플랫폼 도구인 Xamarin Forms를 사용하고 있습니다.

Xamarin

수개월 동안 초보 모바일 개발자의 고난 끝에 드디어 지난 주 야심차게 iTunes Connect에 앱 리뷰 요청을 보냈습니다. 부푼 기대를 안고 기다리던 중 오늘 아침에 승인 거절이라는 좌절스러운 피드백을 받았습니다.

Your app crashed on launch on the following device(s) so we were unable to review it…

5주 가량 10명 정도의 인원으로 AdHoc 빌드의 내부 테스트 과정을 거쳤는데 응용프로그램이 시작과 동시에 죽는다니! 애플측에서 보내온 로그에 이런 단서가 있었습니다.

<Warning> Unhandled managed exception: An exception was thrown while invoking the constructor ‘Void .ctor()’ on type ‘MainViewModel’. —> Attempting to JIT compile method ‘System.Reactive.Concurrency.LocalScheduler:Schedule<System.Reactive.Producer`1/State<System.Security.Principal.IPrincipal>> (System.Reactive.Producer`1/State<System.Security.Principal.IPrincipal>,System.Func`3<System.Reactive.Concurrency.IScheduler, System.Reactive.Producer`1/State<System.Security.Principal.IPrincipal>, System.IDisposable>)’ while running with –aot-only. See http://docs.xamarin.com/ios/about/limitations

MainViewModel 인스턴스를 생성하는 과정에서 JIT(just-in-time) 컴파일을 시도했다는 내용입니다. 지금까지 AdHoc 빌드와 AppStore 빌드의 차이점이 배포 프로파일 외에는 없다고 생각해서 AppStore 빌드를 단 한 번도 테스트하지 않았었던 것이 초보 모바일 개발자의 치명적 실수였습니다. AppStore 빌드로 디바이스에 배포해 실행해보니 바로 응용프로그램이 크래시되었고 로그를 열어본 결과 크래시 지점이 리뷰 로그와 정확하게 일치했습니다. 이제 의문점은 두 가지입니다. JIT이 왜 문제되는가? 그리고 왜 AdHoc 빌드에서는 문제되지 않았나?

 

Full AOT(ahead-of-time)

로그에 포함된 링크를 읽어보면 첫 질문에 대한 답을 얻을 수 있습니다. 중요점을 요약하면 이렇습니다.

  1. 일반적인 .NET/ Mono 코드와는 달리 iPhone 코드는 정적 컴파일만 가능하다.
  2. 그렇기 때문에 모든 코드는 Mono의 AOT를 통해 네이티브 바이너리로 빌드된다.
  3. AOT는 일반적인 .NET/ Mono 코드에 비해 한계점을 가진다.

여기서 .NET의 제네릭과 값 형식 대해 언급할 필요가 있습니다. .NET의 제네릭은 컴파일러와 런타임 모두의 지원을 받아 개별 형식으로 빌드되어 성능 이점을 가집니다. 컴파일러에 의해 정적 빌드되지 않은 제네릭 형식이 .NET 런타임에 의해 빌드되기도 하고 제네릭 매개변수로 값 형식이 사용될 수도 있습니다. 참조 형식에 대해서는 메모리 사용이 동일하기 때문에 하나의 물리적인 형식이 사용되지만 값 형식이 관련된 경우는 각각의 형식이 빌드됩니다.

크래시된 코드의 경우 Rx(Reactive Extensions) 코드에서 JIT 컴파일이 발생했는데 이 때 값 형식이 연관됩니다.

internal abstract class Producer<TSource> : IProducer<TSource>, IObservable<TSource>
{
    ...

    private struct State
    {
        ...
    }

    ...
}

위 형식이 제네릭 매개변수인 제네릭 메서드가 AOT에 의해 정적 빌드되지 않고 JIT 컴파일 시도되어 오류가 발생한 것이죠. 그러면 왜 이 메서드가 AppStore 빌드에서만 정적 빌드되지 않는 것일까요?

Generic Value Type Sharing

AdHoc 빌드와 AppStore 빌드 로그를 비교한 결과 딱 한 가지 차이점을 발견했습니다. 다른 내용은 모두 동일했지만 아래 인수가 AppStore 빌드에서만 컴파일러에 전달되고 있었죠.

.../mtouch ... --gsharedvt=false

--gsharedvt 인수는 Generic Value Type Sharing 활성화 여부를 지정하는 데에 사용됩니다. 이 기능을 통해 AOT 컴파일러가 값 형식과 관련된 제네릭 형식을 만들 수 있고 코드 공유를 통한 최적화 효과도 가진다고 합니다. 기본 값은 true입니다.

그런데 어떤 이유에서인지 AppStore 빌드에서는 --gsharedvt 인수가 명시적으로 false로 지정되고 있었습니다. 이 인수를 설정할 수 있는 별도의 인터페이스는 발견하지 못했고 추가 인수를 지정하는 곳에 직접 입력했습니다.

--gsharedvt=true

다시 AppStore 빌드를 디바이스에서 구동시켜보니 정상적으로 동작합니다. 추가적인 영향에 대해서는 테스트를 더 해봐야겠지만 일단 새 빌드를 제출했습니다.

결론

Generic Value Type Sharing은 JIT을 사용할 수 없는 iPhone 환경에서 값 형식과 연관된 제네릭 형식이 AOT 빌드되게 도와줍니다. 값 형식과 사용되는 제네릭 형식 또는 제네릭 메서드로 인해 “Attempting to JIT compile method” 런타임 오류가 발생할 경우 --gsharedvt 컴파일러 인수를 확인하고 Generic Value Type Sharing을 활성화해 테스트해 볼 수 있습니다.