[ndc 2016] 유니티, ios에서 linq 사용하기

46
유니티 iOS에서 LINQ 사용하기 내 코드가 이렇게 간결할 리 없어 넥슨 지티 김대희

Upload: daehee-kim

Post on 23-Jan-2018

5.346 views

Category:

Software


1 download

TRANSCRIPT

Page 1: [NDC 2016] 유니티, iOS에서 LINQ 사용하기

유니티 iOS에서 LINQ 사용하기내 코드가 이렇게 간결할 리 없어

넥슨 지티

김대희

Page 2: [NDC 2016] 유니티, iOS에서 LINQ 사용하기

발표자

김대희 게임 프로그래머 4년 차

선데이토즈, 넥슨 코리아를 거쳐

현재 넥슨 지티에서 신규 모바일 게임 제작 중

Page 3: [NDC 2016] 유니티, iOS에서 LINQ 사용하기

뭐가 문제에요?

유니티에서 LINQ를 쓰고 싶어요.

Page 4: [NDC 2016] 유니티, iOS에서 LINQ 사용하기

LINQLanguage-Integrated Query

데이터 저장소 종류와 관계 없이 저장소에 일관성 있는 인터페이스로 질의

LINQ to Objects, LINQ to SQL, LINQ to XML ...

본 세션에서는 LINQ to Objects 관련 내용만을 다룹니다.

Page 5: [NDC 2016] 유니티, iOS에서 LINQ 사용하기

LINQ

int[] numbers = { 5, 10, 8, 3, 6, 12 };

Query syntaxvar results = from num in numbers

where num % 2 == 0orderby numselect num * 2;

foreach(var result in results)Console.WriteLine(result);

Method syntaxvar results = numbers

.Where(num => num % 2 == 0)

.OrderBy(num => num)

.Select(num => num * 2);

foreach(var result in results)Console.WriteLine(result);

Page 6: [NDC 2016] 유니티, iOS에서 LINQ 사용하기

LINQ 사용 예시

정말 간결할까요?

Page 7: [NDC 2016] 유니티, iOS에서 LINQ 사용하기

기획서의 요구사항1) 퀘스트들의 제목을 출력해주세요.

2) 입장한 맵에 해당되는 퀘스트들만 보여줍니다.

3) 현재 진행 중이어야 하고요.

4) 진행 상태가 높은 퀘스트를 가장 먼저 출력합니다.

5) 진행 상태가 같은 경우 퀘스트 아이디가 낮은 순서로 출력합니다.

Page 8: [NDC 2016] 유니티, iOS에서 LINQ 사용하기

기획서의 요구사항List<Quest> results = new List<Quest>();foreach (Quest quest in quests){

if (quest.IsActive){

if (quest.MapIndex == currentMapIndex)results.Add(quest);

}}

results.Sort((x, y) =>{

int compareValue = y.Progress.CompareTo(x.Progress);if (compareValue == 0)

return x.ID.CompareTo(y.ID);return compareValue;

});

foreach (Quest quest in results)Console.WriteLine(quest.Title); Non LINQ

Page 9: [NDC 2016] 유니티, iOS에서 LINQ 사용하기

기획서의 요구사항var results = quests

.Where(quest => quest.IsActive)

.Where(quest => quest.MapIndex == currentMapIndex)

.OrderByDescending(quest => quest.Progress)

.ThenBy(quest => quest.ID);

foreach (Quest quest in results)Console.WriteLine(quest.Title);

참 쉽죠?

LINQ

Page 10: [NDC 2016] 유니티, iOS에서 LINQ 사용하기

LINQ동일한 인터페이스 사용 - 모든 컬렉션 류, 그 외 다른 종류의 저장소에도 동일함.

다양하고 편리한 기능 - Max, Min, Average, OrderBy, First, FirstOrDefault, Last, LastOrDefault …

지연 실행 - 성능 이점

간결한 High Level 코드 품질 - 유지 보수 용이

컴파일 타임 에러 체크 – SQL 시 문자열 쿼리의 체크를 컴파일 타임에 가능

Page 11: [NDC 2016] 유니티, iOS에서 LINQ 사용하기

LINQLINQ는 느리다?

LINQ (to Objects)는 충분히 빠르다

Page 12: [NDC 2016] 유니티, iOS에서 LINQ 사용하기

LINQ

Galaxy S3 100개 1,000개 10,000개 100,000개

Non LINQ 0 ms 0 ms 4 ms 42 ms

LINQ 0 ms 2 ms 26 ms 250 ms

iPhone 6+ 100개 1,000개 10,000개 100,000개

Non LINQ 0 ms 0 ms 3 ms 16 ms

LINQ 0 ms 1 ms 10 ms 37 ms

‘기획서의 요구사항’ 성능 테스트

Page 13: [NDC 2016] 유니티, iOS에서 LINQ 사용하기

LINQ디바이스 성능은 계속 발전

- 차이가 점점 좁혀진다.

80-20 법칙

- 최적화가 필요한 곳만 코드를 풀어 쓰자

클라이언트 사이드에서 대용량의 데이터를 사용하는 곳은 많지 않다.

- 100,000개의 요소를 가진 컬렉션은 거의 없다.

Page 14: [NDC 2016] 유니티, iOS에서 LINQ 사용하기

왜 못썼나요?

지금부터 살펴 봅시다.

Page 15: [NDC 2016] 유니티, iOS에서 LINQ 사용하기

LINQ on iOSList<string> strs = new List<string> { “0”, “1”, “2”, “3” };

string first = strs.FirstOrDefault ();

Debug.Log(first); // “0"

문제 없음

Page 16: [NDC 2016] 유니티, iOS에서 LINQ 사용하기

LINQ on iOSList<int> numbers = new List<int> { 0, 1, 2, 3 };

int first = numbers.FirstOrDefault ();

Debug.Log(first.ToString());

ExecutionEngineException: Attempting to JIT compile method

'System.Linq.Enumerable/PredicateOf`1<int>:.cctor ()' while running with --aot-only.

Rethrow as TypeInitializationException: An exception was thrown by the type initializer for

PredicateOf`1

Page 17: [NDC 2016] 유니티, iOS에서 LINQ 사용하기

LINQ on iOS

LINQ 코드는 System.Core.dll에 존재하여 디버깅도 못해보고

많은 Unity 사용자들은 LINQ 사용을 포기

Page 18: [NDC 2016] 유니티, iOS에서 LINQ 사용하기

Analyzation

LINQ 소스가 있는 System.Core.dll를 디컴파일

앞서 문제가 있었던 FirstOrDefault 메소드 관련 부분만 발췌

Page 19: [NDC 2016] 유니티, iOS에서 LINQ 사용하기

public static class Enumerable{

public static TSource FirstOrDefault<TSource>(this IEnumerable<TSource> source){

return First (source, PredicateOf<TSource>.Always, Fallback.Default);}

private static TSource First<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate, Fallback fallback){

foreach (TSource source1 in source){

if (predicate(source1))return source1;

}if (fallback == Fallback.Throw)

throw new InvalidOperationException();return default(TSource);

}

private enum Fallback{

Default,Throw,

}

private class PredicateOf<T>{

public static readonly Func<T, bool> Always = (t => true);}

}

IEnumerable<int> source

Func<int, bool> predicateIEnumerable<int> source

PredicateOf<int>.Always 이 녀석이 문제

Page 20: [NDC 2016] 유니티, iOS에서 LINQ 사용하기

public static class Enumerable{

public static TSource FirstOrDefault<TSource>(this IEnumerable<TSource> source){

return First (source, PredicateOf<TSource>.Always, Fallback.Default);}

private class PredicateOf<T>{

public static readonly Func<T, bool> Always = (t => true);}

}

예외가 발생하지 않는다.

void _unusedMethod() // type hint, 해당 메소드는 어디서도 호출 되지 않습니다.{

new PredicateOf<int>();}

Page 21: [NDC 2016] 유니티, iOS에서 LINQ 사용하기

C# 실행 환경

C#Source Code

IL(Intermediate Language)

Machine Code

C# Compiler JIT Compiler

RuntimeCompile time

C#Source Code

IL(Intermediate Language)

Machine Code

C# Compiler AOT Compiler

Compile time

JIT

AOT iOS에서의 구동환경

Page 22: [NDC 2016] 유니티, iOS에서 LINQ 사용하기

Mono AOT LimitationsGeneric 과 관련하여 특정 상황에서

Generic Type Parameter가 Value Type일 때 한계를 보임

Page 23: [NDC 2016] 유니티, iOS에서 LINQ 사용하기

Generic Type ParameterList<int>, List<string>, List<float>

위 세 타입은 전부 다른 타입

각기 다른 타입이기에 전부 다른 Machine Code를 생성해야 하지만...

Page 24: [NDC 2016] 유니티, iOS에서 LINQ 사용하기

Generic Code ExplosionGeneric Type Parameter마다 각기 다른 타입에 대해

Machine Code를 생성하게 되면 코드 양 급증

성능에 부정적인 영향

Page 25: [NDC 2016] 유니티, iOS에서 LINQ 사용하기

Generic Sharing

List<int>

List<int>.Add

List<int>.Remove

List<float>

List<float>.Add

List<float>.Remove

List<string>

List<string>.Add

List<string>.Remove

List<File>

List<File>.Add

List<File>.Remove

Value Type Reference Type

구현에 따라 몇 몇 Value Type은 공유 될 수도 있다. ex) int, enum

Page 26: [NDC 2016] 유니티, iOS에서 LINQ 사용하기

Generic SharingAOT의 Generic Type Parameter가 Value Type일 때만

문제가 발생하는 이유는 이와 같이 다른 메커니즘을 지니기 때문

또한 상대적으로 Reference Type에 비해 Value Type은

Generic Type Parameter 마다 코드를 생성해야 하기 때문에

AOT의 경우 코드가 누락될 가능성이 생김.

이 페이지의 내용은 발표자의 가설입니다.

Page 27: [NDC 2016] 유니티, iOS에서 LINQ 사용하기

ExceptionExecutionEngineException: Attempting to JIT compile method

'System.Linq.Enumerable/PredicateOf`1<int>:.cctor ()' while running with --aot-only.

Rethrow as TypeInitializationException: An exception was thrown by the type initializer for

PredicateOf`1

System.Linq.Enumerable/PredicateOf<int>의 Class Constructor(cctor) 메소드를 찾을 수 없어

JIT Compile를 시도하다가 난 예외

Page 28: [NDC 2016] 유니티, iOS에서 LINQ 사용하기

Exception몇 몇 복잡한 상황에서 AOT 컴파일러가 Generic 타입을 캐치하지 못한 채 실행 파일을 생성.

따라서 이전에 언급한 구체적인 타입에 대한 힌트를 주게 되면 AOT 컴파일러는 해당 타입에

대한 코드를 미리 생성 가능.

void _unusedMethod() // type hint{

new PredicateOf<int>();}

Page 29: [NDC 2016] 유니티, iOS에서 LINQ 사용하기

LINQ in Mono 3.x#if !FULL_AOT_RUNTIME

private class PredicateOf<T>{

public static readonly Func<T, bool> Always = (Func<T, bool>)(t => true);}

#endif

public static TSource FirstOrDefault<TSource>(this IEnumerable<TSource> source){#if !FULL_AOT_RUNTIME

return First<TSource>(source, PredicateOf<TSource>.Always, Fallback.Default);#endif

foreach(var element in source)return element;

return default(TSource);}

Mono 3.x는 해당 버그를 우회했다.유니티는 Mono 2.x를 사용

Page 30: [NDC 2016] 유니티, iOS에서 LINQ 사용하기

그 외…몇 몇 메소드의 경우 특정 상황에서 Lambda 또는 익명 메소드를 AOT 컴파일 하지 못한다.(Generic과 무관)

해당 Lambda 또는 익명 메소드를 멤버 메소드로 바꾸면 예외가 사라진다.

public static double Average (this IEnumerable<int> source, Func<TSource, int> selector){

return source.Select(selector).Average<int, long, double> (source, (a, b) => a + b, (a, b) => (double) a / (double) b);}public static double AverageModified(this IEnumerable<int> source, Func<TSource, int> selector){

return source.Select(selector).Average<int, long, double> (source, Func1, Func2); // Lambda를 멤버 메소드로 대체}static long Func1(long a, int b){

return a + b;}static double Func2(long a, long b){

return (double)a / (double)b;}

실제 발표 시 해당 페이지의 일부 내용이 잘못 표기 되어 수정되었습니다.

이 메소드 내부에 사용한 람다식의 코드를생성하지 못한다.ex> Action action = () => Debug.Log(“Foo”);action();

Page 31: [NDC 2016] 유니티, iOS에서 LINQ 사용하기

그 외…그 외에도 여러 가지 패턴의 AOT 관련 예외가 있으며 자세한 사항은 아래의 링크에서 확인.

- http://neue.cc/2014/07/01_474.html

Page 32: [NDC 2016] 유니티, iOS에서 LINQ 사용하기

LINQ ExceptioniOS 환경에서 LINQ를 사용할 수 없는 이유는 LINQ 자체의 결함이 아니라

mono AOT 컴파일러의 버그가 원인

Page 33: [NDC 2016] 유니티, iOS에서 LINQ 사용하기

그럼 어떻게 해야 하나요?

지금까지 내용은 다 잊어도 좋습니다.

지금부터 내용은 잊으면 안 됩니다.

Page 34: [NDC 2016] 유니티, iOS에서 LINQ 사용하기

Third party libraryUniLinq

- Mono 3.10.0의 LINQ 코드를 기반

- 307개의 유닛테스트

- using UniLinq;

- https://github.com/RyotaMurohoshi/UniLinq

Page 35: [NDC 2016] 유니티, iOS에서 LINQ 사용하기

IL2CPPUnity Technology에서 만든 AOT 기술.

iOS 64bit 지원

현재 최신 버전의 IL2CPP는 안정적으로 LINQ를 지원(ver. 5.2.1f 에서 확인)

Page 36: [NDC 2016] 유니티, iOS에서 LINQ 사용하기

IL2CPP vs Mono AOT

C#Source Code

IL(Intermediate Language)

C# CompilerIL2CPP AOT Compiler

Compile time

C++Source Code

Machine Code

C++ Compiler

C#Source Code

IL(Intermediate Language)

Machine CodeC# Compiler AOT Compiler

Compile timeMono AOT

IL2CPP

Page 37: [NDC 2016] 유니티, iOS에서 LINQ 사용하기

LINQ with IL2CPP// Test.cs

public class Test : MonoBehaviour{

void Start () {

List<int> intList = new List<int> () { 0, 1 };intList.FirstOrDefault ();

List<float> floatList = new List<float> { 0, 1 };floatList.FirstOrDefault ();

List<string> stringList = new List<string>{ "0", "1" };stringList.FirstOrDefault ();

List<Material> materialList = new List<Material> ();materialList.FirstOrDefault ();

}}

Page 38: [NDC 2016] 유니티, iOS에서 LINQ 사용하기

LINQ with IL2CPP// Bulk_Generics_3.cpp

// System.Collections.Generic.List`1<System.Int32>struct List_1_t3644373756;

// System.Collections.Generic.List`1<System.Object>struct List_1_t1634065389;

// System.Collections.Generic.List`1<System.Single>struct List_1_t1755167990;

List<string> methodsList<Material> methods Shared

string, Material은 Reference Typeint(Int32), float(Single)은 Value Type

Page 39: [NDC 2016] 유니티, iOS에서 LINQ 사용하기

LINQ with IL2CPP// GenericMethods0.cpp

// TSource System.Linq.Enumerable::FirstOrDefault<System.Int32>(System.Collections.Generic.IEnumerable`1<TSource>)extern "C" int32_t Enumerable_FirstOrDefault_TisInt32_t2847414787_m1700521655_gshared (Il2CppObject * __this /* static, unused */, Il2CppObject* ___source, const MethodInfo* method){

{Il2CppObject* L_0 = ___source;Check_Source_m228347543(NULL /*static, unused*/, (Il2CppObject *)L_0, /*hidden argument*/NULL);Il2CppObject* L_1 = ___source;IL2CPP_RUNTIME_CLASS_INIT(IL2CPP_RGCTX_DATA(method->rgctx_data, 0));Func_2_t3308141622 * L_2 = ((PredicateOf_1_t2617073743_StaticFields*)IL2CPP_RGCTX_DATA(method->rgctx_data,

0)->static_fields)->get_Always_0();

int32_t L_3 = (( int32_t (*) (Il2CppObject * /* static, unused */, Il2CppObject*, Func_2_t3308141622 *, int32_t, constMethodInfo*))IL2CPP_RGCTX_METHOD_INFO(method->rgctx_data, 1)->method)(NULL /*static, unused*/, (Il2CppObject*)L_1, (Func_2_t3308141622 *)L_2, (int32_t)0, /*hidden argument*/IL2CPP_RGCTX_METHOD_INFO(method->rgctx_data, 1));

return L_3;}

}

Page 40: [NDC 2016] 유니티, iOS에서 LINQ 사용하기

LINQ with IL2CPP

PredicateOf_1_t2617073743_StaticFieldsget_Always_0

,

Page 41: [NDC 2016] 유니티, iOS에서 LINQ 사용하기

LINQ with IL2CPP// Enumerable.cs 원본 PredicateOfprivate class PredicateOf<T>{

public static readonly Func<T, bool> Always = (t => true);}

Page 42: [NDC 2016] 유니티, iOS에서 LINQ 사용하기

LINQ with IL2CPP// System_Core_System_Linq_Enumerable_PredicateOf_1_ge108692892.h

// System.Linq.Enumerable/PredicateOf`1<System.Int32>struct PredicateOf_1_t2617073743 : public Il2CppObject{public:

public:};

Page 43: [NDC 2016] 유니티, iOS에서 LINQ 사용하기

LINQ with IL2CPP// System_Core_System_Linq_Enumerable_PredicateOf_1_ge108692892.h

// System.Func`2<System.Int32,System.Boolean>struct Func_2_t3308141622;

struct PredicateOf_1_t2617073743_StaticFields{public:

// System.Func`2<T,System.Boolean> System.Linq.Enumerable/PredicateOf`1::AlwaysFunc_2_t3308141622 * ___Always_0;// System.Func`2<T,System.Boolean> System.Linq.Enumerable/PredicateOf`1::<>f__am$cache1Func_2_t3308141622 * ___U3CU3Ef__amU24cache1_1;

public:inline Func_2_t3308141622 * get_Always_0() const { return ___Always_0; }

…}

Page 44: [NDC 2016] 유니티, iOS에서 LINQ 사용하기

LINQ with IL2CPP// Bulk_Generics3.cpp

// System.Void System.Linq.Enumerable/PredicateOf`1<System.Int32>::.cctor()extern "C" void PredicateOf_1__cctor_m686174972_gshared (Il2CppObject * __this /* static, unused */, const MethodInfo* method){

…}

// System.Boolean System.Linq.Enumerable/PredicateOf`1<System.Int32>::<Always>m__76(T)extern "C" bool PredicateOf_1_U3CAlwaysU3Em__76_m1746297546_gshared (Il2CppObject * __this /* static, unused */, int32_t ___t, const MethodInfo* method){

{return (bool)1;

}}

Mono AOT로 컴파일되 지 않았던 메소드가잘 생성이 되어 있는 걸 알 수 있다.

Page 45: [NDC 2016] 유니티, iOS에서 LINQ 사용하기

결론IL2CPP를 사용. (iOS 64bit 지원을 위해서는 반드시 사용)

만약 낮은 버전의 Unity를 사용해야 한다면 Third party library 사용

그래도 뭔가 찜찜하고 불안하면 IL2CPP에서도 소스 코드 수정이 가능한 Third party library 사용

사실 Unity에서 LINQ를 사용하지 못한다는 편견을 깨고 싶었습니다.

Page 46: [NDC 2016] 유니티, iOS에서 LINQ 사용하기

감사합니다.