diff --git a/CHANGELOG.md b/CHANGELOG.md index 17c1780ee..1658a6f95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,8 @@ This document follows the conventions laid out in [Keep a CHANGELOG][]. ## [unreleased][] ### Added - +- Improved performance. String marshaling between python and clr now cached. + Cache reduces GC pressure and saves from extensive memory copying. - Added support for embedding python into dotnet core 2.0 (NetStandard 2.0) - Added new build system (pythonnet.15.sln) based on dotnetcore-sdk/xplat(crossplatform msbuild). Currently there two side-by-side build systems that produces the same output (net40) from the same sources. diff --git a/src/embed_tests/Python.EmbeddingTest.csproj b/src/embed_tests/Python.EmbeddingTest.csproj index 6aa48becc..caffa7256 100644 --- a/src/embed_tests/Python.EmbeddingTest.csproj +++ b/src/embed_tests/Python.EmbeddingTest.csproj @@ -104,6 +104,7 @@ + diff --git a/src/embed_tests/TestPythonEngineProperties.cs b/src/embed_tests/TestPythonEngineProperties.cs index 243349b82..d95942577 100644 --- a/src/embed_tests/TestPythonEngineProperties.cs +++ b/src/embed_tests/TestPythonEngineProperties.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using NUnit.Framework; using Python.Runtime; diff --git a/src/embed_tests/TestRuntime.cs b/src/embed_tests/TestRuntime.cs index ac1fa1ac0..aeef28135 100644 --- a/src/embed_tests/TestRuntime.cs +++ b/src/embed_tests/TestRuntime.cs @@ -34,7 +34,7 @@ public static void PlatformCache() // Don't shut down the runtime: if the python engine was initialized // but not shut down by another test, we'd end up in a bad state. - } + } [Test] public static void Py_IsInitializedValue() diff --git a/src/embed_tests/TestsSuite.cs b/src/embed_tests/TestsSuite.cs new file mode 100644 index 000000000..44ce2d4b8 --- /dev/null +++ b/src/embed_tests/TestsSuite.cs @@ -0,0 +1,18 @@ +using NUnit.Framework; +using Python.Runtime; + +namespace Python.EmbeddingTest +{ + [SetUpFixture] + public class TestsSuite + { + [OneTimeTearDown] + public void FinalCleanup() + { + if (PythonEngine.IsInitialized) + { + PythonEngine.Shutdown(); + } + } + } +} diff --git a/src/runtime/CustomMarshaler.cs b/src/runtime/CustomMarshaler.cs index b51911816..507710f87 100644 --- a/src/runtime/CustomMarshaler.cs +++ b/src/runtime/CustomMarshaler.cs @@ -18,7 +18,7 @@ public object MarshalNativeToManaged(IntPtr pNativeData) public abstract IntPtr MarshalManagedToNative(object managedObj); - public void CleanUpNativeData(IntPtr pNativeData) + public virtual void CleanUpNativeData(IntPtr pNativeData) { Marshal.FreeHGlobal(pNativeData); } @@ -44,7 +44,12 @@ internal class UcsMarshaler : MarshalerBase private static readonly MarshalerBase Instance = new UcsMarshaler(); private static readonly Encoding PyEncoding = Runtime.PyEncoding; - public override IntPtr MarshalManagedToNative(object managedObj) + private const int MaxStringLength = 100; + private const int MaxItemSize = 4 * (MaxStringLength + 1); + private static readonly EncodedStringsFifoDictionary EncodedStringsDictionary = + new EncodedStringsFifoDictionary(10000, MaxItemSize); + + public override unsafe IntPtr MarshalManagedToNative(object managedObj) { var s = managedObj as string; @@ -53,16 +58,36 @@ public override IntPtr MarshalManagedToNative(object managedObj) return IntPtr.Zero; } - byte[] bStr = PyEncoding.GetBytes(s + "\0"); - IntPtr mem = Marshal.AllocHGlobal(bStr.Length); - try + IntPtr mem; + int stringBytesCount; + if (s.Length <= MaxStringLength) { - Marshal.Copy(bStr, 0, mem, bStr.Length); + if (EncodedStringsDictionary.TryGetValue(s, out mem)) + { + return mem; + } + + stringBytesCount = PyEncoding.GetByteCount(s); + mem = EncodedStringsDictionary.AddUnsafe(s); } - catch (Exception) + else { - Marshal.FreeHGlobal(mem); - throw; + stringBytesCount = PyEncoding.GetByteCount(s); + mem = Marshal.AllocHGlobal(stringBytesCount + 4); + } + + fixed (char* str = s) + { + try + { + PyEncoding.GetBytes(str, s.Length, (byte*)mem, stringBytesCount); + } + catch + { + // Do nothing with this. Very strange problem. + } + + *(int*)(mem + stringBytesCount) = 0; } return mem; @@ -106,6 +131,14 @@ public static int GetUnicodeByteLength(IntPtr p) } } + public override void CleanUpNativeData(IntPtr pNativeData) + { + if (!EncodedStringsDictionary.IsKnownPtr(pNativeData)) + { + base.CleanUpNativeData(pNativeData); + } + } + /// /// Utility function for Marshaling Unicode on PY3 and AnsiStr on PY2. /// Use on functions whose Input signatures changed between PY2/PY3. @@ -118,11 +151,29 @@ public static int GetUnicodeByteLength(IntPtr p) /// /// You MUST deallocate the IntPtr of the Return when done with it. /// - public static IntPtr Py3UnicodePy2StringtoPtr(string s) + public unsafe static IntPtr Py3UnicodePy2StringtoPtr(string s) { - return Runtime.IsPython3 - ? Instance.MarshalManagedToNative(s) - : Marshal.StringToHGlobalAnsi(s); + if (Runtime.IsPython3) + { + int stringBytesCount = PyEncoding.GetByteCount(s); + IntPtr mem = Marshal.AllocHGlobal(stringBytesCount + 4); + fixed (char* str = s) + { + try + { + PyEncoding.GetBytes(str, s.Length, (byte*)mem, stringBytesCount); + } + catch + { + // Do nothing with this. Very strange problem. + } + + *(int*)(mem + stringBytesCount) = 0; + } + return mem; + } + + return Marshal.StringToHGlobalAnsi(s); } /// @@ -208,7 +259,12 @@ internal class Utf8Marshaler : MarshalerBase private static readonly MarshalerBase Instance = new Utf8Marshaler(); private static readonly Encoding PyEncoding = Encoding.UTF8; - public override IntPtr MarshalManagedToNative(object managedObj) + private const int MaxStringLength = 100; + + private static readonly EncodedStringsFifoDictionary EncodedStringsDictionary = + new EncodedStringsFifoDictionary(10000, 4 * (MaxStringLength + 1)); + + public override unsafe IntPtr MarshalManagedToNative(object managedObj) { var s = managedObj as string; @@ -217,21 +273,49 @@ public override IntPtr MarshalManagedToNative(object managedObj) return IntPtr.Zero; } - byte[] bStr = PyEncoding.GetBytes(s + "\0"); - IntPtr mem = Marshal.AllocHGlobal(bStr.Length); - try + IntPtr mem; + int stringBytesCount; + if (s.Length <= MaxStringLength) { - Marshal.Copy(bStr, 0, mem, bStr.Length); + if (EncodedStringsDictionary.TryGetValue(s, out mem)) + { + return mem; + } + + stringBytesCount = PyEncoding.GetByteCount(s); + mem = EncodedStringsDictionary.AddUnsafe(s); } - catch (Exception) + else { - Marshal.FreeHGlobal(mem); - throw; + stringBytesCount = PyEncoding.GetByteCount(s); + mem = Marshal.AllocHGlobal(stringBytesCount + 1); + } + + fixed (char* str = s) + { + try + { + PyEncoding.GetBytes(str, s.Length, (byte*)mem, stringBytesCount); + } + catch + { + // Do nothing with this. Very strange problem. + } + + ((byte*)mem)[stringBytesCount] = 0; } return mem; } + public override void CleanUpNativeData(IntPtr pNativeData) + { + if (!EncodedStringsDictionary.IsKnownPtr(pNativeData)) + { + base.CleanUpNativeData(pNativeData); + } + } + public static ICustomMarshaler GetInstance(string cookie) { return Instance; diff --git a/src/runtime/Python.Runtime.csproj b/src/runtime/Python.Runtime.csproj index fc155ca91..00a290988 100644 --- a/src/runtime/Python.Runtime.csproj +++ b/src/runtime/Python.Runtime.csproj @@ -76,6 +76,12 @@ + + + + + + Properties\SharedAssemblyInfo.cs diff --git a/src/runtime/assemblymanager.cs b/src/runtime/assemblymanager.cs index 3085bb639..039d44951 100644 --- a/src/runtime/assemblymanager.cs +++ b/src/runtime/assemblymanager.cs @@ -27,6 +27,7 @@ internal class AssemblyManager // So for multidomain support it is better to have the dict. recreated for each app-domain initialization private static ConcurrentDictionary> namespaces = new ConcurrentDictionary>(); + //private static Dictionary> generics; private static AssemblyLoadEventHandler lhandler; private static ResolveEventHandler rhandler; diff --git a/src/runtime/perf_utils/EncodedStringsFifoDictionary.cs b/src/runtime/perf_utils/EncodedStringsFifoDictionary.cs new file mode 100644 index 000000000..74a88f0ec --- /dev/null +++ b/src/runtime/perf_utils/EncodedStringsFifoDictionary.cs @@ -0,0 +1,73 @@ +using System; + +namespace Python.Runtime +{ + using System.Runtime.InteropServices; + + public class EncodedStringsFifoDictionary: IDisposable + { + private readonly FifoDictionary _innerDictionary; + + private readonly IntPtr _rawMemory; + + private readonly int _allocatedSize; + + public EncodedStringsFifoDictionary(int capacity, int maxItemSize) + { + if (maxItemSize < 1) + { + throw new ArgumentOutOfRangeException( + nameof(maxItemSize), + "Maximum item size should be non-zero positive."); + } + + _innerDictionary = new FifoDictionary(capacity); + _allocatedSize = maxItemSize * capacity; + _rawMemory = Marshal.AllocHGlobal(_allocatedSize); + + MaxItemSize = maxItemSize; + } + + public int MaxItemSize { get; } + + public bool TryGetValue(string key, out IntPtr value) + { + return _innerDictionary.TryGetValue(key, out value); + } + + public IntPtr AddUnsafe(string key) + { + int nextSlot = _innerDictionary.NextSlotToAdd; + IntPtr ptr = _rawMemory + (MaxItemSize * nextSlot); + _innerDictionary.AddUnsafe(key, ptr); + return ptr; + } + + public bool IsKnownPtr(IntPtr ptr) + { + var uptr = (ulong)ptr; + var umem = (ulong)_rawMemory; + + return uptr >= umem && uptr < umem + (ulong)_allocatedSize; + } + + private void ReleaseUnmanagedResources() + { + if (_rawMemory != IntPtr.Zero) + { + Marshal.FreeHGlobal(_rawMemory); + } + } + + public void Dispose() + { + ReleaseUnmanagedResources(); + GC.SuppressFinalize(this); + } + + ~EncodedStringsFifoDictionary() + { + ReleaseUnmanagedResources(); + } + } +} diff --git a/src/runtime/perf_utils/EncodingGetStringPolyfill.cs b/src/runtime/perf_utils/EncodingGetStringPolyfill.cs new file mode 100644 index 000000000..ac1d0ddcf --- /dev/null +++ b/src/runtime/perf_utils/EncodingGetStringPolyfill.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Text; + +namespace Python.Runtime +{ +#if !NETSTANDARD + /// + /// This polyfill is thread unsafe. + /// + [CLSCompliant(false)] + public static class EncodingGetStringPolyfill + { + private static readonly MethodInfo PlatformGetStringMethodInfo = + typeof(Encoding).GetMethod( + "GetString", + BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, + new[] + { + typeof(byte*), typeof(int) + }, null); + + private static readonly byte[] StdDecodeBuffer = PlatformGetStringMethodInfo == null ? new byte[1024 * 1024] : null; + + private static Dictionary PlatformGetStringMethodsDelegatesCache = new Dictionary(); + + private unsafe delegate string EncodingGetStringUnsafeDelegate(byte* pstr, int size); + + public unsafe static string GetString(this Encoding encoding, byte* pstr, int size) + { + if (PlatformGetStringMethodInfo != null) + { + EncodingGetStringUnsafeDelegate getStringDelegate; + if (!PlatformGetStringMethodsDelegatesCache.TryGetValue(encoding, out getStringDelegate)) + { + getStringDelegate = + (EncodingGetStringUnsafeDelegate)Delegate.CreateDelegate( + typeof(EncodingGetStringUnsafeDelegate), encoding, PlatformGetStringMethodInfo); + PlatformGetStringMethodsDelegatesCache.Add(encoding, getStringDelegate); + } + return getStringDelegate(pstr, size); + } + + byte[] buffer = size <= StdDecodeBuffer.Length ? StdDecodeBuffer : new byte[size]; + Marshal.Copy((IntPtr)pstr, buffer, 0, size); + return encoding.GetString(buffer, 0, size); + } + } +#endif + +} diff --git a/src/runtime/perf_utils/FifoDictionary.cs b/src/runtime/perf_utils/FifoDictionary.cs new file mode 100644 index 000000000..3af127de1 --- /dev/null +++ b/src/runtime/perf_utils/FifoDictionary.cs @@ -0,0 +1,62 @@ +using System; + +namespace Python.Runtime +{ + using System.Collections.Generic; + + public class FifoDictionary + { + private readonly Dictionary _innerDictionary; + + private readonly KeyValuePair[] _fifoList; + + private bool _hasEmptySlots = true; + + public FifoDictionary(int capacity) + { + if (capacity <= 0) + { + throw new ArgumentOutOfRangeException(nameof(capacity), "Capacity should be non-zero positive."); + } + + _innerDictionary = new Dictionary(capacity); + _fifoList = new KeyValuePair[capacity]; + + Capacity = capacity; + } + + public bool TryGetValue(TKey key, out TValue value) + { + int index; + if (_innerDictionary.TryGetValue(key, out index)) + { + value =_fifoList[index].Value; + return true; + } + + value = default(TValue); + return false; + } + + public void AddUnsafe(TKey key, TValue value) + { + if (!_hasEmptySlots) + { + _innerDictionary.Remove(_fifoList[NextSlotToAdd].Key); + } + + _innerDictionary.Add(key, NextSlotToAdd); + _fifoList[NextSlotToAdd] = new KeyValuePair(key, value); + + NextSlotToAdd++; + if (NextSlotToAdd >= Capacity) + { + _hasEmptySlots = false; + NextSlotToAdd = 0; + } + } + + public int NextSlotToAdd { get; private set; } + public int Capacity { get; } + } +} diff --git a/src/runtime/perf_utils/RawImmutableMemBlock.cs b/src/runtime/perf_utils/RawImmutableMemBlock.cs new file mode 100644 index 000000000..6230bf5ec --- /dev/null +++ b/src/runtime/perf_utils/RawImmutableMemBlock.cs @@ -0,0 +1,84 @@ +namespace Python.Runtime +{ + using System; + + public struct RawImmutableMemBlock: IEquatable + { + private readonly int _hash; + + public RawImmutableMemBlock(IntPtr ptr, int size) + { + if (ptr == IntPtr.Zero) + { + throw new ArgumentException("Memory pointer should not be zero", nameof(ptr)); + } + + if (size < 0) + { + throw new ArgumentOutOfRangeException(nameof(size), "Size should be zero or positive."); + } + + Ptr = ptr; + Size = size; + _hash = RawMemUtils.FastXorHash(ptr, size); + } + + public RawImmutableMemBlock(RawImmutableMemBlock memBlock, IntPtr newPtr) + { + if (memBlock.Ptr == IntPtr.Zero) + { + throw new ArgumentException("Cannot copy non initialized RawImmutableMemBlock structure.", nameof(memBlock)); + } + + if (newPtr == IntPtr.Zero) + { + throw new ArgumentException("Cannot copy to zero pointer."); + } + + RawMemUtils.CopyMemBlocks(memBlock.Ptr, newPtr, memBlock.Size); + Ptr = newPtr; + Size = memBlock.Size; + _hash = memBlock._hash; + } + + public IntPtr Ptr { get; } + + public int Size { get; } + + public bool Equals(RawImmutableMemBlock other) + { + bool preEqual = _hash == other._hash && Size == other.Size; + if (!preEqual) + { + return false; + } + + return RawMemUtils.CompareMemBlocks(Ptr, other.Ptr, Size); + } + + /// + public override bool Equals(object obj) + { + return obj is RawImmutableMemBlock && Equals((RawImmutableMemBlock)obj); + } + + /// + public override int GetHashCode() + { + unchecked + { + return (_hash * 397) ^ Size; + } + } + + public static bool operator ==(RawImmutableMemBlock left, RawImmutableMemBlock right) + { + return left.Equals(right); + } + + public static bool operator !=(RawImmutableMemBlock left, RawImmutableMemBlock right) + { + return !left.Equals(right); + } + } +} diff --git a/src/runtime/perf_utils/RawMemUtils.cs b/src/runtime/perf_utils/RawMemUtils.cs new file mode 100644 index 000000000..694acd1e8 --- /dev/null +++ b/src/runtime/perf_utils/RawMemUtils.cs @@ -0,0 +1,140 @@ +namespace Python.Runtime +{ + using System; + + public static class RawMemUtils + { + public static unsafe bool CopyMemBlocks(IntPtr src, IntPtr dest, int size) + { + // XOR with 64 bit step + var p64_1 = (ulong*)src; + var p64_2 = (ulong*)dest; + int c64count = size >> 3; + + int i = 0; + while (i + /// Calculating simple 32 bit xor hash for raw memory. + /// + /// Memory pointer. + /// Size to hash. + /// 32 bit hash the in signed int format. + public static unsafe int FastXorHash(IntPtr mem, int size) + { + unchecked + { + // XOR with 64 bit step + ulong r64 = 0; + var p64 = (ulong*)mem; + var pn = (byte*)(mem + (size & ~7)); + while (p64 < pn) + { + r64 ^= *p64++; + } + + uint r32 = (uint)r64 ^ (uint)(r64 >> 32); + if ((size & 4) != 0) + { + r32 ^= *(uint*)pn; + pn += 4; + } + + if ((size & 2) != 0) + { + r32 ^= *(ushort*)pn; + pn += 2; + } + + if ((size & 1) != 0) + { + r32 ^= *pn; + } + + return (int)r32; + } + } + } +} diff --git a/src/runtime/perf_utils/RawMemoryFifoDictionary.cs b/src/runtime/perf_utils/RawMemoryFifoDictionary.cs new file mode 100644 index 000000000..d845271d5 --- /dev/null +++ b/src/runtime/perf_utils/RawMemoryFifoDictionary.cs @@ -0,0 +1,59 @@ +namespace Python.Runtime +{ + using System; + using System.Runtime.InteropServices; + + public class RawMemoryFifoDictionary : IDisposable + { + private readonly FifoDictionary _innerDictionary; + + private readonly IntPtr _rawMemory; + + public RawMemoryFifoDictionary(int capacity, int maxItemSize) + { + if (maxItemSize < 1) + { + throw new ArgumentOutOfRangeException( + nameof(maxItemSize), + "Maximum item size should be non-zero positive."); + } + + MaxItemSize = maxItemSize; + _innerDictionary = new FifoDictionary(capacity); + _rawMemory = Marshal.AllocHGlobal(maxItemSize*capacity); + } + + ~RawMemoryFifoDictionary() + { + ReleaseUnmanagedResources(); + } + + public int MaxItemSize { get; } + + public bool TryGetValue(RawImmutableMemBlock key, out TValue value) + { + return _innerDictionary.TryGetValue(key, out value); + } + + public void AddUnsafe(RawImmutableMemBlock key, TValue value) + { + int nextSlot = _innerDictionary.NextSlotToAdd; + var localKey = new RawImmutableMemBlock(key, _rawMemory + (MaxItemSize * nextSlot)); + _innerDictionary.AddUnsafe(localKey, value); + } + + public void Dispose() + { + ReleaseUnmanagedResources(); + GC.SuppressFinalize(this); + } + + private void ReleaseUnmanagedResources() + { + if (_rawMemory != IntPtr.Zero) + { + Marshal.FreeHGlobal(_rawMemory); + } + } + } +} diff --git a/src/runtime/pyobject.cs b/src/runtime/pyobject.cs index 7dca85545..02eca3c44 100644 --- a/src/runtime/pyobject.cs +++ b/src/runtime/pyobject.cs @@ -6,6 +6,9 @@ namespace Python.Runtime { + using System.Reflection; + using System.Threading; + /// /// Represents a generic Python object. The methods of this class are /// generally equivalent to the Python "abstract object API". See @@ -261,9 +264,34 @@ public PyObject GetAttr(PyObject name) { throw new PythonException(); } + return new PyObject(op); } + public T GetAttr(string name) + { + IntPtr op = Runtime.PyObject_GetAttrString(obj, name); + + if (op == IntPtr.Zero) + { + throw new PythonException(); + } + + try + { + object resultObj; + if (!Converter.ToManaged(op, typeof(T), out resultObj, false)) + { + throw new InvalidCastException("cannot convert object to target type"); + } + + return (T)resultObj; + } + finally + { + Runtime.XDecref(op); + } + } /// /// GetAttr Method @@ -1089,6 +1117,12 @@ public override bool TryInvoke(InvokeBinder binder, object[] args, out object re public override bool TryConvert(ConvertBinder binder, out object result) { + if (typeof(PyObject).IsAssignableFrom(binder.Type)) + { + result = this; + return true; + } + return Converter.ToManaged(this.obj, binder.Type, out result, false); } diff --git a/src/runtime/runtime.cs b/src/runtime/runtime.cs index d4cb85583..7e93b65fe 100644 --- a/src/runtime/runtime.cs +++ b/src/runtime/runtime.cs @@ -266,6 +266,12 @@ public enum MachineType /// internal static readonly Encoding PyEncoding = _UCS == 2 ? Encoding.Unicode : Encoding.UTF32; + /// + /// 4Mb of Python strings to .Net strings cache. + /// + private static readonly RawMemoryFifoDictionary UcsStringsInternDictionary = + new RawMemoryFifoDictionary(10000, 100 * _UCS); + /// /// Initialize the runtime... /// @@ -1524,7 +1530,7 @@ internal static IntPtr PyUnicode_FromString(string s) /// /// PyStringType or PyUnicodeType object to convert /// Managed String - internal static string GetManagedString(IntPtr op) + internal static unsafe string GetManagedString(IntPtr op) { IntPtr type = PyObject_TYPE(op); @@ -1541,9 +1547,20 @@ internal static string GetManagedString(IntPtr op) int length = (int)PyUnicode_GetSize(op); int size = length * _UCS; - var buffer = new byte[size]; - Marshal.Copy(p, buffer, 0, size); - return PyEncoding.GetString(buffer, 0, size); + if (size <= UcsStringsInternDictionary.MaxItemSize) + { + var ucsStringMemBlock = new RawImmutableMemBlock(p, size); + string str; + if (!UcsStringsInternDictionary.TryGetValue(ucsStringMemBlock, out str)) + { + str = PyEncoding.GetString((byte*)p, size); + UcsStringsInternDictionary.AddUnsafe(ucsStringMemBlock, str); + } + + return str; + } + + return PyEncoding.GetString((byte*)p, size); } return null;