Speeding up Python with C++ and Pybind11

5 minute read

Published:

A how-to set-up guide for using C++ with Python with Pybind11.

Instructions are based on this guide with some of the kinks worked out for common problems.

I am using conda to manage dependencies and using the MS Visual Studio 2019 IDE for C++.

Code here

1) Set-Up

  • Create project environment, I will be using conda but other environment managers are available. Go to a command line and enter the following:
     conda create -n venv
     conda activate venv
     conda install pip
     pip install pybind11
    
  • Configure MS Visual Studio C++ settings
    • Create new C++ project called “superfastcode”
    • Configure project properties, go to ribbon, Project > superfastcode Properties
TabPropertyValue
GeneralGeneral > Target NameSpecify the name of the module as you want to refer to it from Python in from…import statements. You use this same name in the C++ when defining the module for Python. If you want to use the name of the project as the module name, leave the default value of $(ProjectName).
 General (or Advanced) > Target Extension.pyd
 Project Defaults > Configuration TypeDynamic Library (.dll)
C/C++ > GeneralAdditional Include DirectoriesAdd the Python include folder as appropriate for your installation, for example C:\Users\james\Miniconda3\envs\venv\include.
C/C++ > Code GenerationRuntime LibraryMulti-threaded DLL (/MD) (see Warning below)
Linker > GeneralAdditional Library DirectoriesAdd the Python libs folder containing .lib files as appropriate for your installation, for example, C:\Users\james\Miniconda3\venv\libs. (Be sure to point to the libs folder that contains .lib files, and not the Lib folder that contains .py files.)

2) Write some code

  • Within MS Visual Studio, add a .cpp file called “module.cpp”. To do this, go to Solution Explorer, Source Files, then select ‘add’ and choose the .cpp file
  • Copy the following code
    #include <cmath>
    #include <pybind11/pybind11.h>
    
    const double e = 2.7182818284590452353602874713527;
    
    double sinh_impl(double x) {
        return (1 - pow(e, (-2 * x))) / (2 * pow(e, -x));
    }
    
    double cosh_impl(double x) {
        return (1 + pow(e, (-2 * x))) / (2 * pow(e, -x));
    }
    
    double tanh_impl(double x) {
        return sinh_impl(x) / cosh_impl(x);
    }
    
    namespace py = pybind11;
    
    PYBIND11_MODULE(superfastcode, m) {
        m.def("fast_tanh", &tanh_impl, R"pbdoc(
            Compute a hyperbolic tangent of a single argument expressed in radians.
        )pbdoc");
    
    #ifdef VERSION_INFO
        m.attr("__version__") = VERSION_INFO;
    #else
        m.attr("__version__") = "dev";
    #endif
    }
    
  • Check the solution builds, in MS VS, go to ribbon ctrl+shift+B or Build > build solution, ensuring the correct configuration.

3) Allow access to your C++ code from Python

  • Create a setup.py file to expose function to Python
    • Add a .cpp file as above to the Source Files, but rename it to setup.py
    • Copy the following
      import os, sys
      
      from distutils.core import setup, Extension
      from distutils import sysconfig
      
      cpp_args = ['-std=c++11', '-stdlib=libc++', '-mmacosx-version-min=10.7']
      
      sfc_module = Extension(
          'superfastcode', sources=['module.cpp'],
          include_dirs=['pybind11/include'],
          language='c++',
          extra_compile_args=cpp_args,
          )
      
      setup(
          name='superfastcode',
          version='1.0',
          description='Python package with superfastcode C++ extension (PyBind11)',
          ext_modules=[sfc_module],
      )
      
  • Back to the commandline with environment venv activated, install the C++ module using pip
    • Navigate to the directory containing ‘setup.py’ on the terminal
    • Enter the following in ther terminal: pip install .
  • Check it works
    • Again on the command line, go to a python interpretter for the venv environment
       python 
       >> from superfastcode import fast_tanh
      

Common Issues

  • 32 bit vs 64 bit
    • Errors such as “fatal error LNK1112: module machine type ‘x64’ conflicts with target machine type ‘X86’” are due to some bit configuratio mis-match
    • Ensure that the bit version of Python and hence pybind11 installed matches the C++ bit chosen. At time of writing I installed Python 3.8 64 bit by default so chose x64 in the MS Visual Studio Configuaration Manager.
    • Some more debugging issues here
  • Cannot find pybind11.h
    • Ensure the Python “include” directory, which includes “pybind11.h”, is entered in the “Additional include directories” in the Project Properties setup as detailed above. Also ensure that project is being built using the configuration such as Platform x64 corresponding to the project properties with the correct include directories

Working with C++ and NumPy

NumPy arrays may be accessed through the protocol buffer. See more examples in the pybind11 docs here.

  • Copy full C++ code below into “module.cpp”.
    #include <cmath>
    #include <pybind11/pybind11.h>
    #include <pybind11/numpy.h>

    const double e = 2.7182818284590452353602874713527;

    double sinh_impl(double x) {
        return (1 - pow(e, (-2 * x))) / (2 * pow(e, -x));
    }

    double cosh_impl(double x) {
        return (1 + pow(e, (-2 * x))) / (2 * pow(e, -x));
    }

    double tanh_impl(double x) {
        return sinh_impl(x) / cosh_impl(x);
    }

    namespace py = pybind11;

    py::array_t<double> add_arrays(py::array_t<double> input1, py::array_t<double> input2) {
        py::buffer_info buf1 = input1.request(), buf2 = input2.request();

        if (buf1.ndim != 1 || buf2.ndim != 1)
            throw std::runtime_error("Number of dimensions must be one");

        if (buf1.size != buf2.size)
            throw std::runtime_error("Input shapes must match");

        /* No pointer is passed, so NumPy will allocate the buffer */
        auto result = py::array_t<double>(buf1.size);

        py::buffer_info buf3 = result.request();

        double* ptr1 = (double*)buf1.ptr,
            * ptr2 = (double*)buf2.ptr,
            * ptr3 = (double*)buf3.ptr;

        for (size_t idx = 0; idx < buf1.shape[0]; idx++)
            ptr3[idx] = ptr1[idx] + ptr2[idx];

        return result;
    }

    PYBIND11_MODULE(superfastcode, m) {
        m.def("fast_tanh", &tanh_impl, R"pbdoc(
            Compute a hyperbolic tangent of a single argument expressed in radians.
        )pbdoc");
        m.def("add_arrays", &add_arrays, "Add two NumPy arrays");

    #ifdef VERSION_INFO
        m.attr("__version__") = VERSION_INFO;
    #else
        m.attr("__version__") = "dev";
    #endif
    }
  • Test in Python as follows, from terminal or otherwise
    python
    >>> import numpy as np
    >>> from superfastcode import add_arrays
    >>> a = np.array([1.,2.,3.])
    >>> b = a.copy()
    >>> add_arrays(a,b)
    array([2., 4., 6.])