Performance: Arc<Value> pop/push cycle overhead for heap types #368

Closed
opened 2026-03-24 00:25:57 +00:00 by navicore · 2 comments
navicore commented 2026-03-24 00:25:57 +00:00 (Migrated from github.com)

Problem

Every pop() of a heap value does Arc::try_unwrap (decrement + potential free), and every push() does Arc::new (allocate + increment). For operations that pop a value, transform it, and push it back, this creates an unnecessary alloc/dealloc cycle even when the value is sole-owned.

This affects all heap types (Float, String, Variant, Map, Channel, etc.) on every operation.

Impact

The build-100k benchmark calls list.push 100,000 times. Each call does:

  1. pop value (Int — inline, cheap)
  2. pop list (Arc::try_unwrap on outer Arc<Value>)
  3. COW mutation on inner Arc<VariantData> (works correctly)
  4. push result (Arc::new to wrap in new Arc<Value>)

Steps 2 and 4 allocate and free an Arc per operation, even though the value is sole-owned throughout.

Potential Approaches

  • StackValue-level operations: Work directly on the raw u64 tagged pointer without going through Value. Read the Arc<Value> pointer, reach into the inner data, mutate, write back. No Arc alloc/dealloc.
  • Arena-allocated heap values: Replace Arc<Value> with arena-allocated heap objects that use inline refcounting. Clone is a refcount bump, drop is a decrement — no allocation at all.
  • Hybrid: Keep Arc<Value> for cross-strand sharing but use a lighter wrapper for strand-local operations.

Context

This was identified during the tagged-ptr migration (PR #367). The COW optimization for collections (Vec fields + Arc::get_mut) is correct but its benefit is limited because the surrounding pop/push infrastructure dominates the cost.

A targeted workaround (list.push!) is being added for the most critical path, but the underlying issue affects all heap type operations.

## Problem Every `pop()` of a heap value does `Arc::try_unwrap` (decrement + potential free), and every `push()` does `Arc::new` (allocate + increment). For operations that pop a value, transform it, and push it back, this creates an unnecessary alloc/dealloc cycle even when the value is sole-owned. This affects all heap types (Float, String, Variant, Map, Channel, etc.) on every operation. ## Impact The build-100k benchmark calls `list.push` 100,000 times. Each call does: 1. `pop` value (Int — inline, cheap) 2. `pop` list (`Arc::try_unwrap` on outer `Arc<Value>`) 3. COW mutation on inner `Arc<VariantData>` (works correctly) 4. `push` result (`Arc::new` to wrap in new `Arc<Value>`) Steps 2 and 4 allocate and free an Arc per operation, even though the value is sole-owned throughout. ## Potential Approaches - **StackValue-level operations**: Work directly on the raw `u64` tagged pointer without going through `Value`. Read the `Arc<Value>` pointer, reach into the inner data, mutate, write back. No Arc alloc/dealloc. - **Arena-allocated heap values**: Replace `Arc<Value>` with arena-allocated heap objects that use inline refcounting. Clone is a refcount bump, drop is a decrement — no allocation at all. - **Hybrid**: Keep `Arc<Value>` for cross-strand sharing but use a lighter wrapper for strand-local operations. ## Context This was identified during the tagged-ptr migration (PR #367). The COW optimization for collections (Vec fields + Arc::get_mut) is correct but its benefit is limited because the surrounding pop/push infrastructure dominates the cost. A targeted workaround (`list.push!`) is being added for the most critical path, but the underlying issue affects all heap type operations.
navicore commented 2026-03-28 05:13:01 +00:00 (Migrated from github.com)

@navicore-bot please consider working this issue if you find a feasible approach that doesn't compromise the tagged-ptr work recently completed.

@navicore-bot please consider working this issue if you find a feasible approach that doesn't compromise the tagged-ptr work recently completed.
navicore commented 2026-03-29 01:54:39 +00:00 (Migrated from github.com)

Completed. The Arc pop/push overhead is eliminated for all hot-path operations via peek_heap_mut / heap_value_mut primitives:

  • list.push#378
  • map.set / map.remove#379
  • variant.append / list.set#380

Sole-owned heap values are now mutated in place without the Arc alloc/dealloc cycle. Shared values fall back to the existing clone path.

Completed. The Arc<Value> pop/push overhead is eliminated for all hot-path operations via `peek_heap_mut` / `heap_value_mut` primitives: - **list.push** — #378 - **map.set / map.remove** — #379 - **variant.append / list.set** — #380 Sole-owned heap values are now mutated in place without the Arc alloc/dealloc cycle. Shared values fall back to the existing clone path.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
navicore/patch-seq#368
No description provided.