gh-156955: Speed up csv.writer by avoiding per char searches - #156956
Open
brittanyrey wants to merge 1 commit into
Open
gh-156955: Speed up csv.writer by avoiding per char searches#156956brittanyrey wants to merge 1 commit into
brittanyrey wants to merge 1 commit into
Conversation
…er character join_append_data() tested every character of every field for membership in dialect->lineterminator with PyUnicode_FindChar(), an out-of-line call made twice per character (the function runs a count pass and a copy pass). At 54% of cycles it was the single hottest symbol in csv.writer, which made writing CSV slower than parsing it back. Cache the terminator's highest code point on the dialect, which is immutable, and compare inline. Ordinary text exceeds that maximum, so the membership test is skipped without touching the terminator at all; when it does run it is a short loop over the terminator's characters rather than a cross-module call. Membership semantics are unchanged, including multi-character, empty and non-BMP terminators. Interleaved A/B, median of 25 per-round ratios, pinned to one CPU: mixed 2000x4 1.430 ms -> 0.547 ms 2.62x text 2000x4 1.486 ms -> 0.561 ms 2.64x wide 500x2 (200ch) 3.159 ms -> 1.094 ms 2.88x mixed QUOTE_ALL 1.438 ms -> 0.606 ms 2.37x mixed lineterm='\n' 1.335 ms -> 0.605 ms 2.21x mixed lineterm='END' 1.535 ms -> 0.822 ms 1.87x quoted 2000x4 0.486 ms -> 0.323 ms 1.50x short 5000x4 0.906 ms -> 0.650 ms 1.39x csv.reader (control) 0.887 ms -> 0.887 ms 1.00x The maximum is cached on the dialect rather than recomputed per field so that the change never loses. Degenerate inputs (rows of empty fields, or a 4096-character lineterminator) measure 1.00-1.01x, and a long terminator with short fields improves from 0.11x to 2.94x against a per-field variant.
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.
In
_csv,join_append_data()tested every character of every field for membership indialect->lineterminatorwithPyUnicode_FindChar()(an out-of-line call made twice per character; the function runs a count pass and a copy pass). At 54% of cycles it was the single hottest symbol incsv.writer, which made writing CSV slower than parsing it back.perf recordoncsv.writer, 2000 rows x 4 fields x 400 iterations, before:Cache the terminator's highest code point on the dialect, which is immutable, and compare inline. A character above that maximum cannot be in the terminator, so the scan is skipped. When it does run, it is a lightweight loop over the terminator rather than a boundary crossing call.
PyUnicode_FindChardisappears from the profile entirely.Results
Interleaved A/B, median of 25 per-round ratios, pinned to one CPU, with
csv.readeras an untouched control:wide 500x2(200-char fields)text 2000x4mixed 2000x4mixed QUOTE_ALLlineterminator='\n'lineterminator='END'quoted 2000x4short 5000x4csv.reader(control)Additional adversarial cases chosen to try and draw out regressions — nothing is below 1.00x:
lineterminator4096 charslineterminator4096 charslineterminator='\r\n'lineterminator4096 charsRelation to prior work
Reviewed comments and pulled in benchmarks from related PRs to harden this implementation.
Benchmark Code + Additional Performance Outputs
Benchmark code
Five blocks: the first three are this PR's, the last two are taken verbatim from
the related PRs so the results are directly comparable to theirs.
Alternates the variants adjacent in time and reports the median of per-round
ratios rather than a ratio of global minima, because this machine drifts between
windows. Variants are separate _csv*.so builds swapped on PYTHONPATH against
one unchanged python, since comparing two python binaries in the same build
tree would load whichever shared module was built last.
Built specifically to look for inputs where the change loses.
PyUnicodeWriterincsv.writer#138271 — csv.writer pyperf harnessVerbatim from #138271 (comment)
pyperf results — csv.writer harness from #138271
The harness #138271 used to show its own approach was 1.18x slower applied against this implementation.
Same script. main vs this branch.