Conversation
a89a6a1 to
66913af
Compare
| context.arguments[0].span, | ||
| )); | ||
| } | ||
| indexmap::map::Entry::Vacant(e) => { |
There was a problem hiding this comment.
This is more of a correctness sidequest rather than a performance improvement. In the case where the key is not in the map, we can avoid the preemptive clone of the key.
66913af to
1f6da9b
Compare
|
|
||
| for (name, ty) in ty.members().iter() { | ||
| // Check for optional members that should be set to `None` | ||
| // Check for optional members that should be set to none |
There was a problem hiding this comment.
I took the approach that WDL None values should be referred to as "none" to disambiguate it from Rust's None value.
| ty: impl Into<Type>, | ||
| name: Arc<String>, | ||
| members: Arc<IndexMap<String, Value>>, | ||
| members: impl Into<IndexMap<String, Value>>, |
There was a problem hiding this comment.
Unlike name, every caller of this just constructed an IndexMap and immediately wrapped it with an Arc. So the original Arc was truly just for making this cheaply clonable.
While investigating Sprocket's baseline memory usage, certain workloads—particularly those involving deeply nested compound types like `Map[String, Map[String, ...]]`—scaled poorly. The root cause was the `Value` enum in `wdl-engine`, which occupied 96 bytes per element due to large inline types (`Type` at 56 bytes, `TaskPostEvaluationValue` at 96 bytes) inflating the enum even though the common case (`PrimitiveValue`) is only 16 bytes. This refactor pushes `Arc` wrapping down into the types that inflate `Value`. Each compound value type (`Array`, `Map`, `Pair`, `Struct`, `EnumVariant`) and each large hidden value type (`TaskPreEvaluation`, `TaskPostEvaluation`) becomes a thin wrapper around `Arc<Inner>`, where the inner struct owns all fields directly—eliminating nested `Arc` layering. Two new wrapper types (`NoneValue` and `TypeNameRefValue`) handle variants that previously stored a bare `Type`. `CallValue` wraps its `CallType` field behind `Arc` as well. The tradeoff is an additional pointer indirection when accessing fields of compound and hidden values, since their data now lives behind `Arc` on the heap rather than inline in the enum. In practice, this cost is negligible: these types were already heap-allocated internally (e.g., `Array` stored an `Arc<Vec<Value>>`), so the refactor consolidates rather than adds indirection. Clone remains cheap—a refcount bump on one `Arc` rather than multiple. A const assertion prevents `Value` from exceeding 24 bytes in the future. The `as_map` standard library function was also updated to use the `IndexMap` entry API, eliminating a redundant key clone. See PR for benchmark results.
1f6da9b to
6ee481b
Compare
peterhuene
left a comment
There was a problem hiding this comment.
Looks great, just one minor nit!
| let mut arguments: [CallArgument; MAX_PARAMETERS] = | ||
| std::array::from_fn(|_| CallArgument::none()); |
There was a problem hiding this comment.
I think this could just be a call to repeat as CallArgument should probably be Clone:
| let mut arguments: [CallArgument; MAX_PARAMETERS] = | |
| std::array::from_fn(|_| CallArgument::none()); | |
| let mut arguments: [CallArgument; MAX_PARAMETERS] = | |
| std::array::repeat(CallArgument::none())); |
While investigating Sprocket's baseline memory usage, I noticed that certain workloads—particularly those involving deeply nested compound types like
Map[String, Map[String, ...]]—scaled poorly. The root cause was theValueenum inwdl-engine, which occupied 96 bytes per element due to large inline types (Typeat 56 bytes,TaskPostEvaluationValueat 96 bytes) inflating the enum even though the common case (PrimitiveValue) is only 16 bytes.This refactor pushes
Arcwrapping down into the types that inflateValue. Each compound value type (Array,Map,Pair,Struct,EnumVariant) and each large hidden value type (TaskPreEvaluation,TaskPostEvaluation) becomes a thin wrapper aroundArc<Inner>, where the inner struct owns all fields directly—eliminating nestedArclayering. Two new wrapper types (NoneValueandTypeNameRefValue) handle variants that previously stored a bareType.CallValuewraps itsCallTypefield behindArcas well.The tradeoff is an additional pointer indirection when accessing fields of compound and hidden values, since their data now lives behind
Arcon the heap rather than inline in the enum. In practice, this cost is negligible: these types were already heap-allocated internally (e.g.,Arraystored anArc<Vec<Value>>), so the refactor consolidates rather than adds indirection. Clone remains cheap—a refcount bump on oneArcrather than multiple.A const assertion prevents
Valuefrom exceeding 24 bytes in the future. Theas_mapstandard library function was also updated to use theIndexMapentry API, eliminating a redundant key clone.Benchmarked on an Apple M3 Max against the prior version across five synthetic workloads:
The improvement is most pronounced for nested maps because each map entry stores a
Valuefor the key and aValuefor the nested map, so the 4x per-element reduction compounds across all four nesting levels. Flat arrays benefit less because only oneArraycontainer exists—the savings come from shrinking each elementValuewithin its backingVec.For all contributors: