Skip to content

Speed up message sorting in write_mo() and sorted write_po() - #1438

Open
DicksonWu654 wants to merge 1 commit into
python-babel:masterfrom
DicksonWu654:perf/message-sort
Open

DicksonWu654 wants to merge 1 commit into
python-babel:masterfrom
DicksonWu654:perf/message-sort

Conversation

@DicksonWu654

Copy link
Copy Markdown

write_mo() always sorts the catalog's messages, and write_po(sort_output=True) does too. list.sort() calls Message.__lt__, which goes through __cmp__ and rebuilds both (id, context) tuples (plus a nested helper function) for every comparison, so a catalog of n messages builds about 2·n·log2(n) tuples. On real catalogs the sort is a large part of pybabel compile.

This adds a small private helper, _sort_messages_by_id(), used by write_mo() and pofile._sort_messages(). For plain Message objects whose id (or first plural id) and context are str/None, it builds the same (id, context or '') key that __cmp__ compares, once per message, and does a key sort. Python's sort is stable either way, so ties keep their order and the output is the same. If any message is a Message subclass or has an unusual id/context, it falls back to messages.sort(), so custom comparison methods and the existing errors are unchanged.

Benchmarks

Real catalogs: Sphinx (ar, ca, fi, hu, sv, zh_TW; 869 messages each) and Django 5.2.8 (de, ja; 348 messages each), all eight per sample. Each sample was a separate process pinned to one core on Linux. master and this branch ran in alternating order (AB, BA, ...); the table gives the median per-pair ratio over 8 pairs on 3.12 and 6 on 3.15 and 3.9.

Workload Python master this PR speedup (range)
read_po + write_mo (what pybabel compile does) 3.12.12 70.8 ms 61.5 ms 1.15x (1.12–1.21x)
3.15.2 69.0 ms 59.8 ms 1.14x (1.11–1.21x)
3.9.25 107.6 ms 92.7 ms 1.16x (1.11–1.23x)
write_po(..., sort_output=True) 3.12.12 104.7 ms 86.2 ms 1.22x (1.20–1.25x)
3.15.2 103.5 ms 87.8 ms 1.19x (1.15–1.31x)
3.9.25 150.8 ms 125.6 ms 1.20x (1.16–1.21x)

write_mo alone (catalogs already parsed) goes from 16.0 ms to 5.0 ms on 3.12 (3.2x, 6 pairs). The default write_po() (no sorting) doesn't change.

Checks

  • I compared master and this branch in separate processes: write_mo (with and without use_fuzzy) and write_po (sorted by message, by file and unsorted) for the 8 real catalogs and 6 generated catalogs with plurals, contexts (None, '', mixed case, non-ASCII) and duplicate ids, plus subclass and invalid-id cases (empty plural tuple, None id, non-string context). Output bytes, and exception types and messages, were identical on Python 3.9, 3.12 and 3.15.
  • New tests in tests/messages/test_catalog.py check the order against sorted() (plurals, contexts, ties) and that a subclass's __cmp__ is still used.
  • The full test suite passes on Python 3.9, 3.12 and 3.15, and pre-commit run --all-files passes.

Related: #1374 (open) adds a second sort of the MO offset table by encoded key further down in write_mo(). It doesn't touch the message sort, so the two changes are independent and don't conflict.

🤖 Generated with Claude Code

write_mo() always sorts the catalog, and write_po(sort_output=True) does
too. list.sort() calls Message.__lt__, which goes through __cmp__ and builds
both (id, context) tuples again for every comparison. For plain Message
objects with string keys, compute each key once and sort by key; the order,
including ties, is the same. Subclasses and unusual ids keep using the
comparison methods.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@akx akx added the AI Assisted label Nov 9, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants