From ae7a45bf3cf0284acb8281f136e4d5a05645bd62 Mon Sep 17 00:00:00 2001 From: PJK Date: Sun, 19 Jul 2026 00:45:01 +0200 Subject: [PATCH 1/3] Inline cbor_incref and cbor_decref fast paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move the refcount hot path (increment; decrement + zero check) into static inline definitions in the public common.h. The free branch of cbor_decref stays out of line as _cbor_decref_free, called from the inline once refcount hits zero. For typical DOM-building workloads (build tree, serialize, drop), every construction touches an incref/decref that previously crossed a function-call boundary. Inlining removes that overhead at every call site — including libcbor's own internal recursion in the free branch — without changing semantics. Note: switching to static inline in the header changes ABI for consumers that dlsym cbor_incref/cbor_decref out of the shared library. All 28 existing ctest suites pass. Co-Authored-By: Claude Opus 4.7 --- src/cbor/common.c | 122 +++++++++++++++++++++------------------------- src/cbor/common.h | 19 +++++++- 2 files changed, 73 insertions(+), 68 deletions(-) diff --git a/src/cbor/common.c b/src/cbor/common.c index 75ee3888..b7d6b708 100644 --- a/src/cbor/common.c +++ b/src/cbor/common.c @@ -81,80 +81,70 @@ bool cbor_is_float(const cbor_item_t* item) { return cbor_isa_float_ctrl(item) && !cbor_float_ctrl_is_ctrl(item); } -cbor_item_t* cbor_incref(cbor_item_t* item) { - item->refcount++; - return item; -} - -void cbor_decref(cbor_item_t** item_ref) { - cbor_item_t* item = *item_ref; - CBOR_ASSERT(item->refcount > 0); - if (--item->refcount == 0) { - switch (item->type) { - case CBOR_TYPE_UINT: - /* Fallthrough */ - case CBOR_TYPE_NEGINT: - /* Combined allocation, freeing the item suffices */ - { break; } - case CBOR_TYPE_BYTESTRING: { - if (cbor_bytestring_is_definite(item)) { - _cbor_free(item->data); - } else { - /* We need to decref all chunks */ - cbor_item_t** handle = cbor_bytestring_chunks_handle(item); - for (size_t i = 0; i < cbor_bytestring_chunk_count(item); i++) - cbor_decref(&handle[i]); - _cbor_free(((struct cbor_indefinite_string_data*)item->data)->chunks); - _cbor_free(item->data); - } - break; - } - case CBOR_TYPE_STRING: { - if (cbor_string_is_definite(item)) { - _cbor_free(item->data); - } else { - /* We need to decref all chunks */ - cbor_item_t** handle = cbor_string_chunks_handle(item); - for (size_t i = 0; i < cbor_string_chunk_count(item); i++) - cbor_decref(&handle[i]); - _cbor_free(((struct cbor_indefinite_string_data*)item->data)->chunks); - _cbor_free(item->data); - } - break; - } - case CBOR_TYPE_ARRAY: { - /* Get all items and decref them */ - cbor_item_t** handle = cbor_array_handle(item); - size_t size = cbor_array_size(item); - for (size_t i = 0; i < size; i++) - if (handle[i] != NULL) cbor_decref(&handle[i]); +void _cbor_decref_free(cbor_item_t* item) { + switch (item->type) { + case CBOR_TYPE_UINT: + /* Fallthrough */ + case CBOR_TYPE_NEGINT: + /* Combined allocation, freeing the item suffices */ + { break; } + case CBOR_TYPE_BYTESTRING: { + if (cbor_bytestring_is_definite(item)) { _cbor_free(item->data); - break; - } - case CBOR_TYPE_MAP: { - struct cbor_pair* handle = cbor_map_handle(item); - for (size_t i = 0; i < item->metadata.map_metadata.end_ptr; - i++, handle++) { - cbor_decref(&handle->key); - if (handle->value != NULL) cbor_decref(&handle->value); - } + } else { + /* We need to decref all chunks */ + cbor_item_t** handle = cbor_bytestring_chunks_handle(item); + for (size_t i = 0; i < cbor_bytestring_chunk_count(item); i++) + cbor_decref(&handle[i]); + _cbor_free(((struct cbor_indefinite_string_data*)item->data)->chunks); _cbor_free(item->data); - break; } - case CBOR_TYPE_TAG: { - if (item->metadata.tag_metadata.tagged_item != NULL) - cbor_decref(&item->metadata.tag_metadata.tagged_item); + break; + } + case CBOR_TYPE_STRING: { + if (cbor_string_is_definite(item)) { + _cbor_free(item->data); + } else { + /* We need to decref all chunks */ + cbor_item_t** handle = cbor_string_chunks_handle(item); + for (size_t i = 0; i < cbor_string_chunk_count(item); i++) + cbor_decref(&handle[i]); + _cbor_free(((struct cbor_indefinite_string_data*)item->data)->chunks); _cbor_free(item->data); - break; } - case CBOR_TYPE_FLOAT_CTRL: { - /* Floats have combined allocation */ - break; + break; + } + case CBOR_TYPE_ARRAY: { + /* Get all items and decref them */ + cbor_item_t** handle = cbor_array_handle(item); + size_t size = cbor_array_size(item); + for (size_t i = 0; i < size; i++) + if (handle[i] != NULL) cbor_decref(&handle[i]); + _cbor_free(item->data); + break; + } + case CBOR_TYPE_MAP: { + struct cbor_pair* handle = cbor_map_handle(item); + for (size_t i = 0; i < item->metadata.map_metadata.end_ptr; + i++, handle++) { + cbor_decref(&handle->key); + if (handle->value != NULL) cbor_decref(&handle->value); } + _cbor_free(item->data); + break; + } + case CBOR_TYPE_TAG: { + if (item->metadata.tag_metadata.tagged_item != NULL) + cbor_decref(&item->metadata.tag_metadata.tagged_item); + _cbor_free(item->data); + break; + } + case CBOR_TYPE_FLOAT_CTRL: { + /* Floats have combined allocation */ + break; } - _cbor_free(item); - *item_ref = NULL; } + _cbor_free(item); } void cbor_intermediate_decref(cbor_item_t* item) { cbor_decref(&item); } diff --git a/src/cbor/common.h b/src/cbor/common.h index bdb08af4..823659c3 100644 --- a/src/cbor/common.h +++ b/src/cbor/common.h @@ -314,7 +314,15 @@ CBOR_EXPORT bool cbor_is_undef(const cbor_item_t* item); * @param item Reference to an item * @return The input \p item */ -CBOR_EXPORT cbor_item_t* cbor_incref(cbor_item_t* item); +static inline cbor_item_t* cbor_incref(cbor_item_t* item) { + item->refcount++; + return item; +} + +/** Slow path of #cbor_decref: performs the actual deallocation once the + * reference count reaches zero. Do not call directly; use #cbor_decref. + */ +CBOR_EXPORT void _cbor_decref_free(cbor_item_t* item); /** Decreases the item's reference count by one, deallocating the item if * needed @@ -324,7 +332,14 @@ CBOR_EXPORT cbor_item_t* cbor_incref(cbor_item_t* item); * * @param item Reference to an item. Will be set to `NULL` if deallocated */ -CBOR_EXPORT void cbor_decref(cbor_item_t** item); +static inline void cbor_decref(cbor_item_t** item_ref) { + cbor_item_t* item = *item_ref; + CBOR_ASSERT(item->refcount > 0); + if (--item->refcount == 0) { + _cbor_decref_free(item); + *item_ref = NULL; + } +} /** Decreases the item's reference count by one, deallocating the item if * needed From 098266be314cb2524f4493c6a32238c78c3c6820 Mon Sep 17 00:00:00 2001 From: PJK Date: Sun, 19 Jul 2026 17:15:55 +0200 Subject: [PATCH 2/3] Document inline hot-path in Doxygen; add changelog entry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expand the Doxygen comments on cbor_incref, cbor_decref, and the new _cbor_decref_free helper to briefly explain the inlining rationale. Add a Next-release changelog entry marking this as ABI BREAKING — same signatures, but cbor_incref and cbor_decref are no longer exported from the shared library. Co-Authored-By: Claude Opus 4.7 --- CHANGELOG.md | 3 +++ src/cbor/common.h | 12 ++++++++++++ 2 files changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d50c6c1c..64dcbf19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ Template: Next --------------------- +- ABI BREAKING: [Inline `cbor_incref` and `cbor_decref` fast paths](https://github.com/PJK/libcbor/pull/434) + - `cbor_incref` and `cbor_decref` are now `static inline` in `cbor/common.h`; the deallocation branch of `cbor_decref` moved into a new exported helper `_cbor_decref_free`. Source-compatible — same signatures and semantics — but the two symbols are no longer exported from the shared library, so binaries relying on their extern resolution (including `dlsym`) will fail to link against the new `.so`. Rebuilding against the updated headers is sufficient. + 0.14.0 (2026-04-07) --------------------- diff --git a/src/cbor/common.h b/src/cbor/common.h index 823659c3..6c361384 100644 --- a/src/cbor/common.h +++ b/src/cbor/common.h @@ -311,6 +311,10 @@ CBOR_EXPORT bool cbor_is_undef(const cbor_item_t* item); * * This function can be used to extend reference counting to client code. * + * Defined `static inline` so the single-instruction increment is emitted + * directly at the call site — meaningful for DOM-building hot loops where + * every constructed item touches the refcount. + * * @param item Reference to an item * @return The input \p item */ @@ -321,6 +325,10 @@ static inline cbor_item_t* cbor_incref(cbor_item_t* item) { /** Slow path of #cbor_decref: performs the actual deallocation once the * reference count reaches zero. Do not call directly; use #cbor_decref. + * + * Split from #cbor_decref so the fast path (decrement + zero check) can + * inline while the recursive free logic — which dominates code size — + * stays out of line. */ CBOR_EXPORT void _cbor_decref_free(cbor_item_t* item); @@ -330,6 +338,10 @@ CBOR_EXPORT void _cbor_decref_free(cbor_item_t* item); * In case the item is deallocated, the reference count of all items this * item references will also be #cbor_decref 'ed recursively. * + * The fast path (decrement, check for zero) is `static inline`; only the + * uncommon deallocation branch calls into #_cbor_decref_free. This keeps + * every non-final decref a couple of instructions at the call site. + * * @param item Reference to an item. Will be set to `NULL` if deallocated */ static inline void cbor_decref(cbor_item_t** item_ref) { From 15f5d601a75232d97f55403412899c5d9147fa49 Mon Sep 17 00:00:00 2001 From: PJK Date: Sun, 19 Jul 2026 22:34:17 +0200 Subject: [PATCH 3/3] Reword refcount inline note: cbor_item_t trees, not "DOM" Co-Authored-By: Claude Opus 4.7 --- src/cbor/common.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cbor/common.h b/src/cbor/common.h index 6c361384..87b64e25 100644 --- a/src/cbor/common.h +++ b/src/cbor/common.h @@ -312,8 +312,8 @@ CBOR_EXPORT bool cbor_is_undef(const cbor_item_t* item); * This function can be used to extend reference counting to client code. * * Defined `static inline` so the single-instruction increment is emitted - * directly at the call site — meaningful for DOM-building hot loops where - * every constructed item touches the refcount. + * directly at the call site — meaningful for hot loops that build + * #cbor_item_t trees, where every constructed item touches the refcount. * * @param item Reference to an item * @return The input \p item