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; pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy