perf: remove per-row String allocations from the Spark url functions#23884
Open
andygrove wants to merge 2 commits into
Open
perf: remove per-row String allocations from the Spark url functions#23884andygrove wants to merge 2 commits into
andygrove wants to merge 2 commits into
Conversation
url_encode built a String for every row via byte_serialize(..).collect::<String>() and collected the results into a StringArray, so each row paid for its own allocation. The percent-encoded form is now assembled in a single scratch buffer reused across rows and appended to a pre-sized builder. The now-unused UrlEncode::encode helper, which returned a Result that was never an Err, is removed. -56% at 1024 rows and -48% at 8192 against the benchmark added in apache#23882.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #23884 +/- ##
==========================================
- Coverage 80.65% 80.65% -0.01%
==========================================
Files 1091 1092 +1
Lines 371031 371089 +58
Branches 371031 371089 +58
==========================================
+ Hits 299256 299293 +37
- Misses 53935 53946 +11
- Partials 17840 17850 +10 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…key array directly url_decode ended with .map(|parsed| parsed.into_owned()). Both replace_plus and decode_utf8 borrow their input when there is nothing to rewrite, so that allocated a String for every row even when the value needed no decoding at all. decode now returns a Cow and the array paths append it to a pre-sized builder. To let the Cow survive, spark_handled_url_decode's error handler changes from a closure over Result<Option<String>> to an OnDecodeError enum. Its only caller, try_url_decode, passed a closure that mapped Err to Ok(None), which is exactly OnDecodeError::Null. parse_url built its all-null key array for the 2-argument form by calling append_null() once per row on an uncapacitated builder; new_null_array does it in one allocation. url_decode -29% where nothing needs unescaping, -4% where it does, against the benchmark added in apache#23882.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Which issue does this PR close?
N/A
Rationale for this change
Three allocation problems in the
urlmodule, all on the per-row path.url_encodebuilt aStringfor every row:url_decodeended with.map(|parsed| parsed.into_owned()). Bothreplace_plusanddecode_utf8return aCowthat borrows when there isnothing to rewrite, so
into_ownedallocated aStringfor every row even whenthe value contained no percent-escapes and no
+at all.parse_urlbuilt the all-nullkeyarray for the two-argument form bycalling
append_null()once per row against a builder with no capacity.What changes are included in this PR?
url_encode.rs:encode_all!loop that clears and refills one scratch
Stringper batch.UrlEncode::encodeis removed; it returned aResultwhose error arm wasnever constructed and now has no callers.
url_decode.rs:decodereturnsCow<'_, str>instead ofString. Whenreplace_plusborrows, the decode borrows straight from the input; when it has already
allocated (the input contained
+), owning the decoded form costs nothingbeyond what was already spent.
Cowto a pre-sized builder rather thancollecting
Result<StringArray>from a per-rowString.spark_handled_url_decode's second parameter changes fromimpl Fn(Result<Option<String>>) -> Result<Option<String>>to a newOnDecodeErrorenum. TheStringin that signature is what forced theallocation. Its only caller is
try_url_decode, whose closure wasErr(_) => Ok(None)— exactlyOnDecodeError::Null.parse_url.rs:append_null()-per-row loop becomesnew_null_array(&DataType::Utf8, len).No behaviour change in any of the three.
Are these changes tested?
Existing coverage pins the behaviour:
spark/url/url_encode.slt,url_decode.slt,try_url_decode.slt, andparse_url.sltassert concreteresults including reserved characters,
+handling, malformed percent-encoding(which must error for
url_decodeand yield NULL fortry_url_decode), and thetwo- versus three-argument
parse_urlforms. All 5spark/urlsqllogictestfiles pass, along with the 258
datafusion-sparkunit tests.The
try_url_decodeunit test is what pins theOnDecodeError::Nullpath,since it drives a malformed input through the refactored signature.
Benchmarks are added separately in #23882 so the baselines can be measured on
mainbefore this lands.Benchmarks
Criterion,
apache/main@f1ab86dadas baseline. Median of the reportedchange interval.
url_encode:url_encode/utf8url_encode/largeutf8url_encode/utf8viewurl_decode, split by whether the input actually needs unescaping:url_decode/plain_utf8url_decode/plain_utf8viewurl_decode/escaped_utf8url_decode/escaped_largeutf8The
plainrows are the ones that benefit: with nothing to unescape the decodedvalue borrows its input. The
escapedrows still have to allocate, so they gainonly the pre-sized builder and the removed intermediate — a few percent, as
expected.
parse_urlhas no benchmark; its change removes anappend_null()loop thatruns once per batch rather than affecting a measured per-row path.
Are there any user-facing changes?
No change to SQL behaviour — all three functions return identical results.
spark_handled_url_decodeis public Rust API and its signature changes asdescribed above;
OnDecodeErroris new public API ondatafusion-spark.