From 67d187e081af1a58a3a7cfd8fc0275cf6dfc0557 Mon Sep 17 00:00:00 2001 From: Joe Amenta Date: Wed, 13 Mar 2019 06:01:13 -0400 Subject: [PATCH] Use NativeMemoryArray when available and appropriate. --- nuspec/Itinero.nuspec | 6 +- src/Itinero/DefaultArrayFactory.cs | 91 +++++++++++++++++++++++++++++- src/Itinero/Itinero.csproj | 8 +-- test/Itinero.Test/ContextTests.cs | 86 ++++++++++++++++++++++++++-- 4 files changed, 177 insertions(+), 14 deletions(-) diff --git a/nuspec/Itinero.nuspec b/nuspec/Itinero.nuspec index d3a71d7d..6c37bb20 100644 --- a/nuspec/Itinero.nuspec +++ b/nuspec/Itinero.nuspec @@ -16,16 +16,16 @@ osm, openstreetmap, routing, mapping - + - + - + diff --git a/src/Itinero/DefaultArrayFactory.cs b/src/Itinero/DefaultArrayFactory.cs index 69452cfe..27d0eb3d 100644 --- a/src/Itinero/DefaultArrayFactory.cs +++ b/src/Itinero/DefaultArrayFactory.cs @@ -1,4 +1,5 @@ -using Reminiscence.Arrays; +using System; +using Reminiscence.Arrays; namespace Itinero { @@ -8,7 +9,95 @@ namespace Itinero /// public sealed class DefaultArrayFactory : IArrayFactory { +#if !HAS_NATIVE_MEMORY_ARRAY /// public ArrayBase CreateMemoryBackedArray(long size) => new MemoryArray(size); +#else + /// + /// Gets or sets an instance to use to allocate + /// contiguous blocks of virtual memory. + /// + /// The default is an allocator that uses the global heap, i.e., the trio of: + /// , + /// , and + /// . + /// + /// + public IUnmanagedMemoryAllocator UnmanagedMemoryAllocator { get; set; } = DefaultUnmanagedMemoryAllocator.Instance; + + // overwritten only by tests, temporarily, to validate behavior on 32-bit systems. + internal bool Is32BitProcess { get; set; } = IntPtr.Size == 4; + + /// + public ArrayBase CreateMemoryBackedArray(long size) + { + // 32-bit processes risk running out of contiguous virtual memory blocks big enough for + // the kinds of arrays we use long before they risk running out of memory, so if the + // native array would be larger than a single MemoryArray block on a 32-bit process, + // then fall back to MemoryArray for safety. + const long MemoryArrayThresholdFor32Bits = 1 << 20; + if (Is32BitProcess && size > MemoryArrayThresholdFor32Bits) + { + return new MemoryArray(size); + } + + // NativeMemoryArray is constrained to unmanaged types, so we have to do a bit of a + // workaround in order to use it this way. Don't worry about performance of all the type + // checks; they should happen at JIT-time, not at runtime. + // RuntimeHelpers.IsReferenceOrContainsReferences() is available in .NET Core 2.0 and + // .NET Standard 2.1 which we could use as part of a solution that eliminates this stack + // of copy-paste and hardcoded list of types. + object arrayObj = null; + if (typeof(T) == typeof(byte)) + { + arrayObj = new NativeMemoryArray(this.UnmanagedMemoryAllocator, size); + } + else if (typeof(T) == typeof(sbyte)) + { + arrayObj = new NativeMemoryArray(this.UnmanagedMemoryAllocator, size); + } + else if (typeof(T) == typeof(short)) + { + arrayObj = new NativeMemoryArray(this.UnmanagedMemoryAllocator, size); + } + else if (typeof(T) == typeof(ushort)) + { + arrayObj = new NativeMemoryArray(this.UnmanagedMemoryAllocator, size); + } + else if (typeof(T) == typeof(char)) + { + arrayObj = new NativeMemoryArray(this.UnmanagedMemoryAllocator, size); + } + else if (typeof(T) == typeof(int)) + { + arrayObj = new NativeMemoryArray(this.UnmanagedMemoryAllocator, size); + } + else if (typeof(T) == typeof(uint)) + { + arrayObj = new NativeMemoryArray(this.UnmanagedMemoryAllocator, size); + } + else if (typeof(T) == typeof(long)) + { + arrayObj = new NativeMemoryArray(this.UnmanagedMemoryAllocator, size); + } + else if (typeof(T) == typeof(ulong)) + { + arrayObj = new NativeMemoryArray(this.UnmanagedMemoryAllocator, size); + } + else if (typeof(T) == typeof(float)) + { + arrayObj = new NativeMemoryArray(this.UnmanagedMemoryAllocator, size); + } + else if (typeof(T) == typeof(double)) + { + arrayObj = new NativeMemoryArray(this.UnmanagedMemoryAllocator, size); + } + // else it's either a compatible type that we never bothered to add (in which case, + // please add it here!), or it is / contains references (in which case, we cannot return + // NativeMemoryArray, though a future specific optimization could handle it). + + return ((ArrayBase)arrayObj) ?? new MemoryArray(size); + } +#endif } } diff --git a/src/Itinero/Itinero.csproj b/src/Itinero/Itinero.csproj index 68e5e228..49712460 100644 --- a/src/Itinero/Itinero.csproj +++ b/src/Itinero/Itinero.csproj @@ -12,7 +12,7 @@ false - + @@ -24,13 +24,13 @@ - $(DefineConstants);NETFX_CORE;DOTNET_CORE + $(DefineConstants);NETFX_CORE;DOTNET_CORE;HAS_NATIVE_MEMORY_ARRAY - $(DefineConstants);NETFX_CORE;DOTNET_CORE + $(DefineConstants);NETFX_CORE;DOTNET_CORE;HAS_NATIVE_MEMORY_ARRAY - $(DefineConstants);NET45 + $(DefineConstants);NET45;HAS_NATIVE_MEMORY_ARRAY diff --git a/test/Itinero.Test/ContextTests.cs b/test/Itinero.Test/ContextTests.cs index 42cbf4de..dc041c83 100644 --- a/test/Itinero.Test/ContextTests.cs +++ b/test/Itinero.Test/ContextTests.cs @@ -1,5 +1,4 @@ using System; - using NUnit.Framework; using Reminiscence.Arrays; @@ -17,15 +16,90 @@ public class ContextTests /// /// The default factory in the context should be initialized properly. /// + [TestCase(default(byte))] + [TestCase(default(sbyte))] + [TestCase(default(short))] + [TestCase(default(ushort))] + [TestCase(default(char))] + [TestCase(default(int))] + [TestCase(default(uint))] + [TestCase(default(long))] + [TestCase(default(ulong))] + [TestCase(default(double))] + [TestCase(default(float))] + public void DefaultArrayFactoryShouldReturnNativeMemoryArrayWhenAppropriate(T exemplar) + { + const long SmallSize = 592; + const long LargeSize = 1 << 20 + 1; + + if (!(Context.ArrayFactory is DefaultArrayFactory defaultArrayFactory)) + { + Assert.Inconclusive("Need the default to be a DefaultArrayFactory."); + return; + } + + Type nativeMemoryArrayType = typeof(NativeMemoryArray<>).MakeGenericType(typeof(T)); + + bool old32BitProcessValue = defaultArrayFactory.Is32BitProcess; + try + { + // test behavior in a 32-bit process for the first round of tests. + defaultArrayFactory.Is32BitProcess = true; + + // test 32-bit with a huge block + using (var array = defaultArrayFactory.CreateMemoryBackedArray(LargeSize)) + { + Assert.AreEqual(LargeSize, array.Length); + Assert.IsInstanceOf>(array, "32-bit processes are expected to have virtual memory that's more cramped, so we should use MemoryArray's chunked allocator."); + } + + // test 32-bit with a small block + using (var array = defaultArrayFactory.CreateMemoryBackedArray(SmallSize)) + { + Assert.AreEqual(SmallSize, array.Length); + Assert.IsInstanceOf(nativeMemoryArrayType, array, "MemoryArray would allocate this in a single block anyway, so there's no reason not to use NativeMemoryArray."); + } + + // test behavior in a 64-bit process for the next round of tests. + defaultArrayFactory.Is32BitProcess = false; + + // test 64-bit with a small block + using (var array = defaultArrayFactory.CreateMemoryBackedArray(SmallSize)) + { + Assert.AreEqual(SmallSize, array.Length); + Assert.IsInstanceOf(nativeMemoryArrayType, array, "64-bit should always use NativeMemoryArray for compatible types when available."); + } + + // test 64-bit with a huge block + using (var array = defaultArrayFactory.CreateMemoryBackedArray(LargeSize)) + { + Assert.AreEqual(LargeSize, array.Length); + Assert.IsInstanceOf(nativeMemoryArrayType, array, "64-bit should always use NativeMemoryArray for compatible types when available."); + } + } + finally + { + defaultArrayFactory.Is32BitProcess = old32BitProcessValue; + } + } + [Test] - public void DefaultArrayFactoryShouldReturnProperMemoryBackedArray() + public void DefaultArrayFactoryShouldNeverUseNativeMemoryArrayForIncompatibleType() { - const long SizeToUse = 592; - using (var array = Context.ArrayFactory.CreateMemoryBackedArray(SizeToUse)) + using (var array = Context.ArrayFactory.CreateMemoryBackedArray(1)) { - Assert.AreEqual(SizeToUse, array.Length, "Default returns an array that doesn't even obey the contract."); - Assert.IsInstanceOf>(array, "Default should return a standard MemoryArray to ensure perfect backwards-compatibility."); + Assert.IsInstanceOf>(array, "Never use NativeMemoryArray for arbitrary reference types."); } + + using (var array = Context.ArrayFactory.CreateMemoryBackedArray(1)) + { + Assert.IsInstanceOf>(array, "Never use NativeMemoryArray for structs that embed arbitrary reference types."); + } + } + + private struct StructWithReferences + { + private object obj; } } }