HOPE - combine the ease of Python and the speed of C++¶
HOPE is a specialized method-at-a-time JIT compiler written in Python for translating Python source code into C++ and compiles this at runtime. In contrast to other existing JIT compliers, which are designed for general purpose, we have focused our development of the subset of the Python language that is most relevant for astrophysical calculations. By concentrating on this subset, HOPE is able to achieve the highest possible performance
By using HOPE, the user can benefit from being able to write common numerical code in Python and having the performance of compiled implementation. To enable the HOPE JIT compilation, the user needs to add a decorator to the function definition. The package does not require additional information, which ensures that HOPE is as non-intrusive as possible:
from hope import jit
@jit
def sum(x, y):
return x + y
The HOPE package has been developed at ETH Zurich in the Software Lab of the Cosmology Research Group of the ETH Institute of Astronomy, and is now publicly available at GitHub.
Further information on the package can be found in our paper, on readthedocs.org and on our website.
Installation¶
The package has been uploaded to PyPI and can be installed at the command line via pip:
$ pip install hope
Or, if you have virtualenvwrapper installed:
$ mkvirtualenv hope
$ pip install hope
Contents¶
Documentation¶
-
hope.
jit
(fkt)[source]¶ Compiles a function to native code and return the optimized function. The new function has the performance of a compiled function written in C.
Parameters: fkt (function) – function to compile to c Returns: function – optimized function This function can either be used as decorator
@jit def sum(x, y): return x + y
or as a normal function
def sum(x, y): return x + y sum_opt = jit(sum)
-
hope.
serialize
(obj, name)[source]¶ Write a pickled representation of obj to a file named
name
insidehope.config.prefix
Parameters: - obj (mixed) – arbitrary object to serialize
- name (str) – name of the object
-
hope.
unserialize
(name)[source]¶ Read an object named
name
formhope.config.prefix
. If the file does not exitsunserialize
returnsNone
Parameters: name (str) – name of the object Returns: mixed – unserialized object
hope.config¶
-
hope.config.
cxxflags
= [u'-Wall', u'-Wno-unused-variable', u'-std=c++11']¶ List of c++ compiler flags. Normally hope does determing the right flags itself.
-
hope.config.
hopeless
= False¶ Disable hope. If hope.config.hopeless is True, hope.jit return the original function. Use this function for debug purpos
-
hope.config.
keeptemp
= False¶ Keep the intermediate c++ source and compiler output generated during compilation.
-
hope.config.
optimize
= False¶ Use ‘’‘sympy’‘’ to simplify expression and exptract common subexpression detection
-
hope.config.
prefix
= u'.hope'¶ Prefix of the folder hope saves all data in.
-
hope.config.
rangecheck
= False¶ Check if indeces are out of bounds
-
hope.config.
verbose
= False¶ Print a intermediate representation of each function during compilation.
Language Specification¶
This document specifies the Python subset supported by HOPE.
NumPy Types¶
The following NumPy types are supported
bool_
integer
,signedinteger
,byte
,short
,intc
,intp
,int0
,int_
,longlong
int8
,int16
,int32
,int64
unsignedinteger
,ubyte
,ushort
,uintc
,uintp
,uint0
,uint_
,ulonglong
uint8
,uint16
,uint32
,uint64
single
,float_
float32
,float64
ndarray
Conditional Expressions¶
- If
- If/Else
- If/ElseIf/Else
Loops¶
The while
statement is supported as well as for
loops but only with range(stop)
or range(start, stop)
resp. xrange
:
for i in range(start, stop):
foo()
Return Statement¶
A function needs to have a fixed return type. HOPE currently supports scalar and array data types as return values.
The following code will not compile as the type of the return value may change depending on the execution:
@hope.jit
def incompatible_return(arg):
if arg > 10:
return 1
else:
return 2.3 # ERROR: Inconsistent return type
Call functions¶
Call to pure Python functions are supported if the function
- is accessible form the global scope of the function
- has no decorators
- only uses the subset of Python supported by HOPE
- has no recursive or cyclic calls
Then the called function is also compiled to c++ and included in the shared object regardless where the function was defined originally.
Operators¶
Assignment
Assign | b = a |
Unary operators
UAdd | +a |
USub | -a |
Binary operators
Add | a + b |
Sub | a - b |
Mult | a * b |
Div | a / b |
FloorDiv | a // b |
Pow | a ** b |
Mod | a % b |
LShift | a << b |
RShift | a >> b |
BitOr | a | b |
BitXor | a ^ b |
BitAnd | a & b |
Augmented assign statements
AugAdd | a += b |
AugSub | a -= b |
AugMult | a *= b |
AugDiv | a /= b |
AugFloorDiv | a /= b |
AugPow | a **= b |
AugMod | a %= b |
AugLShift | a <<= b |
AugRShift | a <<= b |
AugBitOr | a | b |
AugBitXor | a ^ b |
AugBitAnd | a & b |
Comparison Operators
Eq | a == b |
NotEq | a != b |
Lt | a < b |
LtE | a <= b |
Gt | a > b |
GtE | a >= b |
Bool Operators
&& | a and b |
|| | a or b |
NumPy Array creation routines¶
empty(shape[, dtype]) |
Return a new array of given shape and type, without initializing entries. |
ones(shape[, dtype]) |
Return a new array of given shape and type, filled with ones. |
zeros(shape[, dtype]) |
Return a new array of given shape and type, filled with zeros. |
NumPy Mathematical functions¶
Trigonometric functions
sin(x) |
Trigonometric sine, element-wise. |
cos(x) |
Cosine elementwise. |
tan(x) |
Compute tangent element-wise. |
arcsin(x) |
Inverse sine, element-wise. |
arccos(x) |
Trigonometric inverse cosine, element-wise. |
arctan(x) |
Trigonometric inverse tangent, element-wise. |
Hyperbolic functions
sinh(x) |
Hyperbolic sine, element-wise. |
cosh(x) |
Hyperbolic cosine, element-wise. |
tanh(x) |
Compute hyperbolic tangent element-wise. |
Exponents and logarithms
exp(x) |
Calculate the exponential of all elements in the input array. |
Miscellaneous
sum(x) |
Return the sum of array elements. |
sqrt(x) |
Return the positive square-root of an array, element-wise. |
interp(x, xp, fp[, left, right]) |
One-dimensional linear interpolation. |
ceil(x) |
Return the ceiling of the input, element-wise. |
floor(x) |
Return the floor of the input, element-wise. |
trunc(x) |
Return the truncated value of the input, element-wise. |
pi |
Returns the pi constant |
fabs |
Compute the absolute values element-wise |
sign |
Returns an element-wise indication of the sign of a number |
Attributes of numpy.ndarray
¶
No attributes are supported at the moment
Others¶
- Added cast operators for np.bool_, np.int_, np.intc, np.int8, np.int16, np.int32, np.int64, np.uint8, np.uint16, np.uint32, np.uint64, np.float_, np.
Hope Architecture¶
The Just-in-time compiling process undergoes several steps. Those steps are explained in the following and detailed in an example and in a dedicated optimization section:

Start The Python interpreter loads a function or method previously decorated with the @hope.jit
decorator.
Cache verification HOPE checks if a compiled version of the requested functions has previously been cached.
Parse function The first time the decorated function is called, the wrapper generates an abstract syntax tree (AST) by parsing the function definition using the Python built-in ast
package.
Generate HOPE AST Using the visitor pattern, the Python AST is traversed and a corresponding HOPE specific AST is generated. During the traversal we use the Python built-in inspect
package to infer the data types of the live objects such as parameters, variable and return values.
Numerical optimization HOPE traverses the new AST in order to identify numerical optimization possibilities and alters the tree accordingly.
Generate C++ code A C++ code is generated from the HOPE AST.
Compile code to shared object library The Python built-in setuptools
package is then used to compile a shared object library from the generated code.
Add library to cache Using the extracted information from the function signature and a hash over the function body the compiled shared object library is cached for future calls.
Load library The shared object library is dynamically loaded into the runtime.
Execute compiled function A call to the function is directed to the function in the shared object library and executed with the passed parameters.
Subsequent function call HOPE analyzes the types of the passed arguments and queries the cache for a function matching the requested name and arguments.
Further reading¶
Example¶
Assume the following example:
poly.py¶
from hope import jit
def poly(x, y, a):
x1 = x - a
y[:] = x1 + x1 * x1
poly_hope = jit(poly)
Step-by-step evaluation¶
In the following we analyze the execution of the example.
from poly import poly
import numpy as np
y = np.empty(1000, dtype=np.float32)
poly_hope(np.random.random(1000).astype(np.float32), y, 3.141)
Executing python call1.py
will cause the following steps to happen:
When evaluating the statement poly_hope = jit(poly)
- HOPE checks if a shared object of a compiled version of
poly
is available. Since we run it the first time no object is available, so HOPE returns a wrapper function that contains a reference to the original function.
When evaluating the statement poly_hope(np.random.random(1000).astype(np.float32), y, 3.141)
The wrapper function, which was returned by jit, is called
A Python AST of
poly
is generated:FunctionDef( name='poly' , args=arguments(args=[Name(id=x), Name(id=y), Name(id=a)]) , body=[ Assign( targets=[Name(id=x1)] , value=BinOp(left=Name(id=x), op=Sub, right=Name(id=a)) ) , Assign( targets=[Subscript(value=Name(id=y) , slice=Slice(lower=None, upper=None, step=None))] , value=BinOp(left=Name(id=x1) , op=Add , right=BinOp(left=Name(id=x1) , op=Mult , right=Name(id=x1))) ) ] )The arguments passed to
poly
are analyzed:
x
:numpy.float32
, 1Dy
:numpy.float32
, 1Da
:numpy.float64
, scalar (originallya
has typefloat
but this is equivalent tonumpy.float64
)HOPE generates an identification for the arguments:
f1f1d
HOPE generates a HOPE AST from the Python AST and the analyzed arguments:
Module( main=poly , functions=[ FunctionDef( name='poly' , args=arguments(args=[ Variable(id=x, shape=(0, x_0), dtype=numpy.float32 , scope=signature, allocated=true) , Variable(id=y, shape=(0, y_0), dtype=numpy.float32 , scope=signature, allocated=true) , Variable(id=a, shape=(), dtype=numpy.float64 , scope=signature, allocated=true) ]) , merged=[[(0, x_0), (0, y_0)]] , body=[ Block(body=[ Assign( target=Variable(id=x1, shape=(0, x_0), dtype=numpy.float32 , scope=block, allocated=false) , value=BinOp(left=Variable(id=x, ...) , op=Sub, right=Variable(id=a, ...) , shape=(0, x_0), dtype=numpy.float32) , Assign( target=View(variable=Variable(id=y, ...) , extend=[0, y_0) , shape=(0, x_0), dtype=numpy.float32 , value=BinOp( left=Variable(id=x, ...) , op=Sub , right=BinOp(left=Variable(id=x1, ...) , op=Mult, right=Variable(id=x1, ...) , shape=(0, x_0), dtype=numpy.float32) , shape=(0, x_0), dtype=numpy.float32 ) ], shape=(0, x_0), dtype=numpy.float32) ] ) ] )Differences between the Python AST and the HOPE AST:
The HOPE AST is statically typed, each token has a scalar type (
dtype
) and for a start, stop for each dimension (shape
) where shape=(0, x_0) means start=0, stop=x.shape[0]The function definition has a property merged. This list of lists identifies all
segments
(each dimension of ashape
is calledsegment
), which are equal. This is determined as follow:
- the statement
x1 = x - a
implies thatx1
has the same shape asx
- the statement zz
y[:] = x1 + x1 * x1
is only valid if x1 and y have the same shape.so
x
andy
must have the same shape.The function body contains a
Block
token. This token is generated the following way:
- Each statement in the body is wrapped into a
Block
token. EachBlock
token has the shape of the statement- All neighbor blocks with the same shape are merged
Variables have a scope, which can either be:
signature
: variables that are passed on callbody
: variables, which occur in more than one Blockblock
: variables, which occur only in one Block tokenHOPE traverses the new AST in order to identify numerical optimization possibilities optimization
generate C++11 code from the HOPE AST. The Block taken above is translated into the following C++ code:
- the shape of
x
is stored in thesx
array- the C pointer to the data of
x
is storedcx
,ca
is a double value containing the value ofa
for (npy_intp i0 = 0; i0 < sx[0] - 0; ++i0) { auto cx1 = (cx[i0] - ca); cy[i0] = (cx1 + (cx1 * cx1)); }
- The whole
Block
statement is turned into one loop over the shape of the block. This allows us to evaluate the operation element-wise, which improves cache locality.- For variables with
Block
scope there is no need to allocate a whole array, we only allocate a scalar value.the C++ code is compiled into a shared object library
the shared object library is dynamically imported and the compiled function is evaluated.
from poly import poly
import numpy as np
y = np.empty(1000, dtype=np.float32)
poly_hope(np.random.random(1000).astype(np.float32), y, 3.141)
y = np.empty(1000, dtype=np.float64)
poly_hope(np.random.random(1000).astype(np.float64), y, 42)
Executing python call2.py
will cause the following steps to happen:
When evaluating the statement poly_hope = jit(poly)
- checks if a shared object of a compiled version of
poly
is available. Since a shared object is available the shared object is dynamically loaded- a callback function for unknown signatures is registered in the module
- the reference to the compiled
poly
function is returned
When evaluating the statement poly_hope(np.random.random(1000).astype(np.float32), y, 3.141)
- the compiled
poly
function is called
When evaluating the statement poly_hope(np.random.random(1000).astype(np.float64), y, 42)
there is no compiled
poly
function for the passed argument types, so the registered callback is calledthe arguments which are passed to
poly
are analysed:
x
:numpy.float64
, 1Dy
:numpy.float64
, 1Da
:numpy.int64
, scalar (originallya
has typeint
but this is equivalent tonumpy.int64
)The code is regenerated as described above, but this time with two different function signatures. Once for
x
:numpy.float32
, 1Dy
:numpy.float32
, 1Da
:numpy.float64
, scalarand once for
x
:numpy.float64
, 1Dy
:numpy.float64
, 1Da
:numpy.int64
, scalar (originallya
has typeint
but this is equivalent tonumpy.int64
)The new shared object library is dynamically imported and evaluated
Optimization¶
After the HOPE specific AST has been created the package performs a static recursive analysis of the expressions to introduce numerical optimization. The supported possibilities are divided into three groups:
Simplification of expressions¶
To simplify expression we have used the SymPy
library. SymPy is a Python library for symbolic mathematics and has been entirely written in Python. To apply the optimization, the AST expression is translated into SymPy
syntax AST and passed to the simplify
function. The function applies various different heuristics to reduce the complexity of the passed expression. The simplification is not exactly defined and varies depending on the input.
For instance, one example of simplification is that \(sin(x)^2 + cos(x)^2\) will be simplified to \(1\).
Factorizing out subexpressions¶
Furthermore the SymPy
library is used to factorize out recurring subexpression (common subexpression elimination) using the previously created SymPy
AST and SymPy
‘s cse
function.
Replacing the pow function for integer exponents¶
From C++11 on, the pow
function in the C standard library is not overloaded for integer exponents. The internal implementation of the computation of a base to the power of a double exponent is typically done using a series expansion, though this may vary depending on the compiler and hardware architecture. Generally this is efficient for double exponents but not necessarily for integer exponents.
HOPE therefore tries to identify power expressions with integer exponents and factorizes the expression into several multiplications e.g. \(y=x^5\) will be decomposed into \(x_2=x^2\) and \(y=x_2\times x_2 \times x\). This reduces the computational costs and increases the performance of the execution.
Contributing¶
Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given.
You can contribute in many ways:
Types of Contributions¶
Report Bugs¶
If you are reporting a bug, please include:
- Your operating system name and version.
- Any details about your local setup that might be helpful in troubleshooting.
- Detailed steps to reproduce the bug.
Fix Bugs¶
Implement Features¶
Write Documentation¶
HOPE could always use more documentation, whether as part of the official HOPE docs, in docstrings, or even on the web in blog posts, articles, and such.
Submit Feedback¶
If you are proposing a feature:
- Explain in detail how it would work.
- Keep the scope as narrow as possible, to make it easier to implement.
- Remember that this is a volunteer-driven project, and that contributions are welcome :)
Pull Request Guidelines¶
Before you submit a pull request, check that it meets these guidelines:
- The pull request should include tests.
- If the pull request adds functionality, the docs should be updated. Put your new functionality into a function with a docstring, and add the feature to the list in README.rst.
- The pull request should work for Python 2.7, and 3.3. make sure that the tests pass for all supported Python versions.
Credits¶
Development Lead¶
- Lukas Gamper <gamperl@gmail.com>
- Joel Akeret <jakeret@phys.ethz.ch>
Contributors¶
We would like to thank several people for helping to test this package before release.
- Alexandre Refregier
- Adam Amara
- Claudio Bruderer
- Chihway Chang
- Sebastian Gaebel
- Joerg Herbel
Citations¶
As you use HOPE for your exciting discoveries, please cite the paper that describes the package:
Akeret, J., Gamper, L., Amara, A. and Refregier, A., Astronomy and Computing (2015)
Feedback¶
If you have any suggestions or questions about HOPE feel free to email me at hope@phys.ethz.ch.
If you encounter any errors or problems with HOPE, please let me know!
History¶
0.6.1 (2016-07-04)¶
- fixing bug when accessing class members for operations
0.6.0 (2016-04-19)¶
- Fixed bug in 2d array slicing
- Array slicing with negative index
- Fixed name clash bug with object attributes
- Replaced assignment with reference to object attributes
0.5.0 (2016-01-20)¶
- Fixed memory leak when creating array in jitted fkt
- Fixed incorrect bound handling in numpy.interp
0.4.0 (2015-02-04)¶
- Increased compilation speed for large functions
- Support for variable allocation within if-else
- Added support for numpy.sign
- Updated Cython implementation in benchmarks
- Fixed array allocation problem under OSX Yosemite (thx iankronquist)
0.3.1 (2014-10-24)¶
- Better support for Python 3.3 and 3.4
- Proper integration in Travis CI
- Improved support for Linux systems (accepting x86_64-linux-gnu-gcc)
- Avoiding warning on Linux by removing Wstrict-prototypes arg
- Supporting gcc proxied clang (OS X)
- Added set of examples
0.3.0 (2014-10-16)¶
- Language: scalar return values
- Shared libraries are written to hope.config.prefix
- Function call can have return values
- Fixed function calls to function with no arguments
- Make sure code is recompiled if the python code has changed
- Added config.optimize to simplify expression using sympy and replace pow
- Speed improvements for hope
- Added support for object properties
- Added support for object methods
- Addes support for True and False
- Addes support for While
- Addes support for numpy.sum
- Addes support for numpy.pi
- Added support for numpy.floor, numpy.ceil, numpy.trunc, numpy.fabs, numpy.log
- improved error messages
- Added config.rangecheck flag
- Support xrange in for loop
- Added cast operators for np.bool_, np.int_, np.intc, np.int8, np.int16, np.int32, np.int64, np.uint8, np.uint16, np.uint32, np.uint64, np.float_, np.float32, np.float64,
- Added bool operators
- Added the following operators:
FloorDiv | a // b |
Mod | a % b |
LShift | a << b |
RShift | a >> b |
BitOr | a | b |
BitXor | a ^ b |
BitAnd | a & b |
AugFloorDiv | a //= b |
AugPow | a **= b |
AugMod | a %= b |
AugLShift | a <<= b |
AugRShift | a <<= b |
AugBitOr | a | b |
AugBitXor | a ^ b |
AugBitAnd | a & b |
0.2.0 (2014-03-05)¶
- First release on private PyPI.
0.1.0 (2014-02-27)¶
- Initial creation.
Benchmarks¶
All the benchmarks have been made available online as IPython notebooks (also on GitHub). If you would like to run the benchmarks on your machine, you can download the notebooks in the nbviewer.
Note: Make sure to execute the native_cpp_gen the first time in order to generate the native C++ code and some Python utility modules.