Skip to content

Make a plain Cache.get read less and reuse a cursor per thread - #38

Merged
avgvi merged 4 commits into
masterfrom
perf/get-lean-path
Nov 2, 2026
Merged

avgvi merged 4 commits into
masterfrom
perf/get-lean-path

Conversation

@avgvi

@avgvi avgvi commented Nov 2, 2026

Copy link
Copy Markdown
Member

A plain Cache.get does less work per call. That's a get with statistics off, an eviction policy that doesn't write on reads, and no expire_time, tag or store_time asked for. Hits are about 17% faster than master and 5.6.3, misses about 12%, and DjangoCache.get about 16%.

  • Fewer columns: the plain lookup reads only mode, filename and value, with fetchone(). The key and raw columns are unique, so there is at most one row. Requests for metadata, and caches that record statistics or access times, keep the existing queries.
  • One cursor per thread: the plain lookup runs on a cursor kept next to the thread's connection instead of creating one per call. It is created in _connect and dropped with the connection by close() and after a fork. Only statements whose result is fetched right away use it.
  • Unreadable entries: with on_error='miss', an unreadable entry found on this path is deleted by its key and raw columns, with the same check that the row hasn't changed. Other paths still delete by rowid.
  • Lint: get gets two more pylint size disables, for the same reason as before: a helper call would cost a share of every hit. max-module-lines goes from 4100 to 4200, and a long line on the benchmarks page is wrapped. Both are identical to PR Make Cache.set faster by running fewer statements and less Python per write #37.
  • Changelog: a CHANGELOG.md entry under Unreleased.

Measured with interleaved runs: each build in its own venv on the Python 3.14.6 GIL build, SQLite 3.53.1, Apple M5 Pro, 20 rounds in random order. Medians are against PyPI diskcache 5.6.3; 5.6.3 and master were each run twice to show the noise.

build get hit get miss get(tag=True) Django get
5.6.3, second venv (noise) −0.1% −0.1% +0.1% −0.0%
master, two venvs −0.2% / −0.5% −0.2% / −0.7% +4.9% / +4.8% −0.6% / −0.4%
fewer columns only −11.6% −6.7% +4.6% −11.5%
this branch −17.0% −12.4% +4.9% −16.2%

get with metadata flags is unchanged by this PR. It is still about 5% slower than 5.6.3 because of the metadata tuple helper, and memoize_stampede calls get(expire_time=True). That could be a follow-up.

The code merges cleanly with PR #37. Both PRs add a ### Performance section to CHANGELOG.md, so whichever merges second needs a one-line rebase.

Checked:

  • The full test suite passes on Python 3.14 on macOS (809 passed, 3 skipped, 99% coverage, the same uncovered lines as master).
  • New tests cover:
    • the cursor is reused within a thread, separate across threads, and replaced after close()
    • a plain get reads only the value columns and a tagged get the full row
    • the fork fallback through both get and set
  • Existing tests now cover the metadata path's error handling.
  • Keeping the cursor after close(), never taking the new path, or deleting unreadable entries by a key that matches nothing each makes tests fail.
  • bluecheck, doc8, flake8, isortcheck, mypy and pylint pass.

@avgvi
avgvi merged commit d857808 into master Nov 2, 2026
23 checks passed
@avgvi
avgvi deleted the perf/get-lean-path branch November 2, 2026 20:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant