') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); GH-112855: Speed up `pathlib.PurePath` pickling by barneygale · Pull Request #112856 · python/cpython · GitHub
Skip to content

GH-112855: Speed up pathlib.PurePath pickling - #112856

Merged
barneygale merged 8 commits into
python:mainfrom
barneygale:optimise-path-pickle
Apr 20, 2024
Merged

GH-112855: Speed up pathlib.PurePath pickling#112856
barneygale merged 8 commits into
python:mainfrom
barneygale:optimise-path-pickle

Conversation

@barneygale

@barneygalebarneygale commented Dec 7, 2023

Copy link
Copy Markdown
Contributor

The second item in the tuple returned from __reduce__() is a tuple of arguments to supply to path constructor. Previously we returned the parts tuple here, which entailed joining, parsing and normalising the path object, and produced a compact pickle representation.

With this patch, we instead return a tuple of paths that were originally given to the path constructor. This makes pickling much faster (at the expense of compactness).

It's worth noting that, in the olden times, pathlib performed this parsing/normalization up-front in every case, and so using parts for pickling was almost free. Nowadays pathlib only parses/normalises paths when it's necessary or advantageous to do so (e.g. computing a path parent, or iterating over a directory, respectively).

The second item in the tuple returned from `__reduce__()` is a tuple of
arguments to supply to path constructor. Previously we returned the `parts`
tuple here, which entailed joining, parsing and normalising the path
object, and produced a compact pickle representation.
With this patch, we instead return a tuple of paths that were originally
given to the path constructor. This makes pickling much faster (at the
expense of compactness). By also omitting to `sys.intern()` the path parts,
we slightly speed up path parsing/normalization more generally.
@barneygale

Copy link
Copy Markdown
ContributorAuthor

Makes pickling ~3x faster, depending on what you include in the measurement:

$ ./python -m timeit -s "from pathlib import PurePath""PurePath('foo').__reduce__()"
100000 loops, best of 5: 2.17 usec per loop # before
500000 loops, best of 5: 617 nsec per loop # after
$ ./python -m timeit -s "from pathlib import PurePath; p = PurePath('foo')""p.__reduce__()"
2000000 loops, best of 5: 169 nsec per loop # before
5000000 loops, best of 5: 78.1 nsec per loop # after

It's hard to measure using only public APIs, but path parsing generally is a little faster due to dropping the sys.intern(str(x)) bit. I measure about a 3% improvement from generating a string representation of PurePath('foo'):

./python -m timeit -s "from pathlib import PurePath;" "str(PurePath('foo'))"
100000 loops, best of 5: 2.98 usec per loop # before
100000 loops, best of 5: 2.88 usec per loop # after

@barneygale

barneygale commented Dec 7, 2023

Copy link
Copy Markdown
ContributorAuthor

@AlexWaygood asked whether this might make pathlib + multiprocessing faster, and indeed it does. Simple benchmark script:

frommultiprocessingimportPoolfrompathlibimportPurePathdeff(pathobj):
returnstr(pathobj)
if__name__=='__main__':
paths= [PurePath(str(i), str(j))
foriinrange(1000)
forjinrange(1000)]
withPool(5) asp:
p.map(f, paths)

Before:

$ time ./python mp.py
real 0m7.730s
user 0m13.161s
sys 0m0.869s

After:

real 0m4.165s
user 0m10.614s
sys 0m0.976s

--> ~1.85x faster in this example.

@barneygale

Copy link
Copy Markdown
ContributorAuthor

Simple compactness test:

importpathlibimportpicklepaths=pathlib.Path().glob('**/*')
print(len(pickle.dumps(tuple(paths))))

In my CPython checkout, the size increased by ~20% with this PR.

If I instead glob from Path.cwd(), which adds a repetitive /home/barney/projects/cpython prefix to every path, then the size increased by ~50% from baseline.

Seems like a good bargain to me.

@pitrou

Copy link
Copy Markdown
Member

This doesn't look like a good idea to me, especially as it may increase memory consumption when unpickling. Unless you know of a workload where this gives a benefit, I would tend to reject this PR.

@barneygale

Copy link
Copy Markdown
ContributorAuthor

Thanks @pitrou. Personally I see no reason to prioritise memory consumption over speed of processing here. As a user of pathlib, I'd expect the performance/space characteristics of serialising path objects to roughly match that of string paths. All workloads involving pickling are affected, such as the multiprocessing example in my previous comment, where the wall time is nearly halved by this PR.

@pitrou

Copy link
Copy Markdown
Member

All workloads involving pickling are affected, such as the multiprocessing example in my previous comment, where the wall time is nearly halved by this PR.

This example is not a workload, it's a completely unrealistic micro-benchmark which doesn't reflect actual usage.

To rephrase my objection:
"Unless you know of a (actual, real world) workload where this gives a benefit, I would tend to reject this PR".

Comment threadLib/pathlib/_abc.py Outdated
Comment threadLib/test/test_pathlib.py Outdated
@barneygale

Copy link
Copy Markdown
ContributorAuthor

Could you provide a workload, real-world or otherwise, that shows your concerns? Everything I try shows a speed-up.

@pitrou

Copy link
Copy Markdown
Member

I would say: glob a large filesystem tree (something like list(Path('/usr').glob('**'))) and measure memory consumption.

@barneygale

barneygale commented Dec 11, 2023

Copy link
Copy Markdown
ContributorAuthor

Mega, thank you. Running a glob across my media collection (133003 files):

barney@acorn /media/sexy $ /usr/bin/time -v ~/projects/cpython/python -c 'from pathlib import Path; list(Path.cwd().glob("**/*"))'
Maximum resident set size (kbytes): 88284 # before
Maximum resident set size (kbytes): 84828 # after

... which is a little surprising! I'll dig in.

@pitrou

Copy link
Copy Markdown
Member

(make sure you compile CPython in non-debug mode, by the way :-))

@barneygale

Copy link
Copy Markdown
ContributorAuthor

Turns out pathlib doesn't intern strings while globbing or iterating over directories, and possibly never did? The private _make_child_relpath() constructor creates a fully-parsed path, bypassing the path parsing routine (_Flavour.parse_parts() in the past, _abc.PurePathBase.parse_path() currently), and therefore skipping the calls to sys.intern().

@pitrou

Copy link
Copy Markdown
Member

Turns out pathlib doesn't intern strings while globbing or iterating over directories, and possibly never did?

IIRC it was meant to (globbing or walking directories is really the primary situation where you'd get multiple instances of the same path component), so I'm a bit surprised.

@barneygale

Copy link
Copy Markdown
ContributorAuthor

It surprises me too :o

FWIW, I've restored the interning of path parts in this PR, so the only thing it changes is __reduce__()

@barneygale

Copy link
Copy Markdown
ContributorAuthor

I plan to merge this patch within a few days, as I'm reasonably sure it will provide a yummy speedup without having much impact on memory usage, given that pathlib doesn't intern parts in most cases where related paths are generated (directory children, with_name(), etc). But I'll keep a close eye on bug reports / forums / etc to see if anyone reports increased memory/disk usage when pickling paths in 3.13.

@Hnasar

Copy link
Copy Markdown

@barneygale do you think it's worth backporting this pickling optimization to 3.12? 3x performance for a one-line change is pretty nice
(thanks for all your work on pathlib btw!!)

@AlexWaygood

Copy link
Copy Markdown
Member

@Hnasar we don't backport performance optimisations, I'm afraid, only bugfixes.

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

Labels

performancePerformance or resource usagetopic-pathlib

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@barneygale@pitrou@Hnasar@AlexWaygood