From 58567cc18c5b048e08307b5ba18a9934a395ca42 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 28 Apr 2025 08:38:56 -0700 Subject: [PATCH] gh-132952: Speed up startup by importing _io instead of io (#132957) --- .../pycore_global_objects_fini_generated.h | 1 + Include/internal/pycore_global_strings.h | 1 + .../internal/pycore_runtime_init_generated.h | 1 + .../internal/pycore_unicodeobject_generated.h | 4 ++++ Lib/io.py | 3 --- Lib/site.py | 2 +- Lib/test/test_io.py | 19 +++++++++++++++++++ Lib/test/test_site.py | 12 ++++++++++++ ...-04-26-08-49-05.gh-issue-132952.ifvP10.rst | 4 ++++ Modules/_io/_iomodule.c | 5 +++++ Python/pylifecycle.c | 4 ++-- 11 files changed, 50 insertions(+), 6 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-04-26-08-49-05.gh-issue-132952.ifvP10.rst diff --git a/Include/internal/pycore_global_objects_fini_generated.h b/Include/internal/pycore_global_objects_fini_generated.h index 5485d0bd64f..e412db1de68 100644 --- a/Include/internal/pycore_global_objects_fini_generated.h +++ b/Include/internal/pycore_global_objects_fini_generated.h @@ -1023,6 +1023,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(intern)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(intersection)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(interval)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(io)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(is_running)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(is_struct)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(isatty)); diff --git a/Include/internal/pycore_global_strings.h b/Include/internal/pycore_global_strings.h index 3ce192511e3..2a6c2065af6 100644 --- a/Include/internal/pycore_global_strings.h +++ b/Include/internal/pycore_global_strings.h @@ -514,6 +514,7 @@ struct _Py_global_strings { STRUCT_FOR_ID(intern) STRUCT_FOR_ID(intersection) STRUCT_FOR_ID(interval) + STRUCT_FOR_ID(io) STRUCT_FOR_ID(is_running) STRUCT_FOR_ID(is_struct) STRUCT_FOR_ID(isatty) diff --git a/Include/internal/pycore_runtime_init_generated.h b/Include/internal/pycore_runtime_init_generated.h index 5c95d0fedde..2368157a4fd 100644 --- a/Include/internal/pycore_runtime_init_generated.h +++ b/Include/internal/pycore_runtime_init_generated.h @@ -1021,6 +1021,7 @@ extern "C" { INIT_ID(intern), \ INIT_ID(intersection), \ INIT_ID(interval), \ + INIT_ID(io), \ INIT_ID(is_running), \ INIT_ID(is_struct), \ INIT_ID(isatty), \ diff --git a/Include/internal/pycore_unicodeobject_generated.h b/Include/internal/pycore_unicodeobject_generated.h index a1fc9736d66..72c3346328a 100644 --- a/Include/internal/pycore_unicodeobject_generated.h +++ b/Include/internal/pycore_unicodeobject_generated.h @@ -1844,6 +1844,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(io); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(is_running); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); diff --git a/Lib/io.py b/Lib/io.py index e9fe619392e..63ffadb1d38 100644 --- a/Lib/io.py +++ b/Lib/io.py @@ -60,9 +60,6 @@ from _io import (DEFAULT_BUFFER_SIZE, BlockingIOError, UnsupportedOperation, IncrementalNewlineDecoder, text_encoding, TextIOWrapper) -# Pretend this exception was created here. -UnsupportedOperation.__module__ = "io" - # for seek() SEEK_SET = 0 SEEK_CUR = 1 diff --git a/Lib/site.py b/Lib/site.py index 9da8b6724e1..5c38b1b17d5 100644 --- a/Lib/site.py +++ b/Lib/site.py @@ -73,7 +73,7 @@ import sys import os import builtins import _sitebuiltins -import io +import _io as io import stat # Prefixes for site-packages; add additional prefixes like /usr/local here diff --git a/Lib/test/test_io.py b/Lib/test/test_io.py index ac3b6d131e7..545643aa455 100644 --- a/Lib/test/test_io.py +++ b/Lib/test/test_io.py @@ -445,6 +445,25 @@ class IOTest(unittest.TestCase): self.assertRaises(exc, fp.seek, 1, self.SEEK_CUR) self.assertRaises(exc, fp.seek, -1, self.SEEK_END) + @support.cpython_only + def test_startup_optimization(self): + # gh-132952: Test that `io` is not imported at startup and that the + # __module__ of UnsupportedOperation is set to "io". + assert_python_ok("-S", "-c", textwrap.dedent( + """ + import sys + assert "io" not in sys.modules + try: + sys.stdin.truncate() + except Exception as e: + typ = type(e) + assert typ.__module__ == "io", (typ, typ.__module__) + assert typ.__name__ == "UnsupportedOperation", (typ, typ.__name__) + else: + raise AssertionError("Expected UnsupportedOperation") + """ + )) + @unittest.skipUnless(hasattr(os, "pipe"), "requires os.pipe()") def test_optional_abilities(self): # Test for OSError when optional APIs are not supported diff --git a/Lib/test/test_site.py b/Lib/test/test_site.py index 16cf25798a7..a7e9241f44d 100644 --- a/Lib/test/test_site.py +++ b/Lib/test/test_site.py @@ -8,6 +8,7 @@ import unittest import test.support from test import support from test.support.script_helper import assert_python_ok +from test.support import import_helper from test.support import os_helper from test.support import socket_helper from test.support import captured_stderr @@ -574,6 +575,17 @@ class ImportSideEffectTests(unittest.TestCase): code = e.code self.assertEqual(code, 200, msg="Can't find " + url) + @support.cpython_only + def test_lazy_imports(self): + import_helper.ensure_lazy_imports("site", [ + "io", + "locale", + "traceback", + "atexit", + "warnings", + "textwrap", + ]) + class StartupImportTests(unittest.TestCase): diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-04-26-08-49-05.gh-issue-132952.ifvP10.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-04-26-08-49-05.gh-issue-132952.ifvP10.rst new file mode 100644 index 00000000000..2792ce35c15 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-04-26-08-49-05.gh-issue-132952.ifvP10.rst @@ -0,0 +1,4 @@ +Speed up startup with the ``-S`` argument by importing the +private ``_io`` module instead of :mod:`io`. This fixes a performance +regression introduced earlier in Python 3.14 development and restores performance +to the level of Python 3.13. diff --git a/Modules/_io/_iomodule.c b/Modules/_io/_iomodule.c index bd4d994454d..50fe5d50c91 100644 --- a/Modules/_io/_iomodule.c +++ b/Modules/_io/_iomodule.c @@ -661,6 +661,11 @@ iomodule_exec(PyObject *m) "UnsupportedOperation", PyExc_OSError, PyExc_ValueError); if (state->unsupported_operation == NULL) return -1; + if (PyObject_SetAttrString(state->unsupported_operation, + "__module__", &_Py_ID(io)) < 0) + { + return -1; + } if (PyModule_AddObjectRef(m, "UnsupportedOperation", state->unsupported_operation) < 0) { diff --git a/Python/pylifecycle.c b/Python/pylifecycle.c index 1b9832bff17..0871e147169 100644 --- a/Python/pylifecycle.c +++ b/Python/pylifecycle.c @@ -2755,7 +2755,7 @@ init_set_builtins_open(void) goto error; } - if (!(wrapper = PyImport_ImportModuleAttrString("io", "open"))) { + if (!(wrapper = PyImport_ImportModuleAttrString("_io", "open"))) { goto error; } @@ -2800,7 +2800,7 @@ init_sys_streams(PyThreadState *tstate) } #endif - if (!(iomod = PyImport_ImportModule("io"))) { + if (!(iomod = PyImport_ImportModule("_io"))) { goto error; }