Conversation
Replace soft deprecated PyBytes_FromStringAndSize() and _PyBytes_Resize() with PyBytesWriter.
No PyBytesWriter is needed.
serhiy-storchaka
left a comment
There was a problem hiding this comment.
I do not think the current code is broken.
I didn't say that the current code is broken. The PR only just avoids the soft deprecated PyBytes_FromStringAndSize() function. I reworked the error handling. @serhiy-storchaka: Please review the updated PR. |
|
I wrote a script to test manually this PR by injecting import marshal
import io
import _testcapi
obj = b'x' * (1024 * 1024)
file = io.BytesIO()
for i in range(10):
try:
try:
_testcapi.set_nomemory(i)
res = marshal.dump(obj, file)
finally:
_testcapi.remove_mem_hooks()
except Exception as exc:
print(f"marshal.dump failed: {exc!r}")
else:
print(f"{res=}")Before, the code failed with an assertion error. With my latest change, the code works is all cases (always raise MemoryError as expected). |
|
Ah, I noticed that the PyMarshal C API is not tested by test_capi currently. So I wrote PR gh-156890 to add tests. |
|
I extracted the TYPE_STRING change: it does in fact fix an issue, using |
|
I do not think we need this change. It looks to me like a code churn which makes the code more complicated. |
|
UPDATE: Oh sorry, at my first attempt, I ran benchmarks on a debug build! I replaced results with a benchmark on a release build. I ran a quick benchmark on import pyperf
import marshal
def noop_func():
pass
runner = pyperf.Runner()
for obj in (b'abc', True, 123):
runner.bench_func(f'dumps {obj!r}', marshal.dumps, obj)
runner.bench_func('dumps code object', marshal.dumps, noop_func.__code__)
runner.bench_func('dumps list(range(20))', marshal.dumps, list(range(20)))
runner.bench_func("dumps '\u20ac' * 10", marshal.dumps, '\u20ac' * 10)
runner.bench_func("dumps 'long line '*1000", marshal.dumps, 'long line '*1000)Results:
|
|
I ran a second benchmark building code objects of the stdlib top 10 largest files: Results:
Benchmark hidden because not significant (2): dumps tarfile.py, dumps _pydecimal.py |
|
Aha, so using marshal.c overallocation ( First benchmark on simple small objects:
Second benchmark on large objects from stdlib modules:
|
|
I'm surprised that this change makes marshal.dumps() faster. I expected same performance or slower. So I reran the benchmark with CPU isolation. It's still faster on all benchmarks, except of First benchmark on simple small objects:
Second benchmark on large objects from stdlib modules:
These results are on the latest version of this PR, where the latest commit disables PyBytesWriter overallocation. |
|
marshal.dumps(True) is a single byte (
I compared the PyBytes calls before/after on Before:
After:
The only difference is that using PyBytesWriter, the bytes object is created with 2096 bytes, whereas it's created with 50 bytes currently. I checked the PyBytes calls before/after on subprocess.py (1.08x faster). To serialize Before:
After:
|
|
I tried to reproduce the benchmark: release build of main vs main + this PR (07d4266), pinned to one core, min of 15 repeats, 3 interleaved rounds. The only measurable win is for tiny outputs like So the performance argument does not hold, and the code is not simpler. Other notes, in case you want to pursue this anyway:
I still think that this change is not needed. |
Replace soft deprecated PyBytes_FromStringAndSize() and _PyBytes_Resize() with PyBytesWriter.