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.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..87b64e25 100644 --- a/src/cbor/common.h +++ b/src/cbor/common.h @@ -311,10 +311,26 @@ 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 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 */ -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. + * + * 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); /** Decreases the item's reference count by one, deallocating the item if * needed @@ -322,9 +338,20 @@ CBOR_EXPORT cbor_item_t* cbor_incref(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 */ -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