Skip to content

Latest commit

 

History

History
313 lines (261 loc) · 13.7 KB

developer-guide.md

File metadata and controls

313 lines (261 loc) · 13.7 KB

DyND Developer Guide

This document is an introduction to DyND for any developers who would like to understand the inner workings of the code, and possibly contribute changes to the DyND codebase. While aspects of its design have solidified, many parts of the code and design are still undergoing large changes, driven by adding new functionality and connecting DyND to other systems.

As you explore the system, be aware that the code has gone through several design iterations, and there is code that used to work but doesn't at the moment because not everything in the system has caught up yet. These parts of the code should gradually be updated or discarded.

The Build System

To dig into the code, you probably want to start by getting it building and the tests running on your system. While you can work with just the pure C++ DyND library, we recommend you get the Python bindings and build and run both simultaneously. The instructions to get the source code and build it is available in the build/install guide for dynd-python.

Running The Tests

During development of DyND, running the test suite should be a reflex action. Except in rare cases, both the C++ and the Python test suites should be passing 100% on OS X, Windows, and Linux.

The C++ tests are using the google test framework, and to run them you simply run the program 'test_dynd' which is built from the libdynd/tests subdirectory. On Windows MSVC, right click on the test_dynd project and choose "Set as StartUp Project", so when you hit play, the tests will run.

If you're building the Python bindings as well, the Python tests can be run with a command like

python -c "import dynd;dynd.test()"

Background Introductory Material

To get some insight into some of the design choices made in DyND, there are some introductory slides created in Jupyter notebook form, and presentable using the Live Reveal Jupyter extension.

The first set of such slides is about how DyND views memory.

Three video talks about DyND are available. One was presented at the AMS in January 2013, another was presented at SciPy in June 2013, and the third was presented as a lightning talk at EuroSciPy in August 2014. DyND was also mentioned in a Blaze talk at SciPy in July 2014.

Source Code Directory Layout

The main directories to look at within DyND are the 'src' and 'include' directories. These contain the main source code and header files.

The source files are mostly distributed in subdirectories of src/dynd, with a few that are the main objects or not easily categorized in src/dynd itself. The same applies to the headers in the include/dynd directory. In general, if a header .hpp file has a corresponding .cpp file, it goes into the src/* directory corresponding to include/*.

Main Source

In the include/dynd directory, the files implementing the main DyND objects are type.hpp and array.hpp. The file config.hpp has some per-compiler logic to enable/disable various supported C++ features, define some common macros, and declare the version number strings.

Types Source

All the DyND types are presently located in include/dynd/types. The file include/dynd/type.hpp basically contain a smart pointer which wraps instances of type base_type * in a more convenient interface. The file include/dtypes/type_id.hpp contains a number of primitive enumeration types and some type_id metaprograms.

There are a number of base_<kind>_dtype dtype classes which correspond to a number of the dtype 'kinds'. For example, if a dtype is of the dim kind, then it must inherit from base_dim_type.

The code which consumes and produces Blaze datashapes is in include/dynd/types/datashape_parser.hpp and include/dynd/types/datashape_formatter.hpp.

See the DataShape documentation for more general information about datashape.

Arrays Source

The DyND nd::array object is a smart-pointer object which wraps the lower-level types, memory blocks, and arrmeta handling into an object with a more convenient interface. This object has reference semantics, copying one nd::array to another creates another reference to the same object. It also has many constructors from C++ objects, which create new nd::array objects of the appropriate type, with a copy of the value provided.

Some example code creating and modifying an nd::array:

// Initialize an array using C++11 initializer list
nd::array a = {1.5, 2.0, 3.1};
cout << a << endl;
// Assign one value
a(1).vals() = 100;
cout << a << endl;
// Assign a range (slightly more efficient with vals_at)
a.vals_at(irange() < 2) = {9, 10};
cout << a << endl;

Output:

array([1.5,   2, 3.1],
      type="3 * float64")
array([1.5, 100, 3.1],
      type="3 * float64")
array([  9,  10, 3.1],
      type="3 * float64")

Func Source

The func namespace is where the nd::arrfunc and nd::callable objects are defined, which provide array function abstractions. The nd::arrfunc is being actively developed, and the plan is for callable to go away once all its functionality can be superceded.

From C++, the easiest way to create an arrfunc is with make_apply_arrfunc, which uses template metaprogramming to automatically generate the required dynamic type information, ckernels, and glue functions that an nd::arrfunc requires.

Another useful operation, after you've created a scalar arrfunc, is to lift the arrfunc into something like a NumPy ufunc with lift_arrfunc. Another example that has a more interesting function signature is make_rolling_arrfunc, which applies a provided arrfunc to every interval of an array in a rolling fashion.

CKernels Source

Before an nd::arrfunc is executed, it first gets lowered into a ckernel. This low level abstraction defines an array-oriented, possibly hierarchical way to combine code and data.

CKernels in DyND are mostly implemented in the include/kernels directory. All kernels get built via the ckernel_builder class. These kernels are intended to be cheap to construct and execute when they are small, but scale to hold acceleration structures and other information when what they are computing is more complex.

An example trivial case is a kernel which adds two floating point numbers, which as a ckernel will consist of just the addition function pointer and a NULL destructor. In simple cases like this, and with a small bit of additional composition such as operating on a dimension, creating and executing the ckernel does not use any dynamic memory allocation.

A more complicated case is a kernel which evaluates an expression on values in a struct. Such a ckernel may contain a bytecode representation of the expression, information about field offsets within the struct, and child ckernels for the component functions of the expression.

There are presently three kinds of kernels that are defined and used within DyND: expr_single_t, expr_strided_t, and expr_predicate_t, defined in ckernel_prefix.hpp. In all three cases there is an array of src inputs, and a single dst output, which is a boolean value returned in an int for the predicate case.

The expr_single_t ckernel function prototype is for the case where the kernel needs to be called on different memory locations, one at a time. Implementations of it can focus on doing their job just once with as little overhead as possible.

The expr_strided_t ckernel function prototype is for evaluating the function on many data values with a constant stride between them. This is a common case in DyND, and in general in systems based on arrays. Implementations may check the stride for the contiguous case, to do SIMD execution, for example. There is a constant DYND_BUFFER_CHUNK_SIZE defined, which is the size DyND typically uses to chunk, so having a fast path for this size, or blocking in chunks of this size may be advantageous as well.

The expr_predicate_t ckernel function prototype is for things like comparisons, to allow the boolean result to be returned directly in a register instead of through a memory address. It's used by functions such as sort, where many comparisons on different memory addresses are performed.

Unary kernels, where the number of src inputs is one, form the backbone of being able to copy values between types. Some basic utilities for defining and working with them are in assignment_kernels.hpp.

General-purpose helper classes for defining ckernels are in ckernel_builder.hpp, implemented to work for both regular CPU and CUDA via some preprocessor code.

Memory Blocks Source

Dynamic nd::array data in DyND is stored in memory blocks, which are in the dynd/memblock directory. The base class is defined in memory_block.hpp, and the different memory blocks serve the needs of various dtypes.

The array_memory_block is for nd::array instances. The file dynd/array.hpp defines an object which contains an array_memory_block and defines operations on it. In typical usage, this contains the type, arrmeta, and a reference+pointer to memory holding the data. As an optimization, it is possible to allocate an array_memory_block with extra memory at the end to store the data with fewer memory allocations, in which case the reference to the data contains NULL.

The fixed_size_pod_memory_block is very simple, for POD (plain old data) types that have a fixed size and alignment.

The pod_memory_block provides a simple pooled memory allocator that doles out chunks of memory sequentially for variable-sized data types. Due to the way nd::array uses views, similar to NumPy, once some memory has been allocated within the pod memory block, it cannot be deallocated until the whole memory block is freed. A small exception is for reusable temporary buffers, and a reset operation is provided to support that use case.

The zeroinit_memory_block is just like pod_memory_block, but initializes the memory it allocates to zero before returning it. Types have a flag indicating whether they require zero-initialization or not.

The external_memory_block is for holding on to data owned by a system external to DyND. For example, in some cases DyND can directly map onto the string data of Python's immutable strings, by using this type of memory block and flagging the nd::array as immutable.