Skip to content

Commit 3232f3f

Browse files
committed
cachedb: an optional allocation-free read
get() must hand back a value the caller owns and pkg_free()s, so every lookup on a hot path costs a pkg_malloc and a pkg_free that exist only to satisfy that contract. Profiling the cachedb_perf read path put the allocator at a fixed ~70 ns/op - 15% of a 470 ns lookup over 50 000 entries, 30% of a 236 ns one when the working set is cache resident - while memcpy never appeared at all. Add an optional endpoint that reads into a buffer the caller already owns: no allocation, and one copy instead of two. It is advertised by CACHEDB_CAP_GET_BUF, so a caller uses it only where a backend offers it and keeps get() as the fallback; nothing about the existing endpoint changes, and no backend is obliged to implement the new one. The contract is written for the failure cases, because those are what a caller gets wrong: the buffer must be private to the calling process, it may be written speculatively and abandoned so its contents are undefined on any non-hit, *vlen and *needed are zeroed before anything else so an ignored return code yields a zero length rather than an uninitialised one, and a value that does not fit reports the size it would need in *needed while leaving *vlen at 0 - {buf, *vlen} is always a valid str. A hit is 0 here, unlike get(), which is called out in the comment. CACHEDB_CAP_GET_BUF answers a runtime question - whether the backend a caller happens to be configured against implements the endpoint. It cannot answer the one a module has to settle first: whether the core it is being compiled against has the endpoint at all. get_buf is a member of cachedb_funcs, so referencing it against an older core is a compile error rather than a failed test, and the capability is an enum constant, invisible to the preprocessor. So there is a plain define beside it, and a module can compile against either core and pick the faster path up on its own. The runtime check is still required and the comment says so - a core may carry the endpoint while the backend in use does not implement it. The first implementation is cachedb_perf, in the following commit.
1 parent 5a7f3a6 commit 3232f3f

2 files changed

Lines changed: 52 additions & 0 deletions

File tree

‎cachedb/cachedb.h‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,36 @@ typedef struct cachedb_funcs_t {
162162
*/
163163
int (*is_replicated) (cachedb_con *con);
164164

165+
165166
int capability;
167+
168+
/**
169+
* OPTIONAL, advertised by CACHEDB_CAP_GET_BUF. Reads a value into a
170+
* caller-owned buffer, so a hot read path need not allocate: it is
171+
* get() without the pkg_malloc the caller would then have to free.
172+
*
173+
* @buf must be memory private to the calling process (its own stack or
174+
* pkg). A backend may write into it speculatively and then abandon the
175+
* attempt, so on ANY outcome other than a hit its contents are
176+
* undefined - never a stale previous value to fall back on. The value
177+
* is not NUL-terminated.
178+
*
179+
* Return values:
180+
* -3: @buf is too small; *vlen is 0 and *needed holds the size that
181+
* would be required (the caller may then fall back to get())
182+
* -2: key does not exist, or has expired
183+
* -1: internal error, or a malformed request (NULL @buf, or @buflen
184+
* below the backend's documented minimum)
185+
* 0: found; *vlen bytes were written to @buf, always <= @buflen
186+
*
187+
* *vlen and *needed are zeroed before anything else is done, so a
188+
* caller that ignores the return code reads a zero length rather than
189+
* an uninitialised one. @needed may be NULL. NOTE this differs from
190+
* get(), which signals a hit with a positive value at the script
191+
* boundary - a hit here is 0.
192+
*/
193+
int (*get_buf) (cachedb_con *con, str *attr, char *buf,
194+
unsigned int buflen, unsigned int *vlen, unsigned int *needed);
166195
} cachedb_funcs;
167196

168197
typedef struct cachedb_engines {

‎cachedb/cachedb_cap.h‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,31 @@ typedef enum {
5151
CACHEDB_CAP_MAP_REMOVE = 1<<13,
5252
CACHEDB_CAP_MAP =
5353
(CACHEDB_CAP_MAP_GET|CACHEDB_CAP_MAP_SET|CACHEDB_CAP_MAP_REMOVE),
54+
55+
/* backend implements get_buf() - an allocation-free read into a
56+
* caller-owned buffer. Optional: every backend still provides get() */
57+
CACHEDB_CAP_GET_BUF = 1<<14,
5458
} cachedb_cap;
5559

60+
/*
61+
* Preprocessor-visible companion to CACHEDB_CAP_GET_BUF. A consumer cannot test
62+
* for the endpoint at runtime alone: get_buf is a struct member, so referencing it
63+
* fails to compile against a core that predates it, and the capability above is an
64+
* enum constant the preprocessor cannot see. This lets a module compile against
65+
* either core and pick the allocation-free path up automatically:
66+
*
67+
* #ifdef CACHEDB_HAVE_GET_BUF
68+
* if (cdbf.get_buf && CACHEDB_CAPABILITY(&cdbf, CACHEDB_CAP_GET_BUF))
69+
* ... use it ...
70+
* else
71+
* #endif
72+
* ... use get() ...
73+
*
74+
* The runtime half stays necessary: a core may provide the endpoint while the
75+
* configured backend does not implement it.
76+
*/
77+
#define CACHEDB_HAVE_GET_BUF 1
78+
5679
#define CACHEDB_CAPABILITY(cdbf,cpv) (((cdbf)->capability & (cpv)) == (cpv))
5780

5881
static inline int check_cachedb_api(cachedb_engine *cde)

0 commit comments

Comments
 (0)