1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427
|
:orphan:
Buffers and NumPy removal
=========================
NumPy was used primarily for supplying Python buffers on slicing. Numpy has
always been problematic for JPype. As an optional extra is may or may not be
built into the distribution, but if it is compiled in it is required.
Therefore, is itn't really on "extra". Therefore, removing it would make
distribution for binary versions of JPype much easier.
NumPy returns on slicing is but one part of the three uses of Python buffers in
JPype. Thus to properly remove it we need to rework to remove it we need to
review all three of the paths. These paths are
- Conversion of Python buffers to Java arrays on setArrayRange.
- Conversion of Java arrays to slices (and then from slices to buffers so that
they can be transferred to NumPy.)
- Connecting bytearray to Java direct byte buffers.
We reviewed and revised each of the paths accordingly.
On conversion of Python buffers, the implementation was dated from Python 2.6
era where there was no formal support for buffers. Thus the buffer
implementation never consulted the buffer type to see what type of object was
being transferred nor how the memory was oriented. This entire section had to
be replaced and was given the same conversion rules as NumPy. Any conversion is
possible (including lossy ones like float to bool). The rules to trigger the
conversion is by slicing just as with NumPy. By replicating the rules of NumPy
we hide the fact that NumPy is no longer used and increase typesafety. There
was a number of cases where in the past it would reinterpret cast the memory
that will now function properly. The old behavior was a useless side effect of
the implementation and was unstable with machine architecture so not likely used
be a user.
The getArrayRange portion has to be split into two pieces. Under the previous
implementation the type of the return changes from Python list to NumPy array
depending on the compile option. Thus the test suite tested different
behaviors for each. In removing NumPy we replace the Java array slice return
with a Java array. Thus the type is always consistent. The Java array that is
returned is still backed by the same array as before and has the start, end, and
step set appropriately. This does create one change in behavior as the slice
now has left assignment (just like NumPy) and before it was a copy. It is
difficult exercise this but to do so we have to copy a slice to a new variable
then use a second array dereference to assign an element. Because we converted
to either list or NumPy, the second dereference could not affect the original.
There is no way to avoid this behavior change without adding a large transaction
cost. Which is why NumPy has the exact same behavior as our replacement
implementation. We could in principle make the slice read only rather than
allowing for double slicing, but that would also be an API change.
There is one other consequence of producing a view of the original having to do
with passing back to Java. As Java does not recognize the concept of an array
view we must force it back to a Java type at the JNI level. We will force copy
the array slice into a Java array at that time. Thus replicates the same
functionality. This induces one special edge case as `java.lang.Object` and
`java.lang.Serializable` which are both implemented by arrays must be aware of
the slice copy.
Before we trigger the conversion to NumPy or list by calling the slice without
limits operator `[:]` on the array. Under the new implementation this is
effectively a no-op. Thus we haven't broken or forced any changes in the API.
The third case of direct byte buffers also revealed problems. Again the type of
the buffer was not being check resulting in weird reinterpret cast like
behaviors. Additionally, the memory buffer interface was making a very bad
assumption about the referencing of the buffer by assuming that referencing the
object that holds the buffer is the same as referencing the buffer itself. It
was working only because the buffer was being leaked entirely and was likely
possible to break under situations as the leaked buffer essentially locked the
object forever.
To implement all of this properly unfortunately requires making the Python
wrapper of Java arrays a direct type. This is possible in JPype 0.8 series
where we converted all classes to CPython roots, thus our only choice is to
backport the JPype speed patch into JPype 0.7.
API change summary
------------------
- The type of a slice is no longer polymorphic but is always a Java array now.
- The unbounded slice is now a no op.
- Buffer types now trigger conversion routines rather than reinterpret casting
the memory.
- Direct buffers now guard the memory layout such that they work only with
mutable bytearray like types.
- Assignment of elements though double slicing of an array now affects the
original rather than just doing nothing effective like before.
JPype Speed Patch
=================
Speed has always been an issue for JPype. While the interface of JPype is great
the underlying implementation is wanting. Part of this was choices made early
in the development that favors ease of implementation over speed. This forced a
very thin interface using Python capsules and then the majority of the code in
a pure Python module.
The speed issue stems from two main paths. The first being the method
resolution phase in which we need to consider each method overload argument by
argument which means many queries applied to each object. The second is the
object construction penality when we return a Java object back to Python. The
object bottleneck is both on the cost of the wrappers we produce, but
additionaly all of the objects that are constructed to communicate with a Python
function. Every int, list, string and tuple we use in the process is another
object to construct. Thus returning one object triggers dozens of object to be
constructed.
We have addressed these problems in five ways
- Improve resolution of methods by removing the two phase approach to resolving
a type match cutting the resolution time in half.
- Caching the types in C so that they don't have to go back to Python to execute a
method to check the Python cache and construct if necessary.
- Converting all of the base types to CPython so that they can directly access
the C++ guts without going back through entry points.
- Remove all Python new and init method from the Java class tree so that we
don't leave C during the construction phase. Thus avoiding having to
construct Python objects for each argument used during object construction.
- Adding a Java slot to so that we can directly access Java resources both
increasing the speed of the queries and saving us an additional object
during construction.
All of these are being implemented for JPype 0.8 series. For now we are
backporting the last four to the JPype 0.7 series. The first is not possible to
backport as it requires larger structural changes.
Lets briefly discuss each of the items
Method resolution
-----------------
During method resolution the previous implementation had a two phase approach.
First is tried to match the arguments to the Java types in canConvertToJava. In
this each Java argument had to walk through each possible type conversion and
decide if the conversion is possible. Once the we have the method overload
resovled we then perform the conversion by calling convertToJava. That would
then walk through each possible type conversion to find the right one and apply
it. Thus we did tha work twice.
To prevent this from happening we need to reserve memory for each argument we
are trying to convert. When we walk through the list and find a conversion
rather than just returning true, we place a pointer to the conversion routine.
That way when we call convertToJava we don't have to walk the list a second time
but instead go straight to the pointer to get the routine to execute.
This change has two additional consequences. First the primary source of bugs in
the type conversion was a mismatch between canConvertToJava and convertToJava
thus we are removing that problem entirely. The second and more important to
the user is that the type system is now open. By installing a routine we can
now add a user rule. Therefore if we need `java.sql.TimeStamp` to accept a
Python time object we just need to add this to the type conversion table at the
Python level. This is implemented in the ClassHints patch. About half of our
customizer code was to try to achieve this on a per method level. Thus this
elimiates a lot of our current Python customizer code. The remaining customizer
code is to rename Java methods to Python methods and that will remain.
Caching of Python wrappers
--------------------------
In the previous implementation there was a text keyed dictionary that was
consulted to get type wrappers. To access it C++ called to a Python function
that decided when to return a cached type and when to create a new one. This
meant dozens of object constructed just to find the wrapper. To solve this we
simply move the cache and add it to the JClass directly. We have to back
reference the Python class so it can't go away while the JVM is running.
There is one section of code that also uses the wrapper dict in the customizers
which needs to decided does a wrapper already exist for the customizer. We have
replaced these calls with methods on the module.
Conversion of the Base classes
------------------------------
JPype has a number of base classes (object, primitive, exception, string, array)
which hold the methods for the class. If they are implemented as pure Python
than every access from C++ to these elements needs to create objects accordingly
when then are passed back through the module entry points to get back to C++.
We can avoid this by implementing each of these in CPython first at the module
layer and then extending them in the exposed module so that they have the same
outward appearance as before.
We made one refinement during the conversion by implementing all of the CPython
classes using the Heep type API which has the distinct advantage that unlike
static types, it can be changed at runtime. Thus from Python we can add
behavior to the heap types simply with by using `type.__setattr_`. This was
a bit of a challenge as the documentation on heap types is much more sparse than
for static types. However, after going through the process I would recommend
that all new CPython modules should use heap types rather than static as API is
much better and the result much more flexable and stable. The only downside
being the memory footprint increases from 400 bytes to 900 bytes. There are a
few rough spots in the heap type API in that certain actions like setting the
Buffer have to be added to the type after creation, but otherwise it is a big
improvement. Now if all of the documentation would just drop the old static API
in favor of heap types it would be great.
Constructor simplifications
---------------------------
In order to benefit from moving all of the base classes to C, we have to make
sure that derived classes do not transfer control back to Python. Currently
this happens due to the factory nature of our classes. The entry point for
JObject is shared between the construction of objects from Python and a return
from Java. Thus we have to either separate the factory behavior by pushing
those types out of the type tree or pushing the factory behavior into the C
layer.
We have chosen to split the factories and use overrides of the type system in
the meta class to apply `isinstance` and `issubtype` behavior. We can further
restrict the type system if we need to by adding verifications that the
`__new__` and `__init__` methods must point the original base class
implementations if need. Howver, we have not taken this step as of yet.
The split approach effectively removes these heavy elements from type creation.
The concequence of this is that means all of the rest of logic needs to be in
CPython implementation. These can be rather cumbersome at times.
It is always a slippery slope when pushing code from Python back to CPython.
Some thing are needed as they are on the critical path while others are
called only occasionally and thus represent no cost to leave in Python. On the
other hand some things are easy to implement in CPython because the have
direct access rather than having to go through a module entry point. We have
gone with the approach that all critical path and all code the eliminates the
need for an entry point should be pushed back to C.
Java Slots
-------------
In order to get any reasonable speed with Python, the majority of the code
needs to be in C. But additionally there needs to be the use of slots which are
hard coded locations to search for a particular piece of information. This
presents a challenge when wrapping Java as we need a slot for the Java value
structure which must appear on Python object, long, float, exception, and type.
These different types each have their own memory layout and thus we can't just
add the slot at the base as once something is added to the base object it can
no longer be used in multiple inheritance. Thus we require a different
approach.
Looking through the CPython source, we find they have the same quandary with
respect to `__dict__` and `__weakref__` slots. Those slots do not appear one
the base object but get added along the way. The method they use is to add
those slots to the back of the object be increasing the basesize of the object
and then referencing them with two offset slots in the type object. If the type
is variable length the slot offset are negative thus referencing from the end of
the object, or positive if the object is a fixed layout.
Thus we tried a few formulations to see what would work best.
Broken tree approach
~~~~~~~~~~~~~~~~~~~~
The problem with just directly adding the slots in the tree is that the Java
inheritance tree forces the order of the Python tree we have to apply. If we
add a slot to `java.lang.Object` we have to keep the slot on
`java.lang.Throwable` but that is not possible because Throwable requires it to
be derived from Python `Exception`. Thus if we are going to add a slot to the
base we would have to break the tree into pieces on the Python side. This is
possible due to Python inheritance hacking with some effort.
But this approach had significant down sides. When we go to access the slot
we have to first figure out if the slot is present and if not then fall back to
looking in the dictionary. But one of the most common cases is one in which the
item has no slot at all. Thus if we have to both look for the slot and then hit
the dictionary, this is worse than just going to the dictionary in the first
place. Thus this defeats the point of a slot in many cases.
Python dict approach
~~~~~~~~~~~~~~~~~~~~~~~~~
We attempted the same trick by increasing the basesize to account for our extra
slot. This leaves to difficulties. First, the slot has no offset so we need to
find it each time by implying its location. Second, the extra objects have to
be "invisible" during the type construction phase, or Python will conclude the
memory layout of the object is in conflict. We can fool the type system by
subtracting the extra space from the type during the declaration phase and then
adding it back after the base types are created.
This approach failed because the "invisible" part is checked each and every time
a new type is added to the system. Thus every dynamic type we add checks the
base types for consistency and at some point the type system will find the
inconsistency and cause a failure. Therefore, this system can never be robust.
Dict and weakref appear to be very special cases within the Python system and as
there is no general facility to replicate them working within the system does
not appear to be viable.
Memory hacking approach
~~~~~~~~~~~~~~~~~~~~~~~
The last system we attempted to mess with the memory layout of the object during
the creation phase to append our memory after Pythons. To do this we need to
override the memory allocator to allocate the requested memory plus our extra.
We can then access this appended memory by computing the correct size of the
object and thus our slot is on the end.
We can test if the slot is present by looking to see if both `tp_alloc` and
`tp_finalize` point to our Java slot handlers. This means we are still
effectively a slot as we can test and access with O(1).
The downside of this approach is there are certain cases in which the type of an
object can be changed during the destruction phase which means that our slot can
point to the wrong space if the basesize is changed out from under us. To guard
against this we need to close our type system by imposing a ClassMeta which
closes off mixin types that do not inherit from one of the special case base
classes we have defined.
The API implications should be small. There was never a functional case where
extending a Java object within Python actually made sense as the Python portion
is just lost when passed to Java and unlike Proxies there is no way to retrieve
it. Further the extending a Java object within Python does not bind the
lifespan of the objects so any code that used this is likely already buggy. We
will properly support this option with `@JExtends` at a latter point.
With this limitiation in mind, this appears to be the best implementation
- It adds the slot to all of the required types.
- The slot is immediately accessable using just two fields (basesize, itemsize)
- The slot can be tested for easily (check tp_alloc, tp_finalize)
- It closes the type system by forcing a meta class that guards against
inappropraite class constuction.
We could in principle add the slot to the "front" of the Python object but that
could cause additional issues as we would require also require overriding the
deallocation slot to disappear our memory from the free. The Python GC module
has already reserved the memory in the front of the object so the back is
the next best option.
Speed patch implications
------------------------
Other than improving the speed, the speed patch has a lot of below the hood
changes. So long as the user was not accessing private members there is no API
change, but everything below that is gone. All private symbols like
`__javaclass__` and `__javavalue__` as well as all exposed private members
vanish from the interface. There is no longer a distinction between class
wrappers and `java.lang.Class` instances for purposes of the casting system.
The wrapper is an extension of a Python type and has the class methods and
fields, and the instance is an extension of a Python object without these.
Both hold Java slots to the same object. Therefore a lot of complexity of the
private portions is effectively removed from the user view. Every path now has
the same resolution, check the Java slot first and if not assume it is Python.
Two private methods now appear on the wrapper (though I may be able to hide them
from the user view.) These are the test entry points `_canConvertToJava` and
`_convertToJava`. Thus the speed patch should be transparent all user code that
does not access our private members. That said some code like the Database
wrappers using JPype have roots in some code that did make access to the private
tables. I have sent corrections when we upgraded to 0.7 series thus making them
conforming enough not to touch the private members. But that does mean some
modules may be making such accesses out in the wild.
The good new is after the speed patch pretty much everything that is supposed to
be under the hood is now out of the reach of the user. Thus the chance of
future API breakage is much lower.
Below the hood changes
======================
- We have tried to prevent backend changes from reaching the API, though this
is not always entirely the case. The majority of the cases not already noted
appear to be in the range of implementation side effects. The old
implementation had bugs that create undefined behaviors like reinterpet
casting buffers and the like. It is not possible to both fix the bugs in the
backend and make preserve buggy behavours on the front end. We have limited
our changes to only those for which we can see no desirable use existing.
Calling a list slice assignment on a float from a numpy array of ints and
getting a pile of gibberish was as far as we can tell not useful to the user.
- setResource is dropped in favor of caching the module dictionary once at
start of the JVM. We have a lot of resources we will need and the
setResources method was cumbersome.
- There is a lot of thrashing for the Python module style between C and C++
style. The determining blow was that C++ exception warning showed up when
the proper linkage was given. Thus the perferred style flipped from C++
style to C. Thus the naming style change accordingly.
- In addition to the style change there is also an attempt to isolate symbols
between the different classes. The older style with a formal header that
declares all the symbols at the top encouraged access to the functions and
increased the complexity. Moving to a C style and making everything static
forces the classes to be much more independent.
- With the change to C style there is natural split in CPython class files
between the structure declaration, static methods that implement Python API
functions, the declaration of the type, and the exposed C++ style API used by
the rest of the module.
- There is some thrashing on how much of the C++ wrapper style Python API to
keep. The rewrapping of the API was mainly so support differences between
Python 2.7 and 3.x. So we dropped where we could. Only the JPPyObject which
acts as memory handling device over pure Python style (because Python style
is not exception safe) is strongly needed.
- There is some spacing thrashing between different editors and the continuing
debate of why C was written to have the pointer stick to to the variable
rather than the type. When writing `Object* foo` it implies that the star
is stuck to type rather than the variable where C reads it as the opposite.
Hence there is the endless churn between what is correct `Object *foo` and
what we would say in which `Object*` is actually a type. As Python favors
the former and we currently have the latter that means at some point we
should just have formatter force consistency.
- We introduced Py_IsInstanceSingle. Is is a missing Python API function first
for fast type check method of a single inherited type using the mro. Then
something is singlely inherited we can bypass the whole pile of redirects and
list searchs and just compare if the mro list matches at the same point
counted from the end. As all of our base types are single inherited this
saves time and dodges the more expensive calls that would trigger due to the
PyJPClassMeta overrides.
- We introduced Py_GetAttrDescriptor. This was previously implemented in
Python and was very slow. Python has very good implementations for GetAttr
but unfortunately it has the behavior that it always dereferences
descriptors. Thus it is useless in the case that we need to get the
descriptor from the type tree. We have reimplemented the Python type search
without the descriptor dereferencing behavior. It is not nearly as complete
as the Python method as we only need to consult the dictionary and not handle
every edge case. That of course means that we are much faster.
- As with every rewrite there is the need to cleanup. Anything that wasn't
reached by the test bench or was identified as being called only from one
location was removed or reencorperated back in into the function that call
it.
|