Skip to content

Commit 1c60884

Browse files
committed
gh-156955: Speed up csv.writer by not scanning lineterminator per 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.
1 parent 532b9db commit 1c60884

2 files changed

Lines changed: 41 additions & 3 deletions

File tree

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Speed up :func:`csv.writer` by up to 2.9x. Writing a field no longer calls
2+
:c:func:`PyUnicode_FindChar` on the dialect's ``lineterminator`` once per
3+
character.

Modules/_csv.c

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ typedef struct {
116116
Py_UCS4 delimiter; /* field separator */
117117
Py_UCS4 quotechar; /* quote character */
118118
Py_UCS4 escapechar; /* escape character */
119+
Py_UCS4 lineterm_maxchar; /* highest code point in lineterminator */
119120
PyObject *lineterminator; /* string to write between records */
120121

121122
} DialectObj;
@@ -332,6 +333,22 @@ _set_str(const char *name, PyObject **target, PyObject *src, const char *dflt)
332333
return 0;
333334
}
334335

336+
static Py_UCS4
337+
str_maxchar(PyObject *s)
338+
{
339+
int kind = PyUnicode_KIND(s);
340+
const void *data = PyUnicode_DATA(s);
341+
Py_ssize_t len = PyUnicode_GET_LENGTH(s);
342+
Py_UCS4 maxchar = 0;
343+
for (Py_ssize_t i = 0; i < len; i++) {
344+
Py_UCS4 c = PyUnicode_READ(kind, data, i);
345+
if (c > maxchar) {
346+
maxchar = c;
347+
}
348+
}
349+
return maxchar;
350+
}
351+
335352
static int
336353
dialect_check_quoting(int quoting)
337354
{
@@ -533,6 +550,7 @@ dialect_new(PyTypeObject *type, PyObject *args, PyObject *kwargs)
533550
DIASET(_set_bool, "skipinitialspace", &self->skipinitialspace, skipinitialspace, false);
534551
DIASET(_set_bool, "strict", &self->strict, strict, false);
535552
#undef DIASET
553+
self->lineterm_maxchar = str_maxchar(self->lineterminator);
536554

537555
/* validate options */
538556
if (dialect_check_quoting(self->quoting))
@@ -1165,6 +1183,21 @@ join_reset(WriterObj *self)
11651183

11661184
#define MEM_INCR 32768
11671185

1186+
static inline int
1187+
in_lineterminator(Py_UCS4 c, DialectObj *dialect)
1188+
{
1189+
PyObject *lt = dialect->lineterminator;
1190+
int kind = PyUnicode_KIND(lt);
1191+
const void *data = PyUnicode_DATA(lt);
1192+
Py_ssize_t len = PyUnicode_GET_LENGTH(lt);
1193+
for (Py_ssize_t i = 0; i < len; i++) {
1194+
if (PyUnicode_READ(kind, data, i) == c) {
1195+
return 1;
1196+
}
1197+
}
1198+
return 0;
1199+
}
1200+
11681201
/* Calculate new record length or append field to record. Return new
11691202
* record length.
11701203
*/
@@ -1176,6 +1209,10 @@ join_append_data(WriterObj *self, int field_kind, const void *field_data,
11761209
DialectObj *dialect = self->dialect;
11771210
Py_ssize_t i;
11781211
Py_ssize_t rec_len;
1212+
/* A character above this cannot be in the line terminator, so the
1213+
scan below is skipped; the default "\r\n" rejects all ordinary
1214+
text that way. */
1215+
Py_UCS4 term_maxchar = dialect->lineterm_maxchar;
11791216

11801217
#define INCLEN \
11811218
do {\
@@ -1213,9 +1250,7 @@ join_append_data(WriterObj *self, int field_kind, const void *field_data,
12131250
c == dialect->quotechar ||
12141251
c == '\n' ||
12151252
c == '\r' ||
1216-
PyUnicode_FindChar(
1217-
dialect->lineterminator, c, 0,
1218-
PyUnicode_GET_LENGTH(dialect->lineterminator), 1) >= 0) {
1253+
(c <= term_maxchar && in_lineterminator(c, dialect))) {
12191254
if (dialect->quoting == QUOTE_NONE)
12201255
want_escape = 1;
12211256
else {

0 commit comments

Comments
 (0)