Direct Copy Optimization
Note
This feature was introduced in v1.0.0
Direct copy optimization allows C code to directly copy data into a Python data structure.
This allows for less overhead when calling specific functions that load data into some container.
This overhead can be nontrivial for functions that have to convert many native C types to Python types
(e.g. forcedimension_core.dhd.getPositionAndOrientationFrame()
must perform 12 float allocations and copies).
Functions which support direct copy optimization are loaded under a special module called
direct
in the forcedimension_core.dhd
,
forcedimension_core.dhd.expert
, and forcedimension_core.drd
modules.
These functions require that you use the special containers in forcedimension_core.containers
.
Below is a brief example (omitting error checking for brevity).
import forcedimension_core.containers as containers
import forcedimension_core.dhd as dhd
dhd.open()
pos = containers.Vector3()
# Equivalent to: dhd.getPosition(out=pos)
dhd.direct.getPosition(out=pos)
Which containers should you use when? Direct copy optimized functions hint at which
container to use in the “See Also” section of their docstring. These containers are memory
safe. If an improper container is used, ctypes
will raise a ctypes.ArgumentError
.
There are also containers which subclass NumPy’s numpy.ndarray
available if NumPy is
installed (NumPy 1.20+ required). See Installation for more details.
Theory of Operation
Note
This section is left for library developers and higher level users. It requires a knowledge of how pointers in C work to fully understand.
Under the hood, direct copy optimization is nothing special. The below image shows
how forcedimension_core.dhd.getPosition()
works.
Each arrow represents a copy in the in the diagram
The library invokes the C function _libdhd.dhdGetPosition()
and stores the data from it
in ctypes.c_double
buffers called px
, py
, pz
. Then, it copies the data to a the out
container. As we can see, it would be faster if we could copy the data directly into a Python container.
The code for it is shown below (without docstrings), noting the overhead.
def getPosition(out: MutableArray[int, float], ID: int = -1) -> int:
# Extra allocation here
px = c_double()
py = c_double()
pz = c_double()
# px, py, and pz are pased byref and data is copied into them
err = _runtime._libdhd.dhdGetPosition(px, py, pz, ID)
# Extra copy here
out[0] = px.value
out[1] = py.value
out[2] = pz.value
return err
In general, _libdhd.dhdGetPosition()
needs float pointers to the data you wish to copy to.
The solution then, is to use a type, which can directly pass pointers to
_libdhd.dhdGetPosition()
like array.array
.
3rd party libraries like NumPy also have this capability. Below we show a bare-bones version
of the library implementation.
class Vector3(array):
def __new__(cls, initializer: Iterable[float] = (0., 0., 0.)):
if isinstance(initializer, array):
return initializer
arr = super(Vector3, cls).__new__(cls, 'd', initializer)
if len(arr) != 3:
raise ValueError()
return arr
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Get a pointer to the front of the array
ptr = self.buffer_info()[0]
self._ptrs = (
ctypes.cast(ptr, c_double_ptr), # 0th element
ctypes.cast(ptr + self.itemsize, c_double_ptr), # 1st element
ctypes.cast(ptr + 2 * self.itemsize, c_double_ptr), # 2nd element
)
@property
def ptrs(self) -> Tuple[c_double_ptr, c_double_ptr, c_double_ptr]:
return self._ptrs
def getPosition(out: SupportsPtrs3[c_double], ID: int = -1) -> int:
return _runtime._libdhd.dhdGetPosition(*out.ptrs, ID)
forcedimension_core.containers.Vector3
simply adds an additional property to a Python
array.array
called ptrs
. Now _libdhd.dhdGetPosition()
can be given pointers to the
memory inside the container itself, saving us an allocation and a copy.