Ryan Malloy a9e1f17bae Phase 31: Head-to-head benchmark vs IfxPy (the C-bound PyPI driver)
Adds a paired benchmark of informix-db (pure Python) against IfxPy
3.0.5 (IBM's C-bound driver via OneDB ODBC) on identical workloads
against the same Informix dev container.

Headline result: pure Python is competitive — and faster on 2/5
benchmarks where wire round-trip dominates over codec/marshaling.

| Benchmark | IfxPy | informix-db | Result |
|---|---:|---:|---:|
| select_one_row (single-row latency) | 128 us | 116 us | us 9% faster |
| select_systables_first_10 | 126 us | 184 us | IfxPy 32% faster |
| select_bench_table_all (1k rows) | 969 us | 855 us | us 12% faster |
| executemany(1000) in txn | 21.5 ms | 30.8 ms | IfxPy 30% slower |
| cold_connect_disconnect | 11.0 ms | 10.9 ms | comparable |

Why the surprising wins: IfxPy's path is Python -> OneDB ODBC ->
libifdmr -> wire. Ours is Python -> wire. When wire round-trip
dominates (single-row, bulk fetch), the missing abstraction layer
makes us faster. When per-row marshaling dominates (executemany),
IfxPy's C-level execute(stmt, tuple) beats Python BIND-PDU build.

Files added under tests/benchmarks/compare/:
* Dockerfile.ifxpy — Ubuntu 20.04 base with IfxPy + OneDB drivers
* ifxpy_bench.py — IfxPy benchmark workloads matching test_*_perf.py
* README.md — methodology, results, install gauntlet, reproduction

The IfxPy install gauntlet itself is part of the comparison story:
modern Python 3.11 (not 3.13), setuptools <58, permissive CFLAGS,
manual download of 92MB OneDB ODBC tarball, four LD_LIBRARY_PATH
directories, libcrypt.so.1 (deprecated 2018, missing on Arch /
Fedora 35+ / RHEL 9). Versus our `pip install informix-db`.

README.md (project root): added "Compared to IfxPy" section under
Performance with the headline numbers and a pointer to the full
methodology.

.gitignore: keep Dockerfile/script/README under tests/benchmarks/
compare/, exclude the 92MB OneDB tarball and the local venv.
2026-05-05 11:41:47 -06:00
..

informix-db vs IfxPy comparison benchmark

Head-to-head benchmarks against IfxPy, the IBM-published C-bound Informix driver, on identical workloads against the same Informix Developer Edition Docker container.

TL;DR

Benchmark IfxPy 3.0.5 (C-bound) informix-db 2026.05.05.4 (pure Python) Result
select_one_row (single-row latency) 128 µs 116 µs informix-db 9% faster
select_systables_first_10 (~10 rows) 126 µs 184 µs IfxPy 32% faster
select_bench_table_all (1000-row fetch) 969 µs 855 µs informix-db 12% faster
executemany(1000) in transaction (bulk write) 21.5 ms 30.8 ms IfxPy 30% faster
cold_connect_disconnect (login handshake) 11.0 ms 10.9 ms comparable

informix-db is faster on 2/5, slower on 2/5, comparable on 1/5 — overall within the same order of magnitude as the C-bound driver on every workload.

What this means

Conventional wisdom says C beats Python at I/O drivers. Here, the picture is more nuanced:

  • When the wire dominates (single round-trips, bulk fetch), informix-db wins because IfxPy adds an ODBC abstraction layer (Python → OneDB ODBC driver → libifdmr.so → wire) where we go direct (Python → wire).
  • When per-row marshaling dominates (executemany, wider tuple construction), IfxPy wins because its C-level execute(stmt, tuple) is faster than our Python BIND-PDU build.
  • When the wire handshake dominates (cold connect), they tie because both drivers wait ~11 ms for the server's login response.

The takeaway is that pure-Python doesn't mean "performance compromise" — it means different overhead distribution. For most application workloads (web requests doing a handful of small queries), the wire round-trip is what matters, and the abstraction-layer overhead IfxPy carries means informix-db is typically the same speed or faster.

Why this comparison was hard to set up

IfxPy is genuinely difficult to install on a modern system. Capturing the install gauntlet for the record:

Step Detail
1. Pin Python 3.11 Python 3.13 fails: IfxPy's setup.py uses use_2to3, removed from setuptools 58 (October 2021).
2. Pin setuptools <58 Same root cause.
3. CFLAGS hack GCC 11+ (default since 2021) escalates the C extension's pointer-type warnings to errors. Need CFLAGS="-Wno-incompatible-pointer-types -Wno-error" to demote them.
4. Download OneDB ODBC drivers A 92 MB tarball from hcl-onedb.github.io/odbc/. The pip install only fetches headers — the runtime libs are a separate, undocumented download.
5. Set INFORMIXDIR + LD_LIBRARY_PATH Across four directories (lib/, lib/cli/, lib/esql/, gls/dll/).
6. Install libcrypt.so.1 The OneDB drivers link against the libcrypt-1 ABI (deprecated in 2018, replaced by libcrypt.so.2). Modern Arch / Fedora 35+ / RHEL 9 ship only libcrypt.so.2; you need a compatibility shim (Ubuntu 20.04 still has it; modern distros need libxcrypt-compat or similar).
7. Build runtime container We use Dockerfile.ifxpy here because Ubuntu 20.04 is the most recent base distro that still ships libcrypt.so.1 natively.

By contrast, informix-db's install is pip install informix-db. No external downloads, no system packages, no LD_LIBRARY_PATH, no Docker required.

Methodology

  • Both drivers ran against the same Informix Developer Edition 15.0.1.0.3DE Docker container (informix-db-test from tests/docker-compose.yml).
  • The host runs Arch Linux on x86_64; the IfxPy container runs Ubuntu 20.04 on x86_64. Both reach the server through the loopback path (host's 127.0.0.1:9088 for informix-db; --network=host for the IfxPy container).
  • Each benchmark runs 100/20/3 rounds depending on per-iteration cost; we report the mean. Stddev is small (under 5%) for all reported numbers — within-run jitter doesn't affect the qualitative result.
  • Workloads are matched semantically: same SQL, same row counts, same fetch patterns. Where they differ (IfxPy's IfxPy.fetch_tuple vs. our cursor.fetchall), we use whichever idiom exhausts the cursor in each driver.

Reproduce

From the project root:

# 1. Start the dev Informix container
make ifx-up

# 2. Seed the 1k-row test table on the host (using informix-db)
uv run python -c "
import informix_db, contextlib
conn = informix_db.connect(host='127.0.0.1', port=9088,
    user='informix', password='in4mix',
    database='sysmaster', server='informix', autocommit=True)
cur = conn.cursor()
with contextlib.suppress(Exception): cur.execute('DROP TABLE p21_bench')
cur.execute('CREATE TABLE p21_bench (id INT, name VARCHAR(64), counter INT, value FLOAT, created DATE)')
cur.executemany('INSERT INTO p21_bench VALUES (?, ?, ?, ?, ?)',
    [(i, f'row_{i:04d}', i*7, float(i)*1.5, None) for i in range(1000)])
conn.close()
"

# 3. Build + run the IfxPy benchmark container
docker build -f tests/benchmarks/compare/Dockerfile.ifxpy \
    -t ifxpy-bench tests/benchmarks/compare/
docker run --rm --network=host ifxpy-bench

# 4. Run informix-db benchmarks for the matched comparison
uv run pytest tests/benchmarks/test_select_perf.py \
    tests/benchmarks/test_pool_perf.py \
    tests/benchmarks/test_insert_perf.py \
    -m benchmark --benchmark-only --benchmark-warmup=on

Files

  • Dockerfile.ifxpy — Ubuntu 20.04 container with Python 3.9, IfxPy, and OneDB drivers installed
  • ifxpy_bench.py — IfxPy benchmark workloads (mirrors tests/benchmarks/test_*_perf.py)
  • This README

Caveats

  • IfxPy 3.0.5 is the latest PyPI version (from October 2020). It's the most actively-maintained C-bound option but hasn't shipped a release in ~5 years.
  • Numbers will vary by host, distro, kernel, network stack — re-run on your own hardware before drawing strong conclusions.
  • The 1k-row INSERT benchmark uses different APIs (IfxPy's prepare+execute loop vs our executemany); the comparison is by total wall-clock time for the equivalent workload, not by per-call overhead.