From 4981c0cf702d082edacae37e3a7b6c51c711da9f Mon Sep 17 00:00:00 2001 From: Jackson Dunstan Date: Sun, 12 Aug 2018 17:57:42 -0700 Subject: [PATCH] Add a constructor that fills the list with default values Fix ParallelFor support with Burst Add basic performance tests --- .../NativeLinkedList.cs | 121 +++++++++++ .../Tests/TestNativeLinkedList.cs | 31 ++- PerformanceTests.meta | 8 + PerformanceTests/PerformanceTest.cs | 198 ++++++++++++++++++ PerformanceTests/PerformanceTest.cs.meta | 11 + PerformanceTests/PerformanceTestScene.unity | 155 ++++++++++++++ .../PerformanceTestScene.unity.meta | 7 + 7 files changed, 530 insertions(+), 1 deletion(-) create mode 100644 PerformanceTests.meta create mode 100644 PerformanceTests/PerformanceTest.cs create mode 100644 PerformanceTests/PerformanceTest.cs.meta create mode 100644 PerformanceTests/PerformanceTestScene.unity create mode 100644 PerformanceTests/PerformanceTestScene.unity.meta diff --git a/JacksonDunstanNativeCollections/NativeLinkedList.cs b/JacksonDunstanNativeCollections/NativeLinkedList.cs index 47b4b28..0a16e42 100644 --- a/JacksonDunstanNativeCollections/NativeLinkedList.cs +++ b/JacksonDunstanNativeCollections/NativeLinkedList.cs @@ -9,6 +9,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Runtime.InteropServices; +using Unity.Burst; using Unity.Collections; using Unity.Collections.LowLevel.Unsafe; @@ -622,11 +623,15 @@ public NativeLinkedList(int capacity, Allocator allocator) UnsafeUtility.AlignOf(), allocator); + // Store the allocator for future allocation and deallocation + // operations m_State->m_Allocator = allocator; // Initially empty with the given capacity m_State->m_Length = 0; m_State->m_Capacity = capacity; + + // The list is empty so there is no head or tail m_State->m_HeadIndex = -1; m_State->m_TailIndex = -1; @@ -642,6 +647,116 @@ public NativeLinkedList(int capacity, Allocator allocator) #endif } + /// + /// Create the list with an initial capacity. It initially has no nodes. + /// + /// This complexity of this operation is O(N) plus the allocator's + /// allocation complexity. + /// + /// + /// + /// Initial capacity. This is capped at a minimum of four and the given + /// count. + /// + /// + /// + /// Number of nodes to add. This is capped at a minimum of zero. + /// + /// + /// + /// Allocator to allocate unmanaged memory with + /// + public NativeLinkedList(int capacity, int length, Allocator allocator) + { + // Insist on a non-negative length + if (length < 0) + { + length = 0; + } + + // Insist on a minimum capacity + int requiredCapacity = Math.Max(4, length); + if (capacity < requiredCapacity) + { + capacity = requiredCapacity; + } + + // Allocate the state. It is freed in Dispose(). + m_State = (NativeLinkedListState*)UnsafeUtility.Malloc( + sizeof(NativeLinkedListState), + UnsafeUtility.AlignOf(), + allocator); + + // Create the backing arrays. + int valuesSize = UnsafeUtility.SizeOf() * capacity; + m_State->m_Values = UnsafeUtility.Malloc( + valuesSize, + UnsafeUtility.AlignOf(), + allocator + ); + m_State->m_NextIndexes = (int*)UnsafeUtility.Malloc( + sizeof(int) * capacity, + UnsafeUtility.AlignOf(), + allocator); + m_State->m_PrevIndexes = (int*)UnsafeUtility.Malloc( + sizeof(int) * capacity, + UnsafeUtility.AlignOf(), + allocator); + + // If there are any nodes to initialize + int endIndex = length - 1; + if (length > 0) + { + // Clear the node values to their defaults + UnsafeUtility.MemClear(m_State->m_Values, valuesSize); + + // Initialize next pointers to the next index and the last next + // pointer to an invalid index + for (int i = 0; i < endIndex; ++i) + { + m_State->m_NextIndexes[i] = i + 1; + } + m_State->m_NextIndexes[endIndex] = -1; + + // Initialize prev pointers to the previous index and the first + // prev pointer to an invalid index + m_State->m_PrevIndexes[0] = -1; + for (int i = 1; i < length; ++i) + { + m_State->m_PrevIndexes[i] = i - 1; + } + + // The first node is the head and the last node is the tail + m_State->m_HeadIndex = 0; + m_State->m_TailIndex = endIndex; + } + else + { + // The list is empty so there is no head or tail + m_State->m_HeadIndex = -1; + m_State->m_TailIndex = -1; + } + + // Store the allocator for future allocation and deallocation + // operations + m_State->m_Allocator = allocator; + + // Initially sized to the given count with the given capacity + m_State->m_Length = length; + m_State->m_Capacity = capacity; + + // Version starts at one so that the default (0) is never used + m_State->m_Version = 1; + + // Initialize safety ranges +#if ENABLE_UNITY_COLLECTIONS_CHECKS + m_Length = length; + m_MinIndex = 0; + m_MaxIndex = endIndex; + DisposeSentinel.Create(out m_Safety, out m_DisposeSentinel, 0); +#endif + } + /// /// Get the capacity of the list. This is always greater than or equal /// to its . @@ -2052,6 +2167,7 @@ private void SetSafetyRange(int length) /// Index that must be in the safety check bounds /// [Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS")] + [BurstDiscard] private void RequireIndexInBounds(int index) { #if ENABLE_UNITY_COLLECTIONS_CHECKS @@ -2089,6 +2205,7 @@ private void RequireIndexInBounds(int index) /// full list. /// [Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS")] + [BurstDiscard] private void RequireFullListSafetyCheckBounds() { #if ENABLE_UNITY_COLLECTIONS_CHECKS @@ -2114,6 +2231,7 @@ private void RequireFullListSafetyCheckBounds() /// [0, state->Capacity] /// [Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS")] + [BurstDiscard] private void RequireRangeInBounds( int startIndex, int endIndex) @@ -2152,6 +2270,7 @@ private void RequireFullListSafetyCheckBounds() /// Throw an exception if the list isn't readable /// [Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS")] + [BurstDiscard] private unsafe void RequireReadAccess() { #if ENABLE_UNITY_COLLECTIONS_CHECKS @@ -2163,6 +2282,7 @@ private unsafe void RequireReadAccess() /// Throw an exception if the list isn't writable /// [Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS")] + [BurstDiscard] private unsafe void RequireWriteAccess() { #if ENABLE_UNITY_COLLECTIONS_CHECKS @@ -2174,6 +2294,7 @@ private unsafe void RequireWriteAccess() /// Throw an exception if the state is null /// [Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS")] + [BurstDiscard] private unsafe void RequireValidState() { #if ENABLE_UNITY_COLLECTIONS_CHECKS diff --git a/JacksonDunstanNativeCollections/Tests/TestNativeLinkedList.cs b/JacksonDunstanNativeCollections/Tests/TestNativeLinkedList.cs index 5bc8689..bd7d9f7 100644 --- a/JacksonDunstanNativeCollections/Tests/TestNativeLinkedList.cs +++ b/JacksonDunstanNativeCollections/Tests/TestNativeLinkedList.cs @@ -91,13 +91,42 @@ private static void AssertGeneralInvariants(NativeLinkedList list) } [Test] - public void ConstructorEnforcesMinimumCapacity() + public void EmptyConstructorEnforcesMinimumCapacity() { using (NativeLinkedList list = CreateList(1)) { Assert.That(list.Capacity, Is.EqualTo(4)); } } + + [Test] + public void DefaultValuesConstructorEnforcesMinimumCapacityAndLength() + { + using (NativeLinkedList list = new NativeLinkedList( + 1, + -1, + Allocator.Temp)) + { + Assert.That(list.Length, Is.EqualTo(0)); + Assert.That(list.Capacity, Is.EqualTo(4)); + AssertGeneralInvariants(list); + } + } + + [Test] + public void DefaultValuesConstructorSetsAllValuesToDefault() + { + using (NativeLinkedList list = new NativeLinkedList( + 6, + 4, + Allocator.Temp)) + { + Assert.That(list.Length, Is.EqualTo(4)); + Assert.That(list.Capacity, Is.EqualTo(6)); + Assert.That(list, Is.EqualTo(new [] { 0, 0, 0, 0 })); + AssertGeneralInvariants(list); + } + } [Test] public void PushBackIncreasesLengthAndCapacityReturnsAddedNodeEnumerator() diff --git a/PerformanceTests.meta b/PerformanceTests.meta new file mode 100644 index 0000000..3ca0191 --- /dev/null +++ b/PerformanceTests.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: dbbb40c0150a04942bb6e3a91a9a720b +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/PerformanceTests/PerformanceTest.cs b/PerformanceTests/PerformanceTest.cs new file mode 100644 index 0000000..435986d --- /dev/null +++ b/PerformanceTests/PerformanceTest.cs @@ -0,0 +1,198 @@ +using UnityEngine; +using Unity.Collections; +using Unity.Jobs; +using Unity.Burst; +using JacksonDunstan.NativeCollections; + +public class PerformanceTest : MonoBehaviour +{ + // Iterate jobs + + [BurstCompile] + struct ArrayIterateJob : IJob + { + public NativeArray Array; + public NativeArray Sum; + + public void Execute() + { + for (int i = 0; i < Array.Length; ++i) + { + Sum[0] += Array[i]; + } + } + } + + [BurstCompile] + struct ListIterateJob : IJob + { + public NativeList List; + public NativeArray Sum; + + public void Execute() + { + for (int i = 0; i < List.Length; ++i) + { + Sum[0] += List[i]; + } + } + } + + [BurstCompile] + struct LinkedListIterateJob : IJob + { + public NativeLinkedList LinkedList; + public NativeArray Sum; + + public void Execute() + { + for (int i = 0; i < LinkedList.Length; ++i) + { + Sum[0] += LinkedList[i]; + } + } + } + + // Iterate jobs (ParallelFor) + + [BurstCompile] + struct ArrayIterateJobParallelFor : IJobParallelFor + { + public NativeArray Array; + public NativeArray Sum; + + public void Execute(int index) + { + Sum[0] += Array[index]; + } + } + + [BurstCompile] + struct LinkedListIterateJobParallelFor : IJobParallelFor + { + public NativeLinkedList LinkedList; + public NativeArray Sum; + + public void Execute(int index) + { + Sum[0] += LinkedList[index]; + } + } + + // Use this for initialization + void Start() + { + const int size = 10000000; + + NativeArray sum = new NativeArray(1, Allocator.Temp); + + NativeArray array = new NativeArray( + size, + Allocator.Temp); + for (int i = 0; i < size; ++i) + { + array[i] = 1; + } + ArrayIterateJob arrayIterateJob = new ArrayIterateJob + { + Array = array, + Sum = sum + }; + ArrayIterateJobParallelFor arrayIterateJobParallelFor = new ArrayIterateJobParallelFor + { + Array = array, + Sum = sum + }; + + NativeList list = new NativeList( + size, + Allocator.Temp); + for (int i = 0; i < size; ++i) + { + list.Add(1); + } + ListIterateJob listIterateJob = new ListIterateJob + { + List = list, + Sum = sum + }; + + NativeLinkedList linkedList = new NativeLinkedList( + size, + Allocator.Temp); + for (int i = 0; i < size; ++i) + { + linkedList.PushBack(1); + } + LinkedListIterateJob linkedListIterateJob = new LinkedListIterateJob + { + LinkedList = linkedList, + Sum = sum + }; + LinkedListIterateJobParallelFor linkedListIterateJobParallelFor = new LinkedListIterateJobParallelFor + { + LinkedList = linkedList, + Sum = sum + }; + + // Warm up the job system + arrayIterateJob.Run(); + listIterateJob.Run(); + linkedListIterateJob.Run(); + arrayIterateJobParallelFor.Run(array.Length); + linkedListIterateJobParallelFor.Run(linkedList.Length); + + var sw = new System.Diagnostics.Stopwatch(); + + sum[0] = 0; + sw.Reset(); + sw.Start(); + arrayIterateJob.Run(); + long arrayIterateTime = sw.ElapsedMilliseconds; + + sum[0] = 0; + sw.Reset(); + sw.Start(); + listIterateJob.Run(); + long listIterateTime = sw.ElapsedMilliseconds; + + sum[0] = 0; + sw.Reset(); + sw.Start(); + linkedListIterateJob.Run(); + long linkedListIterateTime = sw.ElapsedMilliseconds; + + sum[0] = 0; + sw.Reset(); + sw.Start(); + arrayIterateJobParallelFor.Run(array.Length); + long arrayIterateParallelForTime = sw.ElapsedMilliseconds; + + sum[0] = 0; + sw.Reset(); + sw.Start(); + linkedListIterateJobParallelFor.Run(linkedList.Length); + long linkedListIterateParallelForTime = sw.ElapsedMilliseconds; + + sum.Dispose(); + array.Dispose(); + list.Dispose(); + linkedList.Dispose(); + + Debug.LogFormat( + "Operation,Job Type,NativeArray Time,NativeList Time,NativeLinkedList Time\n" + + "Iterate,Single,{0},{1},{2}\n" + + "Iterate,ParallelFor,{3},n/a,{4}", + arrayIterateTime, + listIterateTime, + linkedListIterateTime, + arrayIterateParallelForTime, + linkedListIterateParallelForTime); + } + + // Update is called once per frame + void Update() + { + + } +} diff --git a/PerformanceTests/PerformanceTest.cs.meta b/PerformanceTests/PerformanceTest.cs.meta new file mode 100644 index 0000000..a1e9548 --- /dev/null +++ b/PerformanceTests/PerformanceTest.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 311130592996f41ddbaa207a839f57f5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/PerformanceTests/PerformanceTestScene.unity b/PerformanceTests/PerformanceTestScene.unity new file mode 100644 index 0000000..cdc5794 --- /dev/null +++ b/PerformanceTests/PerformanceTestScene.unity @@ -0,0 +1,155 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!29 &1 +OcclusionCullingSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_OcclusionBakeSettings: + smallestOccluder: 5 + smallestHole: 0.25 + backfaceThreshold: 100 + m_SceneGUID: 00000000000000000000000000000000 + m_OcclusionCullingData: {fileID: 0} +--- !u!104 &2 +RenderSettings: + m_ObjectHideFlags: 0 + serializedVersion: 9 + m_Fog: 0 + m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} + m_FogMode: 3 + m_FogDensity: 0.01 + m_LinearFogStart: 0 + m_LinearFogEnd: 300 + m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1} + m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1} + m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1} + m_AmbientIntensity: 1 + m_AmbientMode: 0 + m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} + m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0} + m_HaloStrength: 0.5 + m_FlareStrength: 1 + m_FlareFadeSpeed: 3 + m_HaloTexture: {fileID: 0} + m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0} + m_DefaultReflectionMode: 0 + m_DefaultReflectionResolution: 128 + m_ReflectionBounces: 1 + m_ReflectionIntensity: 1 + m_CustomReflection: {fileID: 0} + m_Sun: {fileID: 0} + m_IndirectSpecularColor: {r: 0, g: 0, b: 0, a: 1} + m_UseRadianceAmbientProbe: 0 +--- !u!157 &3 +LightmapSettings: + m_ObjectHideFlags: 0 + serializedVersion: 11 + m_GIWorkflowMode: 1 + m_GISettings: + serializedVersion: 2 + m_BounceScale: 1 + m_IndirectOutputScale: 1 + m_AlbedoBoost: 1 + m_TemporalCoherenceThreshold: 1 + m_EnvironmentLightingMode: 0 + m_EnableBakedLightmaps: 1 + m_EnableRealtimeLightmaps: 1 + m_LightmapEditorSettings: + serializedVersion: 10 + m_Resolution: 2 + m_BakeResolution: 40 + m_AtlasSize: 1024 + m_AO: 0 + m_AOMaxDistance: 1 + m_CompAOExponent: 1 + m_CompAOExponentDirect: 0 + m_Padding: 2 + m_LightmapParameters: {fileID: 0} + m_LightmapsBakeMode: 1 + m_TextureCompression: 1 + m_FinalGather: 0 + m_FinalGatherFiltering: 1 + m_FinalGatherRayCount: 256 + m_ReflectionCompression: 2 + m_MixedBakeMode: 2 + m_BakeBackend: 1 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 500 + m_PVRBounces: 2 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVRFilteringMode: 1 + m_PVRCulling: 1 + m_PVRFilteringGaussRadiusDirect: 1 + m_PVRFilteringGaussRadiusIndirect: 5 + m_PVRFilteringGaussRadiusAO: 2 + m_PVRFilteringAtrousPositionSigmaDirect: 0.5 + m_PVRFilteringAtrousPositionSigmaIndirect: 2 + m_PVRFilteringAtrousPositionSigmaAO: 1 + m_ShowResolutionOverlay: 1 + m_LightingDataAsset: {fileID: 0} + m_UseShadowmask: 1 +--- !u!196 &4 +NavMeshSettings: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_BuildSettings: + serializedVersion: 2 + agentTypeID: 0 + agentRadius: 0.5 + agentHeight: 2 + agentSlope: 45 + agentClimb: 0.4 + ledgeDropHeight: 0 + maxJumpAcrossDistance: 0 + minRegionArea: 2 + manualCellSize: 0 + cellSize: 0.16666667 + manualTileSize: 0 + tileSize: 256 + accuratePlacement: 0 + debug: + m_Flags: 0 + m_NavMeshData: {fileID: 0} +--- !u!1 &909671986 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 909671987} + - component: {fileID: 909671988} + m_Layer: 0 + m_Name: GameObject + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &909671987 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 0} + m_GameObject: {fileID: 909671986} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 0} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &909671988 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInternal: {fileID: 0} + m_GameObject: {fileID: 909671986} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 311130592996f41ddbaa207a839f57f5, type: 3} + m_Name: + m_EditorClassIdentifier: diff --git a/PerformanceTests/PerformanceTestScene.unity.meta b/PerformanceTests/PerformanceTestScene.unity.meta new file mode 100644 index 0000000..594891e --- /dev/null +++ b/PerformanceTests/PerformanceTestScene.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 4850c6f323d3e45959cfa82571b5e9b9 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: