Perf: remove O(n) from special_map when constructing a list when we already know the element type#6951
Conversation
…_typed Signed-off-by: Jacinta Ferrant <236437600+jacinta-stacks@users.noreply.github.com>
Signed-off-by: Jacinta Ferrant <236437600+jacinta-stacks@users.noreply.github.com>
francesco-stacks
left a comment
There was a problem hiding this comment.
lgtm! Just a couple of nits
Signed-off-by: Jacinta Ferrant <236437600+jacinta-stacks@users.noreply.github.com>
… into chore/perf-cons-list
francesco-stacks
left a comment
There was a problem hiding this comment.
unit tests are currently failing, otherwise lgtm
… into chore/perf-cons-list
benjamin-stacks
left a comment
There was a problem hiding this comment.
My concern is that there is indeed a small trade-off here: We're gaining a bit of performance in exchange for a slight reduction in code maintainability because there are now two separate implementations of the same functionality.
I'm also not sure this is truly an O(n) reduction, because the way I understand it, we're replacing this:
for x in list:
compute_value(x)
for x in list:
compute_least_supertype(x, prev)
with this:
for x in list:
compute_value(x)
compute_least_supertype(x, prev)
so unless I'm misunderstanding something (which is very possible!), the performance improvement is only from removing the overhead of the second iteration.
Thoughts?
| element_type_opt = Some(match element_type_opt.take() { | ||
| None => res_type, | ||
| Some(element_type) => { | ||
| TypeSignature::least_supertype(exec_state.epoch(), &element_type, &res_type)? |
There was a problem hiding this comment.
construct_parent_list_type() explicitly calls least_supertype_v2_1 instead.
I lack the context to judge whether that is currently okay (and it seems to me that your version is more correct), but even if it is: If there is ever a least_supertype_v_42_0, this might create a subtle divergence in behavior between cons_list and cons_list_typed.
There was a problem hiding this comment.
This worries me too.
At the moment, we rely on least_supertype_v2_1 under the hood, and changing this behavior could introduce regressions in block validation, replay, or genesis sync when processing epoch 2.0 blocks.
Another point to consider is whether we need to adjust the behavior of construct_parent_list_type(), since TypeSignature::parent_list_type() depends on least_supertype_v2_1.
As a side note,
TypeSignature::parent_list_type()appears to be used directly in the type checker for versionv2_05, while inleast_supertypethe same epoch should work withleast_supertype_v2_0
| assert_eq!(expected, execute(test).unwrap().unwrap()); | ||
|
|
||
| // All sequences empty — result is an empty list. | ||
| let test = "(map + (list) (list))"; |
There was a problem hiding this comment.
Fun fact, the type checker doesn't actually allow this, despite it being totally valid.
Correct. The save derive from removing an interation over N. |
federico-stacks
left a comment
There was a problem hiding this comment.
To properly evaluate the performance impact of this PR, it first needs to be rebased on top of:
- #7022, which introduces
apply_evaluatedand removes atom usage - #7068, which fixes the off-by-one indexing issue and simplifies the
special_mapimplementation
After those changes, what would remain from this PR is primarily the cons_list_typed approach.
I followed these steps locally and can confirm that the small performance improvements still hold, due to the removal of an iteration over N elements.
That said, I also have concerns about the use of least_supertype (instead of the current least_supertype_v2_1). Additionally, this would result in cons_list and cons_list_typed behaving differently (see discussion here: #6951 (review)).
We may also want to better understand the implications of using least_supertype_v2_1 in parent_list_type(), even in the current implementation (see #6951 (comment)).
|
I vote we close this one. Given the concern from @federico-stacks and myself, and the only tiny perf improvement, I think this is not worth it. |
Codecov Report❌ Patch coverage is
❌ Your project status has failed because the head coverage (84.98%) is below the adjusted base coverage (84.99%). You can increase the head coverage or adjust the Removed Code Behavior. Additional details and impacted files@@ Coverage Diff @@
## develop #6951 +/- ##
===========================================
- Coverage 85.00% 84.98% -0.02%
===========================================
Files 412 412
Lines 219942 219975 +33
Branches 338 338
===========================================
- Hits 186951 186941 -10
- Misses 32991 33034 +43
... and 40 files with indirect coverage changes Continue to review full report in Codecov by Sentry.
🚀 New features to boost your workflow:
|
|
This pull request has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs. |
Related to https://github.com/stx-labs/core-epics/issues/117
special_mappreviously calledValue::cons_list(mapped_results, epoch)to build its output list. That function internally callsconstruct_parent_list_type, which walks every element callingtype_ofand folding withleast_supertype. This is an O(n) second pass over results that had already been computed during the mapping loop itself. So now it accumulates the element type incrementally in the loop (element_type_opt) and callsValue::cons_list_typed, which goes straight toListTypeData::new_list. The O(n) post-pass is eliminated entirely.Benchmark Results
cargo bench -p clarity --bench sequence_ops -- special_map, compared against pre-change baselineTLDR: Not very noticeable improvement...The improvement makes all (map ...) calls in every Clarity contract pay less per invocation, with no tradeoffs. Not sure how much that will actually affect block production but its a net improvement regardless.