Performance¶
Low-overhead Python calls for real Fortran workloads.
On the benchmark system, the normal PRIK interface delivered a 1.04× geometric-mean speedup over NumPy's f2py. Across 13 workloads, PRIK was faster in 12 and f2py in 1; all comparisons were statistically significant.
The chart shows f2py time ÷ PRIK time. Values to the right of 1.0× favor
PRIK; values to the left favor f2py. Results close to 1.0× are practical
parity and may move slightly between machines or runs.
Detailed Results¶
Lower times are better. Every row measures the same Fortran operation through the normal generated interface of each tool.
| Workload | f2py | PRIK | Relative result |
|---|---|---|---|
| Empty function call | 33.5 ns | 32.8 ns | PRIK 1.02× faster |
| Add two scalars | 338 ns | 315 ns | PRIK 1.07× faster |
| Increment vector, 1 element | 103 ns | 88.5 ns | PRIK 1.16× faster |
| Increment vector, 16 elements | 106 ns | 91.8 ns | PRIK 1.15× faster |
| Increment vector, 1,024 elements | 262 ns | 248 ns | PRIK 1.05× faster |
| Increment vector, 1,000,000 elements | 187 µs | 187 µs | PRIK 1.002× faster |
| Sum 4×4 F-order matrix | 127 ns | 126 ns | PRIK 1.01× faster |
| Sum 32×32 F-order matrix | 718 ns | 716 ns | PRIK 1.003× faster |
| Sum 256×256 F-order matrix | 39.0 µs | 38.9 µs | PRIK 1.003× faster |
| Sum 1,024×1,024 F-order matrix | 624 µs | 623 µs | PRIK 1.001× faster |
| Update 4×4 F-order matrix | 247 ns | 224 ns | PRIK 1.10× faster |
| Update 256×256 F-order matrix | 13.2 µs | 13.0 µs | PRIK 1.008× faster |
| Update 1,024×1,024 F-order matrix | 199 µs | 200 µs | f2py 1.005× faster |
| Geometric mean | reference | — | PRIK 1.04× faster |
The smallest workloads expose wrapper overhead most clearly. As more time is spent inside Fortran, both tools approach the cost of the native operation and small differences matter less.
Direct bind(C) Entrypoints¶
This separate cohort isolates three scalar call boundaries: an empty
subroutine, a scalar function, and a scalar subroutine with an output. PRIK and
f2py compile the same Fortran source and call the same bind(C) labels. The
normal-interface geometric mean above remains unchanged.
Before timing, artifact inspection verifies that neither direct route contains
a generated Fortran procedure adapter. Each Python binding object refers to
the three user labels, while its native object and linked extension define
them. f2py keeps its Python C/API binding and uses --no-wrap-functions plus
--skip-empty-wrappers; these options remove unnecessary generated Fortran
wrappers, not the Python binding. The PRIK adapter control measures equivalent
ordinary-Fortran procedures separately.
Direct PRIK and f2py¶
| Workload | f2py direct | PRIK direct | Relative result |
|---|---|---|---|
| Empty call | 34.8 ns | 29.7 ns | PRIK direct 1.17× faster |
| Scalar function | 122 ns | 102 ns | PRIK direct 1.19× faster |
| Scalar subroutine | 122 ns | 102 ns | PRIK direct 1.19× faster |
| Geometric mean | reference | — | PRIK direct 1.18× faster |
PRIK adapter control¶
| Workload | PRIK adapted | PRIK direct | Relative result |
|---|---|---|---|
| Empty call | 31.2 ns | 29.7 ns | PRIK direct 1.05× faster |
| Scalar function | 103 ns | 102 ns | PRIK direct 1.01× faster |
| Scalar subroutine | 104 ns | 102 ns | PRIK direct 1.01× faster |
| Geometric mean | reference | — | PRIK direct 1.02× faster |
Clean Build Time¶
Build latency is measured separately from runtime-call overhead. Each timing starts with an empty output directory and ends when the normal tool command has generated its wrapper, compiled all native and generated sources, and linked an extension. Importability and the expected Python exports are checked immediately afterward, outside the timed interval.
The small-module workload uses the same one-source, five-procedure module as the
runtime suite. The full-library workload gives both tools the same 155-source
reference BLAS implementation and requires all 155 routines to be exposed.
Each workload is built once as a development build with -O0 and once as an
optimized build with -O3 -march=native -mtune=native.
Each value is the mean of 4 clean builds after 1 untimed warm-up.
| Clean build workload | f2py | PRIK | Relative result |
|---|---|---|---|
Development (-O0) · small module (1 source, 5 procedures) |
1.42 sec | 587 ms | PRIK 2.42× faster |
Development (-O0) · full reference BLAS (155 sources) |
6.84 sec | 5.55 sec | PRIK 1.23× faster |
Optimized (-O3 -march=native -mtune=native) · small module (1 source, 5 procedures) |
1.63 sec | 771 ms | PRIK 2.12× faster |
Optimized (-O3 -march=native -mtune=native) · full reference BLAS (155 sources) |
10.8 sec | 11.1 sec | f2py 1.02× faster |
Direct-Entrypoint Clean Build Time¶
The direct build workload compiles the same one-source, three-procedure module used by the direct runtime cohort with the optimized profile. It includes contract or signature processing, Python binding generation, compilation, and linking. The separate PRIK adapter control shows whether omitting one generated Fortran adapter materially changes this small end-to-end build.
Each value is the mean of 4 clean builds after 1 untimed warm-up.
Direct PRIK and f2py¶
| Clean build workload | f2py direct | PRIK direct | Relative result |
|---|---|---|---|
Optimized (-O3 -march=native -mtune=native) · small direct module (1 source, 3 procedures) |
1.62 sec | 542 ms | PRIK direct 2.99× faster |
PRIK adapter control¶
| Clean build workload | PRIK adapted | PRIK direct | Relative result |
|---|---|---|---|
Optimized (-O3 -march=native -mtune=native) · small direct module (1 source, 3 procedures) |
548 ms | 542 ms | PRIK direct 1.01× faster |
Should I use PRIK or f2py?¶
These benchmarks answer two narrow questions: runtime-call overhead and clean build time for the same Fortran sources on the same machine. They do not rank feature coverage, API design, ecosystem maturity, or suitability for every project.
Use NumPy's f2py when its established
generated API—or an editable
.pyf signature—is
enough for your project.
Choose PRIK when you want to design the Python API, not just generate a wrapper.
Its editable semantic .pyi contract is a
simpler, more Pythonic place to rename or hide exports, flatten modules, reorder
or hide native arguments, and return native outputs as Python results.
PRIK treats NumPy arrays as complete API contracts: dtype, rank, shape, memory layout, contiguity, strides, mutation, and copy behavior are all explicit. This includes supported strided views, forward or reversed, without copying.
PRIK also covers important Fortran features: supported derived types as Python classes, allocatables, documented pointer forms, native errors as Python exceptions, and overloaded procedures. PRIK is currently alpha, so check the linked guides for exact limitations.
Fair, Like-for-Like Setup¶
The normal-interface suite wraps one set of Fortran kernels with the default PRIK and f2py interfaces. It checks both extensions for the same results before measuring them. The direct-entrypoint cohort is kept separate and uses only the documented direct-call modes described above.
Each runtime group uses an A/B/B/A sequence with equal PRIK-first and f2py-first process budgets. The two passes are merged before significance, winner counts and geometric means are calculated, preventing either tool from consistently benefiting from being measured second. Clean-build rounds alternate tool order independently.
- Runtime native and generated sources use
-O3 -march=native -mtune=native. - Clean builds use development (
-O0) and optimized (-O3 -march=native -mtune=native) profiles. - Both interfaces keep the GIL held.
- OpenMP, OpenBLAS, and MKL are limited to one thread.
pyperf --rigorouspins each benchmark to logical CPU0.- Normal runtime samples combine equal PRIK-first and f2py-first process budgets.
- Direct runtime samples use balanced forward and reverse PRIK-direct, f2py-direct, and PRIK-adapted process order.
- PRIK build timings use up to 4 concurrent compiler processes; f2py uses its normal Meson/Ninja scheduler.
- Normal and three-route direct build timings alternate tool order, use clean output directories, and exclude post-build import checks.
- CPU: Arm Neoverse N2.
- Operating system: Ubuntu 24.04.5 LTS.
- Kernel/platform:
Linux-6.17.0-1022-azure-aarch64-with-glibc2.39. - Python: 3.12.14.
- NumPy/f2py: 2.5.1.
- Fortran compiler: GNU Fortran 13.3.0.
- pyperf: 2.10.0.
- PRIK revision:
4b00c4ad765d.
These results were recorded on September 13, 2026. Performance depends on the CPU, compiler, operating system, and background activity, so comparisons should use results produced together on the same machine.
Reproduce the Results¶
The complete benchmark suite is included in the repository. From the repository root, reproduce the build, correctness checks, measurements, and comparison with one command:
bash benchmarks/run.sh
The command writes the normal, direct, and adapter-control runtime and
clean-build pyperf result pairs under benchmarks/results/ and prints their
comparison tables.