Skip to content

Commit 2dea84a

Browse files
lostmsufilmor
authored andcommitted
Improve performance of unwrapping .NET objects passed from Python
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.
1 parent f20bcf6 commit 2dea84a

9 files changed

+107
-52
lines changed

AUTHORS.md

+1-3
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,7 @@
4545
- Sam Winstanley ([@swinstanley](https://github.com/swinstanley))
4646
- Sean Freitag ([@cowboygneox](https://github.com/cowboygneox))
4747
- Serge Weinstock ([@sweinst](https://github.com/sweinst))
48-
- Simon Mourier ([@smourier](https://github.com/smourier))
49-
- Victor Milovanov ([@lostmsu](https://github.com/lostmsu))
50-
- Viktoria Kovescses ([@vkovec](https://github.com/vkovec))
48+
- Victor Milovanov([@lostmsu](https://github.com/lostmsu))
5149
- Ville M. Vainio ([@vivainio](https://github.com/vivainio))
5250
- Virgil Dupras ([@hsoft](https://github.com/hsoft))
5351
- Wenguang Yang ([@yagweb](https://github.com/yagweb))

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ This document follows the conventions laid out in [Keep a CHANGELOG][].
1616
- Added argument types information to "No method matches given arguments" message
1717
- Moved wheel import in setup.py inside of a try/except to prevent pip collection failures
1818
- Removes PyLong_GetMax and PyClass_New when targetting Python3
19+
- Improved performance of calls from Python to C#
1920

2021
### Fixed
2122

src/runtime/classbase.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ public static IntPtr tp_str(IntPtr ob)
253253
public static void tp_dealloc(IntPtr ob)
254254
{
255255
ManagedType self = GetManagedObject(ob);
256-
IntPtr dict = Marshal.ReadIntPtr(ob, ObjectOffset.DictOffset(ob));
256+
IntPtr dict = Marshal.ReadIntPtr(ob, ObjectOffset.TypeDictOffset(self.tpHandle));
257257
if (dict != IntPtr.Zero)
258258
{
259259
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;
@@ -67,11 +68,47 @@ public ModulePropertyAttribute()
6768
}
6869
}
6970

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

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

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

99-
public static int DictOffset(IntPtr ob)
156+
public static int magic(IntPtr type)
100157
{
101-
if ((Runtime.PyObject_TypeCheck(ob, Exceptions.BaseException) ||
102-
(Runtime.PyType_Check(ob) && Runtime.PyType_IsSubtype(ob, Exceptions.BaseException))))
103-
{
104-
return ExceptionOffset.ob_dict;
105-
}
106-
return ob_dict;
158+
return ManagedDataOffsets.DataOffset(type);
107159
}
108160

109-
public static int Size(IntPtr ob)
161+
public static int TypeDictOffset(IntPtr type)
110162
{
111-
if ((Runtime.PyObject_TypeCheck(ob, Exceptions.BaseException) ||
112-
(Runtime.PyType_Check(ob) && Runtime.PyType_IsSubtype(ob, Exceptions.BaseException))))
163+
return ManagedDataOffsets.DictOffset(type);
164+
}
165+
166+
public static int Size(IntPtr pyType)
167+
{
168+
if (IsException(pyType))
113169
{
114170
return ExceptionOffset.Size();
115171
}
116-
#if PYTHON_WITH_PYDEBUG
117-
return 6 * IntPtr.Size;
118-
#else
119-
return 4 * IntPtr.Size;
120-
#endif
172+
173+
return size;
121174
}
122175

123176
#if PYTHON_WITH_PYDEBUG
@@ -126,8 +179,15 @@ public static int Size(IntPtr ob)
126179
#endif
127180
public static int ob_refcnt;
128181
public static int ob_type;
129-
private static int ob_dict;
130-
private static int ob_data;
182+
private static readonly int size;
183+
184+
private static bool IsException(IntPtr pyObject)
185+
{
186+
var type = Runtime.PyObject_TYPE(pyObject);
187+
return Runtime.PyObjectType_TypeCheck(type, Exceptions.BaseException)
188+
|| Runtime.PyObjectType_TypeCheck(type, Runtime.PyTypeType)
189+
&& Runtime.PyType_IsSubtype(pyObject, Exceptions.BaseException);
190+
}
131191
}
132192

133193
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
@@ -136,19 +196,17 @@ internal class ExceptionOffset
136196
static ExceptionOffset()
137197
{
138198
Type type = typeof(ExceptionOffset);
139-
FieldInfo[] fi = type.GetFields();
140-
int size = IntPtr.Size;
199+
FieldInfo[] fi = type.GetFields(BindingFlags.Static | BindingFlags.Public);
141200
for (int i = 0; i < fi.Length; i++)
142201
{
143-
fi[i].SetValue(null, (i * size) + ObjectOffset.ob_type + size);
202+
fi[i].SetValue(null, (i * IntPtr.Size) + OriginalObjectOffsets.Size);
144203
}
145-
}
146204

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

208+
public static int Size() { return size; }
209+
152210
// PyException_HEAD
153211
// (start after PyObject_HEAD)
154212
public static int dict = 0;
@@ -162,9 +220,7 @@ public static int Size()
162220
public static int suppress_context = 0;
163221
#endif
164222

165-
// extra c# data
166-
public static int ob_dict;
167-
public static int ob_data;
223+
private static readonly int size;
168224
}
169225

170226

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
@@ -53,7 +53,7 @@ public ModuleObject(string name)
5353
Runtime.XDecref(pyfilename);
5454
Runtime.XDecref(pydocstring);
5555

56-
Marshal.WriteIntPtr(pyHandle, ObjectOffset.DictOffset(pyHandle), dict);
56+
Marshal.WriteIntPtr(pyHandle, ObjectOffset.TypeDictOffset(tpHandle), dict);
5757

5858
InitializeModuleMembers();
5959
}

src/runtime/typemanager.cs

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

88-
var offset = (IntPtr)ObjectOffset.DictOffset(type);
88+
var offset = (IntPtr)ObjectOffset.TypeDictOffset(type);
8989
Marshal.WriteIntPtr(type, TypeOffset.tp_dictoffset, offset);
9090

9191
InitializeSlots(type, impl);
@@ -123,17 +123,17 @@ internal static IntPtr CreateType(ManagedType impl, Type clrType)
123123

124124
IntPtr base_ = IntPtr.Zero;
125125
int ob_size = ObjectOffset.Size(Runtime.PyTypeType);
126-
int tp_dictoffset = ObjectOffset.DictOffset(Runtime.PyTypeType);
127126

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

135+
int tp_dictoffset = ob_size + ManagedDataOffsets.ob_dict;
136+
137137
if (clrType == typeof(Exception))
138138
{
139139
base_ = Exceptions.Exception;

0 commit comments

Comments
 (0)