Skip to content

Commit 4a92d80

Browse files
filmorlostmsu
andauthored
Improve performance of unwrapping .NET objects passed from Python (#930)
This addresses the following scenario: 1. A C# object `a` is created and filled with some data. 2. `a` is passed via Python.NET to Python. To do that Python.NET creates a wrapper object `w`, and stores reference to `a` in one of its fields. 3. Python code later passes `w` back to C#, e.g. calls `SomeCSharpMethod(w)`. 4. Python.NET has to unwrap `w`, so it reads the reference to `a` from it. Prior to this change in 4. Python.NET had to determine what kind of an object `a` is. If it is an exception, a different offset needed to be used. That check was very expensive (up to 4 calls into Python interpreter). This change replaces that check with computing offset unconditionally by subtracting a constant from the object size (which is read from the wrapper), thus avoiding calls to Python interpreter. Co-authored-by: Victor Milovanov <lost@losttech.software>
1 parent 782eff8 commit 4a92d80

10 files changed

+113
-51
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ This document follows the conventions laid out in [Keep a CHANGELOG][].
2424
- Added argument types information to "No method matches given arguments" message
2525
- Moved wheel import in setup.py inside of a try/except to prevent pip collection failures
2626
- Removes PyLong_GetMax and PyClass_New when targetting Python3
27+
- Improved performance of calls from Python to C#
2728
- Added support for converting python iterators to C# arrays
2829
- Changed usage of obselete function GetDelegateForFunctionPointer(IntPtr, Type) to GetDelegateForFunctionPointer<TDelegate>(IntPtr)
2930
- When calling C# from Python, enable passing argument of any type to a parameter of C# type `object` by wrapping it into `PyObject` instance. ([#881][i881])

src/perf_tests/BenchmarkTests.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,14 @@ public void SetUp()
3030
public void ReadInt64Property()
3131
{
3232
double optimisticPerfRatio = GetOptimisticPerfRatio(this.summary.Reports);
33-
AssertPerformanceIsBetterOrSame(optimisticPerfRatio, target: 0.66);
33+
AssertPerformanceIsBetterOrSame(optimisticPerfRatio, target: 0.57);
3434
}
3535

3636
[Test]
3737
public void WriteInt64Property()
3838
{
3939
double optimisticPerfRatio = GetOptimisticPerfRatio(this.summary.Reports);
40-
AssertPerformanceIsBetterOrSame(optimisticPerfRatio, target: 0.64);
40+
AssertPerformanceIsBetterOrSame(optimisticPerfRatio, target: 0.57);
4141
}
4242

4343
static double GetOptimisticPerfRatio(

src/runtime/classbase.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ public static IntPtr tp_repr(IntPtr ob)
291291
public static void tp_dealloc(IntPtr ob)
292292
{
293293
ManagedType self = GetManagedObject(ob);
294-
IntPtr dict = Marshal.ReadIntPtr(ob, ObjectOffset.DictOffset(ob));
294+
IntPtr dict = Marshal.ReadIntPtr(ob, ObjectOffset.TypeDictOffset(self.tpHandle));
295295
if (dict != IntPtr.Zero)
296296
{
297297
Runtime.XDecref(dict);

src/runtime/classderived.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -877,7 +877,7 @@ public static void Finalize(IPythonDerivedType obj)
877877
// the C# object is being destroyed which must mean there are no more
878878
// references to the Python object as well so now we can dealloc the
879879
// python object.
880-
IntPtr dict = Marshal.ReadIntPtr(self.pyHandle, ObjectOffset.DictOffset(self.pyHandle));
880+
IntPtr dict = Marshal.ReadIntPtr(self.pyHandle, ObjectOffset.TypeDictOffset(self.tpHandle));
881881
if (dict != IntPtr.Zero)
882882
{
883883
Runtime.XDecref(dict);

src/runtime/clrobject.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ internal CLRObject(object ob, IntPtr tp)
1414
long flags = Util.ReadCLong(tp, TypeOffset.tp_flags);
1515
if ((flags & TypeFlags.Subclass) != 0)
1616
{
17-
IntPtr dict = Marshal.ReadIntPtr(py, ObjectOffset.DictOffset(tp));
17+
IntPtr dict = Marshal.ReadIntPtr(py, ObjectOffset.TypeDictOffset(tp));
1818
if (dict == IntPtr.Zero)
1919
{
2020
dict = Runtime.PyDict_New();
21-
Marshal.WriteIntPtr(py, ObjectOffset.DictOffset(tp), dict);
21+
Marshal.WriteIntPtr(py, ObjectOffset.TypeDictOffset(tp), dict);
2222
}
2323
}
2424

src/runtime/interop.cs

+96-40
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections;
3-
using System.Collections.Specialized;
3+
using System.Collections.Generic;
4+
using System.Diagnostics;
45
using System.Runtime.InteropServices;
56
using System.Reflection;
67
using System.Text;
@@ -68,11 +69,47 @@ public ModulePropertyAttribute()
6869
}
6970
}
7071

72+
internal static class ManagedDataOffsets
73+
{
74+
static ManagedDataOffsets()
75+
{
76+
FieldInfo[] fi = typeof(ManagedDataOffsets).GetFields(BindingFlags.Static | BindingFlags.Public);
77+
for (int i = 0; i < fi.Length; i++)
78+
{
79+
fi[i].SetValue(null, -(i * IntPtr.Size) - IntPtr.Size);
80+
}
7181

72-
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
73-
internal class ObjectOffset
82+
size = fi.Length * IntPtr.Size;
83+
}
84+
85+
public static readonly int ob_data;
86+
public static readonly int ob_dict;
87+
88+
private static int BaseOffset(IntPtr type)
89+
{
90+
Debug.Assert(type != IntPtr.Zero);
91+
int typeSize = Marshal.ReadInt32(type, TypeOffset.tp_basicsize);
92+
Debug.Assert(typeSize > 0 && typeSize <= ExceptionOffset.Size());
93+
return typeSize;
94+
}
95+
public static int DataOffset(IntPtr type)
96+
{
97+
return BaseOffset(type) + ob_data;
98+
}
99+
100+
public static int DictOffset(IntPtr type)
101+
{
102+
return BaseOffset(type) + ob_dict;
103+
}
104+
105+
public static int Size { get { return size; } }
106+
107+
private static readonly int size;
108+
}
109+
110+
internal static class OriginalObjectOffsets
74111
{
75-
static ObjectOffset()
112+
static OriginalObjectOffsets()
76113
{
77114
int size = IntPtr.Size;
78115
var n = 0; // Py_TRACE_REFS add two pointers to PyObject_HEAD
@@ -83,42 +120,58 @@ static ObjectOffset()
83120
#endif
84121
ob_refcnt = (n + 0) * size;
85122
ob_type = (n + 1) * size;
86-
ob_dict = (n + 2) * size;
87-
ob_data = (n + 3) * size;
88123
}
89124

90-
public static int magic(IntPtr ob)
125+
public static int Size { get { return size; } }
126+
127+
private static readonly int size =
128+
#if PYTHON_WITH_PYDEBUG
129+
4 * IntPtr.Size;
130+
#else
131+
2 * IntPtr.Size;
132+
#endif
133+
134+
#if PYTHON_WITH_PYDEBUG
135+
public static int _ob_next;
136+
public static int _ob_prev;
137+
#endif
138+
public static int ob_refcnt;
139+
public static int ob_type;
140+
}
141+
142+
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
143+
internal class ObjectOffset
144+
{
145+
static ObjectOffset()
146+
{
147+
#if PYTHON_WITH_PYDEBUG
148+
_ob_next = OriginalObjectOffsets._ob_next;
149+
_ob_prev = OriginalObjectOffsets._ob_prev;
150+
#endif
151+
ob_refcnt = OriginalObjectOffsets.ob_refcnt;
152+
ob_type = OriginalObjectOffsets.ob_type;
153+
154+
size = OriginalObjectOffsets.Size + ManagedDataOffsets.Size;
155+
}
156+
157+
public static int magic(IntPtr type)
91158
{
92-
if ((Runtime.PyObject_TypeCheck(ob, Exceptions.BaseException) ||
93-
(Runtime.PyType_Check(ob) && Runtime.PyType_IsSubtype(ob, Exceptions.BaseException))))
94-
{
95-
return ExceptionOffset.ob_data;
96-
}
97-
return ob_data;
159+
return ManagedDataOffsets.DataOffset(type);
98160
}
99161

100-
public static int DictOffset(IntPtr ob)
162+
public static int TypeDictOffset(IntPtr type)
101163
{
102-
if ((Runtime.PyObject_TypeCheck(ob, Exceptions.BaseException) ||
103-
(Runtime.PyType_Check(ob) && Runtime.PyType_IsSubtype(ob, Exceptions.BaseException))))
104-
{
105-
return ExceptionOffset.ob_dict;
106-
}
107-
return ob_dict;
164+
return ManagedDataOffsets.DictOffset(type);
108165
}
109166

110-
public static int Size(IntPtr ob)
167+
public static int Size(IntPtr pyType)
111168
{
112-
if ((Runtime.PyObject_TypeCheck(ob, Exceptions.BaseException) ||
113-
(Runtime.PyType_Check(ob) && Runtime.PyType_IsSubtype(ob, Exceptions.BaseException))))
169+
if (IsException(pyType))
114170
{
115171
return ExceptionOffset.Size();
116172
}
117-
#if PYTHON_WITH_PYDEBUG
118-
return 6 * IntPtr.Size;
119-
#else
120-
return 4 * IntPtr.Size;
121-
#endif
173+
174+
return size;
122175
}
123176

124177
#if PYTHON_WITH_PYDEBUG
@@ -127,8 +180,15 @@ public static int Size(IntPtr ob)
127180
#endif
128181
public static int ob_refcnt;
129182
public static int ob_type;
130-
private static int ob_dict;
131-
private static int ob_data;
183+
private static readonly int size;
184+
185+
private static bool IsException(IntPtr pyObject)
186+
{
187+
var type = Runtime.PyObject_TYPE(pyObject);
188+
return Runtime.PyType_IsSameAsOrSubtype(type, ofType: Exceptions.BaseException)
189+
|| Runtime.PyType_IsSameAsOrSubtype(type, ofType: Runtime.PyTypeType)
190+
&& Runtime.PyType_IsSubtype(pyObject, Exceptions.BaseException);
191+
}
132192
}
133193

134194
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
@@ -137,19 +197,17 @@ internal class ExceptionOffset
137197
static ExceptionOffset()
138198
{
139199
Type type = typeof(ExceptionOffset);
140-
FieldInfo[] fi = type.GetFields();
141-
int size = IntPtr.Size;
200+
FieldInfo[] fi = type.GetFields(BindingFlags.Static | BindingFlags.Public);
142201
for (int i = 0; i < fi.Length; i++)
143202
{
144-
fi[i].SetValue(null, (i * size) + ObjectOffset.ob_type + size);
203+
fi[i].SetValue(null, (i * IntPtr.Size) + OriginalObjectOffsets.Size);
145204
}
146-
}
147205

148-
public static int Size()
149-
{
150-
return ob_data + IntPtr.Size;
206+
size = fi.Length * IntPtr.Size + OriginalObjectOffsets.Size + ManagedDataOffsets.Size;
151207
}
152208

209+
public static int Size() { return size; }
210+
153211
// PyException_HEAD
154212
// (start after PyObject_HEAD)
155213
public static int dict = 0;
@@ -163,9 +221,7 @@ public static int Size()
163221
public static int suppress_context = 0;
164222
#endif
165223

166-
// extra c# data
167-
public static int ob_dict;
168-
public static int ob_data;
224+
private static readonly int size;
169225
}
170226

171227

src/runtime/managedtype.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ internal static ManagedType GetManagedObject(IntPtr ob)
3333
{
3434
IntPtr op = tp == ob
3535
? Marshal.ReadIntPtr(tp, TypeOffset.magic())
36-
: Marshal.ReadIntPtr(ob, ObjectOffset.magic(ob));
36+
: Marshal.ReadIntPtr(ob, ObjectOffset.magic(tp));
3737
if (op == IntPtr.Zero)
3838
{
3939
return null;

src/runtime/moduleobject.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public ModuleObject(string name)
5454
Runtime.XDecref(pyfilename);
5555
Runtime.XDecref(pydocstring);
5656

57-
Marshal.WriteIntPtr(pyHandle, ObjectOffset.DictOffset(pyHandle), dict);
57+
Marshal.WriteIntPtr(pyHandle, ObjectOffset.TypeDictOffset(tpHandle), dict);
5858

5959
InitializeModuleMembers();
6060
}

src/runtime/runtime.cs

+5
Original file line numberDiff line numberDiff line change
@@ -1889,6 +1889,11 @@ internal static bool PyObject_TypeCheck(IntPtr ob, IntPtr tp)
18891889
return (t == tp) || PyType_IsSubtype(t, tp);
18901890
}
18911891

1892+
internal static bool PyType_IsSameAsOrSubtype(IntPtr type, IntPtr ofType)
1893+
{
1894+
return (type == ofType) || PyType_IsSubtype(type, ofType);
1895+
}
1896+
18921897
[DllImport(_PythonDll, CallingConvention = CallingConvention.Cdecl)]
18931898
internal static extern IntPtr PyType_GenericNew(IntPtr type, IntPtr args, IntPtr kw);
18941899

src/runtime/typemanager.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ internal static IntPtr CreateType(Type impl)
8787
// Set tp_basicsize to the size of our managed instance objects.
8888
Marshal.WriteIntPtr(type, TypeOffset.tp_basicsize, (IntPtr)ob_size);
8989

90-
var offset = (IntPtr)ObjectOffset.DictOffset(type);
90+
var offset = (IntPtr)ObjectOffset.TypeDictOffset(type);
9191
Marshal.WriteIntPtr(type, TypeOffset.tp_dictoffset, offset);
9292

9393
InitializeSlots(type, impl);
@@ -125,17 +125,17 @@ internal static IntPtr CreateType(ManagedType impl, Type clrType)
125125

126126
IntPtr base_ = IntPtr.Zero;
127127
int ob_size = ObjectOffset.Size(Runtime.PyTypeType);
128-
int tp_dictoffset = ObjectOffset.DictOffset(Runtime.PyTypeType);
129128

130129
// XXX Hack, use a different base class for System.Exception
131130
// Python 2.5+ allows new style class exceptions but they *must*
132131
// subclass BaseException (or better Exception).
133132
if (typeof(Exception).IsAssignableFrom(clrType))
134133
{
135134
ob_size = ObjectOffset.Size(Exceptions.Exception);
136-
tp_dictoffset = ObjectOffset.DictOffset(Exceptions.Exception);
137135
}
138136

137+
int tp_dictoffset = ob_size + ManagedDataOffsets.ob_dict;
138+
139139
if (clrType == typeof(Exception))
140140
{
141141
base_ = Exceptions.Exception;

0 commit comments

Comments
 (0)