Search Unity

[SOLVED] Copy from NativeArray/NativeList to List?

Discussion in 'Entity Component System' started by FrankvHoof, May 7, 2019.

  1. FrankvHoof

    FrankvHoof

    Joined:
    Nov 3, 2014
    Posts:
    258
    Is there a way to use the new MemCopy to copy to a List?
    NativeList doesn't seem to have a CopyTo, and the actual MemCpy-method is unavailable because AddressOf doesn't take a list..

    Some more detail:
    My renderingsystem works by taking batches of 1023 transforms, and using Graphics.DrawMeshInstanced to render them. In order to save on GC, we pre-allocate a single array, then memcpy and draw with that.
    However, the final batch is not of the same length, thus we can't use the same array.
    Allocating a (managed) array leaves me with spikes of up to 40KB of garbage, and using a for-loop to add from a NativeList to a List (preallocated at 1023 capacity) is way to slow (taking close to 4.5ms).

    Is there a way that I can still memcpy from a NativeList to a List?

    Is there a way I can memcpy to a List (or it's underlying array)?
     
  2. FrankvHoof

    FrankvHoof

    Joined:
    Nov 3, 2014
    Posts:
    258
    After spitballing with to a colleague, I'm currently guessing my best bet is to use Reflection to grab the reference to the list's underlying array, and MemCpy to that.
    Is there a nicer way anyone can think of?
     
  3. supron

    supron

    Joined:
    Aug 24, 2013
    Posts:
    67
    You can grab unity internal class NoAllocHelpers (https://github.com/Unity-Technologi...e/Export/Scripting/NoAllocHelpers.bindings.cs) with reflection and create delegates to its methods. With this class you can resize list and take underlying array, without reflection overhead. It's probably the fastest possible way to do that. I tested it both in mono and il2cpp and it works, but you have to create link.xml to exclude this class from stripping.
     
  4. Singtaa

    Singtaa

    Joined:
    Dec 14, 2010
    Posts:
    492
    Usually
    List.AddRange(IEnumerable collection)
    should suffice (you can pass in the NativeArray directly).

    Otherwise you can find some way to work with
    NativeArrayUnsafeUtility.GetUnsafePtr
    .
     
  5. FrankvHoof

    FrankvHoof

    Joined:
    Nov 3, 2014
    Posts:
    258
    Unfortunately, the code for AddRange looks like this (It calls InsertRange internally):

    Code (CSharp):
    1. public void InsertRange(int index, IEnumerable<T> collection)
    2.     {
    3.       if (collection == null)
    4.         ThrowHelper.ThrowArgumentNullException(ExceptionArgument.collection);
    5.       if ((uint) index > (uint) this._size)
    6.         ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.index, ExceptionResource.ArgumentOutOfRange_Index);
    7.       ICollection<T> objs = collection as ICollection<T>;
    8.       if (objs != null)
    9.       {
    10.         int count = objs.Count;
    11.         if (count > 0)
    12.         {
    13.           this.EnsureCapacity(this._size + count);
    14.           if (index < this._size)
    15.             Array.Copy((Array) this._items, index, (Array) this._items, index + count, this._size - index);
    16.           if (this == objs)
    17.           {
    18.             Array.Copy((Array) this._items, 0, (Array) this._items, index, index);
    19.             Array.Copy((Array) this._items, index + count, (Array) this._items, index * 2, this._size - index);
    20.           }
    21.           else
    22.           {
    23.             T[] array = new T[count];
    24.             objs.CopyTo(array, 0);
    25.             array.CopyTo((Array) this._items, index);
    26.           }
    27.           this._size += count;
    28.         }
    29.       }
    30.       else
    31.       {
    32.         foreach (T obj in collection)
    33.           this.Insert(index++, obj);
    34.       }
    35.       ++this._version;
    36.     }
    As NativeList is not an ICollection, this would be as slow (and produce as much garbage) as a regular foreach

    We were using this with Arrays before we found out NativeArray.CopyTo() does basically the same thing:


    Code (CSharp):
    1.  
    2.         public static unsafe void MemCopy<T>(
    3.             this NativeArray<T> nativeArray,
    4.             T[] array)
    5.             where T : struct
    6.         {
    7.             if (array == null)
    8.             {
    9.                 throw new NullReferenceException(nameof(array) + " is null");
    10.             }
    11.             int nativeArrayLength = nativeArray.Length;
    12.             if (array.Length < nativeArrayLength)
    13.             {
    14.                 throw new IndexOutOfRangeException(
    15.                     nameof(array) + " is shorter than " + nameof(nativeArray));
    16.             }
    17.             int byteLength = nativeArray.Length * UnsafeUtility.SizeOf<T>();
    18.             void* managedBuffer = UnsafeUtility.AddressOf(ref array[0]);
    19.             void* nativeBuffer = nativeArray.GetUnsafePtr();
    20.             // MemCpy( destenation , source , length)
    21.             UnsafeUtility.MemCpy(managedBuffer, nativeBuffer, byteLength);
    22.         }
    However, this breaks for Lists on
    UnsafeUtility.AddressOf(ref array[0])
    because it can't be used with a list.
     
    Singtaa likes this.
  6. FrankvHoof

    FrankvHoof

    Joined:
    Nov 3, 2014
    Posts:
    258
    Thanks. I'll have a look at implementing it in the morning.
    Could you tell me what exactly I'd have to add in my link.xml? (I thought that was mainly used per-assembly?)

    Edit: tertle seems to have a very nice post on this (NativeAddRange) in this thread:
    https://forum.unity.com/threads/nativearray-and-mesh.522951/
     
    Last edited: May 7, 2019
  7. FrankvHoof

    FrankvHoof

    Joined:
    Nov 3, 2014
    Posts:
    258
    @supron Could you tell me what should be in the link.xml?
    I currently added:

    Code (CSharp):
    1. <assembly fullname="UnityEngine">
    2.     <type fullname="UnityEngine.NoAllocHelpers" preserve="all" />
    3. </assembly>
    but it doesn't seem to be working, as when running on IL2CPP (Android) I get:

    AndroidPlayer(ADB@127.0.0.1:34999) ExecutionEngineException: Attempting to call method 'UnityEngine.NoAllocHelpers::ResizeList<UnityEngine.Matrix4x4>' for which no ahead of time (AOT) code was generated.
     
  8. FrankvHoof

    FrankvHoof

    Joined:
    Nov 3, 2014
    Posts:
    258
    Changed out my link.xml to:
    Code (CSharp):
    1. <assembly fullname="UnityEngine.CoreModule">
    2.     <type fullname="UnityEngine.NoAllocHelpers" preserve="all" />
    3. </assembly>
    as that seems to be the assembly it's fetching it from.

    I also tried adding:
    Code (CSharp):
    1. public static void UsedOnlyForAOTCodeGeneration()
    2.         {
    3.             List<Matrix4x4> fakeList = new List<Matrix4x4>();
    4.             ResizeList(fakeList, 1);
    5.             // Include an exception so we can be sure to know if this method is ever called.
    6.             throw new InvalidOperationException("This method is used for AOT code generation only. Do not call it at runtime.");
    7.         }
    but to no avail..
    I don't think the method is being stripped.. Just not AOT-compiled..

    Pinging @supron and @Joachim_Ante for help...
     
  9. Singtaa

    Singtaa

    Joined:
    Dec 14, 2010
    Posts:
    492
    Don't you need the "<linker>" root node?
     
  10. FrankvHoof

    FrankvHoof

    Joined:
    Nov 3, 2014
    Posts:
    258
    yes, but my full linker is much larger than this bit (includes all the packages, etc.)
     
  11. supron

    supron

    Joined:
    Aug 24, 2013
    Posts:
    67
    Your problem comes form generic function. Dynamic delegates to generic functions are not working in AOT environments. NoAllocHelper has two ResizeList functions:
    Code (CSharp):
    1. public static void ResizeList<T>(List<T> list, int size)
    2. extern internal static void Internal_ResizeList(object list, int size);
    Just call internal version instead of generic version. You can create generic method in your class and pass list as "object" to Internal_ResizeList.
     
    FrankvHoof likes this.
  12. FrankvHoof

    FrankvHoof

    Joined:
    Nov 3, 2014
    Posts:
    258
    That did the trick. Thanks.