360 Views
May 10, 17
スライド概要
講演者:フランシス・デュランシー(Unity Technologies)
こんな人におすすめ
・プログラマー全般
受講者が得られる知見
・パフォーマンス上のボトルネック発見のためにILを使う方法
・リアルタイムアプリケーションのためのC#ベストプラクティス
・パフォーマンス向上のためのデザインパターン実装方法
講演動画:https://youtu.be/h_JVSLGRWUQ
リアルタイム3Dコンテンツを制作・運用するための世界的にリードするプラットフォームである「Unity」の日本国内における販売、サポート、コミュニティ活動、研究開発、教育支援を行っています。ゲーム開発者からアーティスト、建築家、自動車デザイナー、映画製作者など、さまざまなクリエイターがUnityを使い想像力を発揮しています。
C# Tips and Tricks
Francis Duranceau Lead Field Engineer, Unity
Plan • The C# compiler • .NET 4.6 • Marshalling • Understanding Boxing, Foreach, Stack vs. Heap allocation • GameManager Singleton pattern
C# compiler
C# compiler • The VM is an abstract stack machine • Then, we compile • Source file goes through • Tree builder that results into an • Abstract syntax tree that goes into the • Attribute evaluator that turns into an • Attributed tree that then feeds the • Tree walker that finally generates the • IL
C# compiler int SimpleCondition(int input) { if(input >= 0) return input * 2; else return -input; }
C# compiler <projectDir>/Temp/StagingArea/IL2Cpp/il2cppOutput/Bulk_Assembly-CSharp_*.cpp
.NET 4.6
.NET 4.6 Most notably : • Async • Tasks • DataContracts • Cryptography • Networking
.NET 4.6 -> C# 6.0 • Null-Conditional Operator • Auto-Property Initializers • Nameof Expressions • Primary Constructors • Expression Bodied Functions and Properties
.NET 4.6 -> C# 6.0
After .NET 4.6...
.NET 4.6 FAQ • What platforms does this affect? All of them, but in different ways: - Editor and Standalone use the new version of Mono when this option is enabled. - All console platforms will only be able to use IL2CPP when targeting the new .NET version. - iOS and WebGL will continue to be IL2CPP only. - Android will continue to support both Mono and IL2CPP. - Other platforms are still undergoing work to support either new Mono or IL2CPP • What about IL2CPP? IL2CPP fully supports the new .NET 4.6 APIs and features.
.NET 4.6 FAQ • What about the a new GC? The newer Mono garbage collector (SGen) requires additional work in Unity and will follow once we have stabilized the new runtime and class libraries. • Why are my builds larger with the new .NET version? The .NET 4.6 class libraries are quite larger than our current .NET 3.5 class libraries. We are actively working on improving the managed linker to reduce size further.
Marshalling
What is ‘Unmanaged’ Code? • As the name implies, unmanaged code means YOU are responsible for a host of things, that are usually taken care of for you when using managed code. For example: • • • • • Memory management Thread Synchronization Security Life-time control of objects Etc.
Using Unmanaged Code Access managed through DLL / Libraries [DllImport (“YourCode”)] followed by a function name, which needs to be static and extern Place a plugin directory alongside the ‘Assets’ folder in your project and place the library there If you get a “DllNotFoundException” make sure the path is correct and your library is present in the Plugins directory.
DLL in Unity Put your plugins in : • Assets/Plugins • Assets/Plugins/x86 • Assets/Plugins/x86_64 Once you have create / compiled and place your DLL / library in the right place, you can call it directly from your managed code.
Invoking Unmanaged Code • Generally speaking, you just call the method associated with the DLLImport tag. • Using GetProcAddress(), the specific function is looked up and executed. • Unfortunately, things aren’t that simple (surprise!) • C ABI is used for most calls. • Makes it near impossible to call functions that don’t adhere to this, like C++. • Again, ABI’s are platform specific. • Use either __stdcall and __cdecl for VC++ or __attribute__((stdcall)) and • __attribute__((cdecl)) for GCC. • If you need to invoke C++ code, use ‘extern “C”’ to ensure the calling convention is adhered to.
Exceptions • Runtime Exceptions can happen and unmanaged code needs to be able to deal with it. • Unfortunately, C doesn’t support exceptions • C++ does to some extent but has a different mechanism. • Don’t cross the streams and let exceptions propagate between managed and unmanaged code or you run the risk of things going bad
Marshalling • Marshalling is the process of ‘converting’ parameters from managed to unmanaged space. • For simple types, this just becomes a straight copy (byte, int, floats, etc) • Sometimes called ‘blitting’ • Bool, Strings, Arrays are a little more complex…. • Depends on what type of string it is: • Ascii, UTF-8, UTF-16, etc. • Memory boundaries need to be kept separate! • Don’t keep references around to managed data! • Data is usually released after call, with obvious consequences. • Possible to lock a memory area by using the C# ’fixed’ statement. • Generally, memory is copied around!
Marshalling (Cnt’d) • Strings, Classes and Structs • As mentioned earlier, strings are handled different than blittable data types. • CLR doesn’t just look for single function but function based on the type. I.e. Different functions for different objects passed in. • In order to facilitate this, you can ‘MarshallAs’ and describe your own datatype, as follows: [DllImport (”somedll")] private static extern void Foo ( [MarshalAs(UnmanagedType.LPStr)] string ansiString, [MarshalAs(UnmanagedType.LPWStr)] string unicodeString, [MarshalAs(UnmanagedType.LPTStr)] string platformString );
Marshalling (Cnt’d) • Structs and classes can’t be passed by value, only by reference. • Since it’s managed memory, anything that’s copied may move / change. • Generally a bad thing to do anyway! • Structs and classes differ by alignment. • Structs are sequentially aligned by default (as you’d expect) • Classes may or may not be laid out in memory the way you defined them! • If you must have them sequential, use the [StructLayout (LayoutKind.Sequential)] tag. • Return values should be copied back into managed memory.
Memory Management / Custom Marshalling • Memory managed owned by CLI will be managed by CLI • I.e. a reference to an object passed down to unmanaged code will be reclaimed by the CLI. • Both managed and unmanaged memory allocations come from the same pool. • Use Marshall.AllocCoTaskMem() and Marshall.FreeCoTaskMem() • Rather than having built-in Marshalling, you can use create custom ones. • Use ‘MarshalAs’ • Remember that you’ll need to write implementations for all variations of objects passed in!
Bottom Line • Don’t overlook the impact Marshalling has on your code. • You’re copying memory around! • Make sure you know who owns what. I.e. The CG doesn’t know what you own / use. • Unless you have a valid reason for using unmanaged code, stick to managed code.
Example of marshalling struct Boss { char* name; int health; }; int SumBossHealth(Boss* bosses, int size) { int sum = 0; for (int i = 0; i < size; ++i) { sum += bosses[i].health; } return sum; } bool IsBossDead(Boss b) { return b.health == 0; }
Example of marshalling [DllImport("__Internal")] [return: MarshalAs(UnmanagedType.U1)] private extern static bool IsBossDead(Boss b); [DllImport("__Internal")] private extern static int SumBossHealth(Boss[] bosses, int size);
Example of marshalling
Boss[] bosses = {new Boss("First Boss", 25), new Boss("Second Boss",
45)};
Debug.Log (string.Format ("Marshaling a non-blittable struct: {0}",
IsBossDead (new Boss("Final Boss", 100))));
Debug.Log(string.Format("Marshaling an array by reference: {0}",
SumBossHealth (bosses, bosses.Length)));
Best practices Understanding Boxing, Foreach, Stack vs. Heap allocation
Best practices - Boxing
using UnityEngine;
using System.Collections;
public class InputAxis : MonoBehaviour
{
void Update ()
{
float x = Input.GetAxis("Horizontal");
Debug.Log(x);
}
Best practices extern "C" void InputAxis_Update_m397189571 (InputAxis_t277341211 * __this, const MethodInfo* method) { [...] float V_0 = 0.0f; { IL2CPP_RUNTIME_CLASS_INIT(Input_t1785128008_il2cpp_TypeInfo_var); float L_0 = Input_GetAxis_m2098048324(NULL /*static, unused*/, _stringLiteral855845486, /*hidden argument*/NULL); V_0 = L_0; float L_1 = V_0; float L_2 = L_1; Il2CppObject * L_3 = Box(Single_t2076509932_il2cpp_TypeInfo_var, &L_2); IL2CPP_RUNTIME_CLASS_INIT(Debug_t1368543263_il2cpp_TypeInfo_var); Debug_Log_m920475918(NULL /*static, unused*/, L_3, /*hidden argument*/NULL); return; } }
Best practices - Foreach int ForEachArray (int[] items) { var total = 0; foreach (var item in items) total += item; return total; }
Best practices - Foreach int ForEachArray(int[] items) { int num = 0; for (int i = 0; i < items.Length; i++) { int num2 = items[i]; num += num2; } return num; }
Best practices – Foreach on collections To avoid allocations when using foreach over a met: collection the following criteria need to be - For collections of value types, the collection and enumerator implement the generic interfaces IEnumerable<T> and IEnumerator<T>. - The enumerator implementation is a struct not a class. - The collection has a public method named GetEnumerator whose return type is the enumerator struct. The BCL types System.Collections.Generic.List<T>, System.Collections.Generic.Dictionary<T>, and System.Collections.Generic.HashSet<T> all follow the guidelines above and should be safe to perform foreach statements on.
Best practices – Foreach on collections
int ForEachCustom (CustomCollection items)
{
var total = 0;
foreach (int item in items)
total += item;
return total;
}
Specify the type explicitely -> public class Enumerator : IEnumerator<int>, not just an Enumerator
on Systems.Objects (by default).
Use a struct and not a class so your when you do new in GetEnumerator, it goes on the stack and
not the heap -> return new Enumerator (this);
Best practices – Foreach on collections
class CustomCollection : IEnumerable<int>
{
private readonly int[] _items;
public CustomCollection(int[] items) { _items = items; }
public Enumerator GetEnumerator ()
{ return new Enumerator (this); }
IEnumerator<int> IEnumerable<int>.GetEnumerator ()
{ return GetEnumerator (); }
IEnumerator IEnumerable.GetEnumerator ()
{ return GetEnumerator (); }
public struct Enumerator : IEnumerator<int>
{
private readonly CustomCollection _collection;
private int _index;
[...]
}
}
Best practices public class ClassVsStruct : MonoBehaviour { struct PointStruct { public int x; public int y; } class PointClass { public int x; public int y; } void Update() { PointStruct ps; ps.x = 3; ps.y = 4; PointStruct ps2 = new PointStruct(); ps2.x = 7; ps2.y = 8; PointClass pc = new PointClass(); pc.x = 5; pc.y = 6; gameObject.transform.position.Set( ps.x + pc.x, ps.y + pc.y, 0); } }
.method private hidebysig instance void Update () cil managed { .maxstack 4 .locals init ( [0] valuetype ClassVsStruct/PointStruct, [1] valuetype ClassVsStruct/PointStruct, [2] class ClassVsStruct/PointClass, [3] valuetype [UnityEngine]UnityEngine.Vector3 ) IL_0000: ldloca.s 0 IL_0002: ldc.i4.3 IL_0003: stfld int32 ClassVsStruct/PointStruct::x IL_0008: ldloca.s 0 IL_000a: ldc.i4.4 IL_000b: stfld int32 ClassVsStruct/PointStruct::y IL_0010: ldloca.s 1 IL_0012: initobj ClassVsStruct/PointStruct IL_0018: ldloca.s 1 IL_001a: ldc.i4.7 IL_001b: stfld int32 ClassVsStruct/PointStruct::x IL_0020: ldloca.s 1 IL_0022: ldc.i4.8 IL_0023: stfld int32 ClassVsStruct/PointStruct::y IL_0028: newobj instance void ClassVsStruct/PointClass::.ctor() IL_002d: stloc.2 IL_002e: ldloc.2 IL_002f: ldc.i4.5
Best practices • Strings are immutable and will make copies • Use String.Builder • Use a hash to avoid Strings static readonly int material_Color = Shader.PropertyToID(“_Color”); static readonly int anim_Attack = Animator.StringToHash(“attack”); material.SetColor(material_Color, Color.white); animator.SetTrigger(anim_Attack);
The GC.Collect myth • There’s a myth saying that when you want to call GC.Collect, you should do it 6 times. • Another myth says it’s 7 times. • Is that really true?
The GC.Collect myth • On some platforms the GC can decommit memory from virtual memory • And that happens after so many collections without some pages being needed • But it can never be used by anything other than GC because it is still reserved … that address range is not available for any other allocator in the process • tldr; There isn’t much benefits. Unless you do something wrong and want to be nice to other applications running on that OS.
Best practices Vector3.zero really does get { return new Vector3(0,0,0); }
GameManager pattern
GameManager pattern • In this use case, there is a very important lesson to learn. But to show you the lesson, I will do a very common mistake… • The Singleton is a very good candidate that can be called thousands of times per frame • Let’s look at one way to implement it.
GameManager pattern
GameManager pattern 0.34ms 0.35ms
GameManager pattern • static bool IsNativeObjectAlive(UnityEngine.Object o) • { • #if !UNITY_EDITOR return o.GetCachedPtr() != IntPtr.Zero; #else if (o.GetCachedPtr() != IntPtr.Zero) return true; if (o is MonoBehaviour || o is ScriptableObject) return false; return DoesObjectWithInstanceIDExist(o.GetInstanceID()); #endif }
GameManager pattern using UnityEngine.Profiling; public class SingletonCaller : MonoBehaviour { CustomSampler samplerFind; [...] void Start() { samplerFind = CustomSampler.Create("CallSingletonGameFind"); } [...] void Update() { samplerFind.Begin(); CallSingletonGameFind(); samplerFind.End(); } }
GameManager pattern
Never profile a game running in the editor.
? operator string str = null; void Test1() { str?.ToUpper(); } void Test2() { if(str != null) str.ToUpper(); }
? operator
What about… • MonoBehaviour vs class? • Sealed • Static • Unsafe • LINQ • Generic
Finally…
Conclusion • Some C# keywords are hiding overhead and allocations • Prefer POD and struct of classes • int, float, struct • NOT class, NOT strings • Check the resulting IL • But more importantly than anything else …
Only profile if the profiler tells you to do so
Thank you!