{"id":30324202,"url":"https://github.com/virgesmith/inexmo","last_synced_at":"2025-08-17T22:35:31.298Z","repository":{"id":309953060,"uuid":"1027563614","full_name":"virgesmith/inexmo","owner":"virgesmith","description":"Implement superfast python functions using C++ ","archived":false,"fork":false,"pushed_at":"2025-08-14T18:17:25.000Z","size":41,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-08-14T20:25:59.814Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/virgesmith.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2025-07-28T07:45:02.000Z","updated_at":"2025-08-14T18:17:30.000Z","dependencies_parsed_at":"2025-08-14T20:26:17.742Z","dependency_job_id":"299b1171-00f8-48a5-a4d3-7a70dfe1688d","html_url":"https://github.com/virgesmith/inexmo","commit_stats":null,"previous_names":["virgesmith/inexmo"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/virgesmith/inexmo","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/virgesmith%2Finexmo","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/virgesmith%2Finexmo/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/virgesmith%2Finexmo/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/virgesmith%2Finexmo/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/virgesmith","download_url":"https://codeload.github.com/virgesmith/inexmo/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/virgesmith%2Finexmo/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":270918292,"owners_count":24667664,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-08-17T02:00:09.016Z","response_time":129,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-08-17T22:35:15.308Z","updated_at":"2025-08-17T22:35:31.291Z","avatar_url":"https://github.com/virgesmith.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Inline Extension Modules\n\nWrite and execute superfast C or C++ inside your Python code! Here's how...\n\nWrite a function or method definition **in python**, add the `compile` decorator and put the C++ implementation in a\ndocstr:\n\n```py\nimport inexmo\n\n@inexmo.compile()\ndef max(i: int, j: int) -\u003e int:  # type: ignore[empty-body]\n  \"return i \u003e j ? i : j;\"\n```\n\nWhen Python loads this file, the source for an extension module is generated with all functions using this decorator.\nThe first time any function is called, the module is built and the attribute corresponding to the (empty) Python\nfunction is replaced with the C++ implementation in the module.\n\nModules are only rebuilt when changes to the any of the functions in the module (or decorator parameters)\nare detected.\n\n## Features\n\n- Supports [`numpy` arrays](https://pybind11.readthedocs.io/en/stable/advanced/pycpp/numpy.html) for customised\n\"vectorised\" operations. You can either implement the function directly, or write a scalar function and make\nuse of pybind11's auto-vectorisation feature, if appropriate. (Parallel library support out of the\nbox may vary, e.g. on a mac, you may need to manually `brew install libomp` for openmp support)\n- Supports arguments by value, reference, and (dumb) pointer, with or without `const` qualifiers\n- Maps python types to C++ types with overridable defaults, and automatically includes minimal headers for compilation\n- Custom macros and extra headers/compiler/linker commands can be added as necessary\n\nCaveats \u0026 points to note:\n\n- Compiled lambdas are not supported but nested functions are, in a limited way - they cannot capture variables from their enclosing scope.\n- Functions with conflicting headers or compiler/linker settings must be implemented in separate files\n- Using auto-vectorisation incurs a major performance penalty if the function is called with scalar arguments\n- Auto-vectorisation naively applies operations to\n[vector inputs sequentially](https://pybind11.readthedocs.io/en/stable/advanced/pycpp/numpy.html#vectorizing-functions).\nIt is not suitable for more complex operations (e.g. matrix multiplication)\n- There is currently no way to change the order header files are included in the module source code\n- For methods, annotations must be provided for the context: `self: Self` for instance methods, or `cls: type` for class\nmethods.\n- IDE syntax highlighting and linting probably won't work correctly for inline C or C++ code.\n\n## Performance\n\n### Loops\n\nImplementing loops in optimised compiled code can be orders of magnitude faster than loops in Python. Consider this\nexample: we have a series of cashflows and we need to compute a running balance. The complication is that a fee is\napplied to the balance at each step, making each successive value dependent on the previous one, which prevents any\nuse of vectorisation. The fastest approach in python seems to be preallocating an empty series and accessing it via\nnumpy:\n\n```py\ndef calc_balances_py(data: pd.Series, rate: float) -\u003e pd.Series:\n    \"\"\"Cannot vectorise, since each value is dependent on the previous value\"\"\"\n    result = pd.Series(index=data.index)\n    result_a = result.to_numpy()\n    current_value = 0.0\n    for i, value in data.items():\n        current_value = (current_value + value) * (1 - rate)\n        result_a[i] = current_value\n    return result\n```\n\nIn C++ we can take essentially the same approach. Although there is no direct C++ API for pandas types, since\n`pd.Series` and `pd.DataFrame` are implemented in terms of numpy arrays, we can use the python API to construct\nand extract the underlying arrays, taking advantage of the shallow-copy semantics. Series are passed as `Any`\n(translates to `py::object`) and so we need to explicitly add pybind11's numpy header:\n\n```py\nfrom inexmo import compile\n\n@compile(extra_headers=[\"\u003cpybind11/numpy.h\u003e\"])\ndef calc_balances_cpp(data: Any, rate: float) -\u003e Any:\n    \"\"\"\n```\n```cpp\n    // Import pandas and construct an empty Series\n    auto pd = py::module::import(\"pandas\");\n    auto result = pd.attr(\"Series\")(py::arg(\"index\") = data.attr(\"index\"));\n\n    // Access the Series via numpy\n    auto data_a = data.attr(\"to_numpy\")().cast\u003cpy::array_t\u003cint64_t\u003e\u003e();\n    auto result_a = result.attr(\"to_numpy\")().cast\u003cpy::array_t\u003cdouble\u003e\u003e();\n\n    auto n = data_a.request().shape[0];\n    auto d = data_a.unchecked\u003c1\u003e();\n    auto r = result_a.mutable_unchecked\u003c1\u003e();\n\n    // Perform the calculation\n    double current_value = 0.0;\n    for (py::ssize_t i = 0; i \u003c n; ++i) {\n        current_value = (current_value + d(i)) * (1.0 - rate);\n        r(i) = current_value;\n    }\n    return result;\n```\n```py\n    \"\"\"\n```\n\nNeedless to say, the C++ implementation vastly outperforms the python (3.13) implementation for all but the smallest arrays:\n\nN | py (ms) | cpp (ms) | speedup (%)\n-:|--------:|---------:|-----------:\n1000 | 0.7 | 1.1 | -43\n10000 | 3.3 | 0.3 | 1067\n100000 | 35.1 | 1.7 | 1950\n1000000 | 311.5 | 6.5 | 4709\n10000000 | 2872.4 | 42.9 | 6601\n\nFull code is in [examples/loop.py](./examples/loop.py). To run the example scripts, install the \"examples\" extra, e.g.\n`pip install inexmo[examples]`.\n\n### `numpy` and vectorised operations\n\n\u003e \"vectorisation\" in this sense means implementing loops in compiled, rather than interpreted, code. In fact, the C++ implementation below also uses optimisations including \"true\" vectorisation (meaning hardware SIMD instructions).\n\nFor \"standard\" linear algebra and array operations, implementations in inexmo are very unlikely to improve on heavily\noptimised numpy implementations such as matrix multiplication.\n\nHowever, significant performance improvements may be seen for more \"bespoke\" operations, particularly for\nlarger objects (the pybind11 machinery has a constant overhead).\n\nFor example, to compute a distance matrix between $N$ points in $D$ dimensions, an efficient `numpy` implementation\ncould be:\n\n```py\ndef calc_dist_matrix_p(p: npt.NDArray) -\u003e npt.NDArray:\n    \"Compute distance matrix from points, using numpy\"\n    return np.sqrt(((p[:, np.newaxis, :] - p[np.newaxis, :, :]) ** 2).sum(axis=2))\n```\nbearing in mind there is some redundancy here as the resulting maxtrix is symmetric; however vectorisation with\nredundancy will always win the tradeoff against loops with no redundancy.\n\nIn C++ this tradeoff does not exist. A reasonably well optimised C++ implementation using `inexmo` is:\n\n```py\nfrom inexmo import compile\n\n@compile(extra_compile_args=[\"-fopenmp\"], extra_link_args=[\"-fopenmp\"])\ndef calc_dist_matrix_c(points: npt.NDArray[np.float64]) -\u003e npt.NDArray[float]:  # type: ignore[empty-body, type-var]\n    \"\"\"\n```\n```cpp\n    py::buffer_info buf = points.request();\n    if (buf.ndim != 2)\n        throw std::runtime_error(\"Input array must be 2D\");\n\n    size_t n = buf.shape[0];\n    size_t d = buf.shape[1];\n\n    py::array_t\u003cdouble\u003e result({n, n});\n    auto r = result.mutable_unchecked\u003c2\u003e();\n    auto p = points.unchecked\u003c2\u003e();\n\n    // Avoid redundant computation for symmetric matrix\n    #pragma omp parallel for schedule(static)\n    for (size_t i = 0; i \u003c n; ++i) {\n        r(i, i) = 0.0;\n        for (size_t j = i + 1; j \u003c n; ++j) {\n            double sum = 0.0;\n            #pragma omp simd reduction(+:sum)\n            for (size_t k = 0; k \u003c d; ++k) {\n                double diff = p(i, k) - p(j, k);\n                sum += diff * diff;\n            }\n            double dist = std::sqrt(sum);\n            r(i, j) = dist;\n            r(j, i) = dist;\n        }\n    }\n    return result;\n```\n```py\n    \"\"\"\n```\n\nExecution times (in ms) are shown below for each implementation for a varying number of 3d points. Even at relatively small sizes, the compiled implementation is significantly faster.\n\nN | py (ms) | cpp (ms) | speedup (%)\n-:|--------:|---------:|-----------:\n100 | 0.5 | 2.5 | -82%\n300 | 3.2 | 2.2 | 46%\n1000 | 43.3 | 13.6 | 218%\n3000 | 208.2 | 82.5 | 152%\n10000 | 2269.0 | 803.2 | 183%\n\nFull code is in [examples/distance_matrix.py](./examples/distance_matrix.py).\n\n## Type Translations\n\n### Default mapping\n\nBasic Python types are recursively mapped to C++ types, like so:\n\nPython | C++\n-------|----\n`None` | `void`\n`int` | `int`\n`np.int32` | `int32_t`\n`np.int64` | `int64_t`\n`bool` | `bool`\n`float` | `double`\n`np.float32` | `float`\n`np.float64` | `double`\n`str` | `std::string`\n`np.ndarray` | `py::array_t`\n`list` | `std::vector`\n`set` | `std::unordered_set`\n`dict` | `std::unordered_map`\n`tuple` | `std::tuple`\n`Any` | `py::object`\n\nThus, `dict[str, list[float]]` becomes `std::unordered_map\u003cstd::string, std::vector\u003cdouble\u003e\u003e`\n\n### Qualifiers\n\nIn Python function arguments are always passed by \"value reference\", but C++ allows multiple methods. The default mapping uses by-value, which when objects are shallow-copied, (like numpy arrays) is not unreasonable. To change this behaviour, annotate the function arguments, passing an appropriate instance of `CppQualifier`, e.g.:\n\n```py\nfrom typing import Annotated\n\nimport numpy as np\nimport numpy.typing as npt\n\nfrom inexmo import compile, CppQualifier\n\n@compile()\ndef do_something(array: Annotated[npt.NDArray[np.float64], CppQualifier.CRef]) -\u003e int:\n    ...\n```\n\nwhich will use `const py::array_t\u003cdouble\u003e\u0026` as the argument type.\n\nAvailable qualifiers are:\n\nQualifier | C++\n----------|----\n`Auto` | `T` (no modification)\n`Ref` | `T\u0026`\n`CRef` | `const T\u0026`\n`RRref` | `T\u0026\u0026`\n`Ptr` | `T*`\n`PtrC` | `T* const`\n`CPtr` | `const T*`\n`CPtrC` | `const T* const`\n\n(NB pybind11 does not appear to support `std::shared_ptr` or `std::unique_ptr` as function arguments)\n\n\n### Overriding\n\nIn some circumstances, you may want to provide a custom mapping. This is done by passing the required C++ type (as a string) in the annotation:\n\n```py\nfrom typing import Annotated\n\nfrom inexmo import compile\n\n@compile()\ndef do_something(array: Annotated[list[float], \"py::list\"]) -\u003e int:\n    ...\n```\n\n## TODO\n\n- [ ] customisable location of modules (default seems to work ok)?\n- [ ] control over header file order\n- [ ] module builds sometimes need 2 runs to trigger\n\n\n## See also\n\n[https://pybind11.readthedocs.io/en/stable/](https://pybind11.readthedocs.io/en/stable/)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvirgesmith%2Finexmo","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fvirgesmith%2Finexmo","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvirgesmith%2Finexmo/lists"}