Skip to content

Native wheel: replies are copied three times in Python (string_at, decode, json.loads) #323

Closed

Description

@tae898

Source for the practice: a literature and documentation pass of 2026-11-09 (the Arrow C Data and C Stream interface specifications, the ADBC documentation, the DuckDB, Polars and psycopg documentation, the JPype user guide, and Raasveldt and Muehleisen, VLDB 2017, on result-set transfer); the exact list of 30 sources is in my notes and I will attach it on request. Everything about our code is read at the tag 26.11.1 (commit 2f28a328be); nothing was run or timed, so there is no speed claim. Part of #315.

Read: _native/lib.py:330-365 copies each reply out of the library with ctypes.string_at, then decodes it, then parses it with json.loads. The usual practice is to wrap the pointer in an owner object and free it when the owner is released, so the bytes are not copied before they are parsed (for example cffi ffi.gc, or pyarrow.foreign_buffer(base=...) for a buffer).

Proposal: a small pointer-owner class that frees the reply on release and lets the decoder read from the address, keeping every result as it is. Python only. This is independent of #312's columnar frames and also helps the one-row paths of #229 and #317.

Measure: per-reply microseconds and bytes copied for a small and a large reply, warmed, before and after. Done when the copy count is lower and the measurement is written here.

Activity

  1. added
    nativeThe arcadedb package (native library build)
    performanceSpeed or memory of the bindings, with a measurement
    pythonPull requests that update python code
    on Nov 9, 2026
  2. added this to the 26.12.1 milestone on Nov 9, 2026
  3. self-assigned this
    on Nov 9, 2026
  4. tae898 commented on Nov 14, 2026

    @tae898
    CollaboratorAuthor

    The measurement this issue asks for was not written here, so here it is. I ran it on the published arcadedb==26.11.1.post2 wheel against the reader of 26.11.1, on my laptop: one run, one table shape (1,000,000 rows of id, name, score, flag). It is a directional run and not an ab.py A/B, so I name a difference only when the ranges do not overlap.

    Copies: the reply text is now copied once, into the str that json.loads reads (before: a bytes copy and a decode copy; PR #327 explains the change). The Python-side peak of reading one 2,048-row reply, the batch the real read uses, is 0.35 MiB before and 0.17 MiB after. For one 1,000,000-row reply it is 183.74 MiB before and 91.87 MiB after.

    Time: reading one 2,048-row reply took 17.2 µs before and 13.1 µs after (median), which my rule calls not separated. Only the extreme case, one 1,000,000-row reply, is separated (84.3 ms to 42.0 ms, 3 repetitions). A whole 1,000,000-row to_json_list() took 2.824 s before and 2.779 s after (median of 7 paired rounds, old over new 1.013 with range 1.005 to 1.021, against 0.983 to 1.009 for the control): not separated. The rows are byte-identical in every run.

    So the claim is fewer copies and half the Python-side peak of a read, and no speed claim. Not measured: the column-frame reads of #331 (each frame is still copied once), other table shapes, other platforms. The change is in 26.11.1.post1 and 26.11.1.post2, and this was the last item of the "done when", so I close this issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

nativeThe arcadedb package (native library build)performanceSpeed or memory of the bindings, with a measurementpythonPull requests that update python code

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions