From 5c04e85451481d56ae0890eec721d973eb297e31 Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Sat, 15 Aug 2026 20:29:56 +0900 Subject: [PATCH] sysconfig, sizeof: advertise a build without the lock, and report its layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #1236 moved `Py_GIL_DISABLED` to 0 and dropped the `t` from `sys.abiflags`, `ABIFLAGS`, `sys.winver` and the installed stdlib directory. Restore the four spellings and `lib/pyre3.14t`, including the cpyext include directory, `stage-stdlib.py`, the `importing.rs` search paths and the `dist-workspace.toml` comment naming the archived directory. The blackhole and residual-call halves of that commit are untouched. The object header such a build carries is four words, not two, and the `tp_basicsize`/`tp_itemsize` projection reported the two-word layout: - `cpython_type_layout`: the entries that grow by two words, plus `type` and `PyWeakReference`, which grow by three. - `cpython_type_offsets`: the `type`, `set`/`frozenset` and `memoryview` inline offsets, and the managed weakref word, which sits two words behind the instance rather than four. - `str.__sizeof__`: PyASCIIObject 7 words, PyCompactUnicodeObject 9, PyUnicodeObject 10. - `type.__sizeof__`: PyHeapTypeObject 120 words, PyTypeObject 54. - `object.__sizeof__`: the fallback for a type with no entry. - `sys.getsizeof`: `_PyType_PreHeaderSize` charges no `PyGC_Head`. Without the lock the collector keeps its bits in the object header, so a tracked instance pays nothing for the wider header while an untracked one pays the full two words. - `list.__sizeof__`: `list_sort_impl` writes -1 into `allocated` while the items are detached; clamp it rather than wrap in `size_t`. `cpython_object_is_gc` loses its only caller and keeps the flag it reads; the comments on it and on `flag_have_gc` no longer claim `getsizeof` adds a collector pre-header. `dict_set_sizeof_python314.py` derives the header width from `Py_GIL_DISABLED` rather than hard-coding it. The parity runner executes each fixture under both the CPython oracle and pyre, and those two now disagree about the build, so one set of constants cannot satisfy both. `test.support.calcobjsize` keys off the same config var. `test.test_interpreters` is already recorded SKIP in `KNOWN_SKIPS` and the baseline by #1253, whose stated reason — the package `__init__` raises `SkipTest("GIL disabled")` when `Py_GIL_DISABLED` — holds again with the advertisement restored. Values measured against CPython 3.14.6 free-threaded: 22 of the 23 types the table covers now answer the same number. `list_reverseiterator` still has no entry and reports 32 against 48; that gap predates this change. `test.test_str` `test_raiseMemError` was the visible failure; the rest of the projection is reached only by `@support.cpython_only` assertions, which pyre skips. Assisted-by: Claude --- dist-workspace.toml | 2 +- .../parity_tests/dict_set_sizeof_python314.py | 27 ++-- .../include/{pyre3.14 => pyre3.14t}/Python.h | 0 pyre/pyre-interpreter/src/eval.rs | 2 +- pyre/pyre-interpreter/src/importing.rs | 49 ++++---- pyre/pyre-interpreter/src/module/sys/vm.rs | 44 +++---- pyre/pyre-interpreter/src/typedef.rs | 117 +++++++++++------- pyre/pyre-object/src/typeobject.rs | 9 +- pyre/pyrex/tests/cpyext_smoke.rs | 2 +- scripts/stage-stdlib.py | 10 +- 10 files changed, 145 insertions(+), 117 deletions(-) rename pyre/pyre-interpreter/include/{pyre3.14 => pyre3.14t}/Python.h (100%) diff --git a/dist-workspace.toml b/dist-workspace.toml index 1cc1f94c195..c6d9d70ff7b 100644 --- a/dist-workspace.toml +++ b/dist-workspace.toml @@ -29,7 +29,7 @@ install-updater = false # and it is included beside the executable in every archive and installer. # `include` keeps each item's own name and has no per-target form, so the one # entry names the stdlib directory on every platform: `lib` holding -# `pyre3.14`, except on Windows where the stdlib is `Lib` itself and the two +# `pyre3.14t`, except on Windows where the stdlib is `Lib` itself and the two # spellings are the same path. include = ["dist-assets/lib"] github-build-setup = "../pyre-dist-build-setup.yml" diff --git a/pyre/extra_tests/parity_tests/dict_set_sizeof_python314.py b/pyre/extra_tests/parity_tests/dict_set_sizeof_python314.py index c9b4adc9308..c4c7145e141 100644 --- a/pyre/extra_tests/parity_tests/dict_set_sizeof_python314.py +++ b/pyre/extra_tests/parity_tests/dict_set_sizeof_python314.py @@ -3,19 +3,30 @@ """Python 3.14 ``__sizeof__`` surface for dict and set-like types.""" +import struct +import sysconfig + for typ in (dict, set, frozenset): assert "__sizeof__" in typ.__dict__ assert typ.__sizeof__.__text_signature__ == "($self, /)" -assert dict().__sizeof__() == 48 -assert {0: None}.__sizeof__() == 208 -assert {str(i): None for i in range(6)}.__sizeof__() == 256 -assert dict.fromkeys(range(11)).__sizeof__() == 616 +# The numbers below are the layout of a build that has a global interpreter +# lock. Without one the object header carries the thread id, flags, mutex, gc +# bits and the two refcount halves rather than one refcount, which is two words +# wider, and every container answers that much more. `test.support.calcobjsize` +# derives its struct sizes from the same config var, so the oracle and pyre can +# disagree about the build and still agree about the layout. +HEADER = 2 * struct.calcsize("P") if sysconfig.get_config_var("Py_GIL_DISABLED") else 0 + +assert dict().__sizeof__() == 48 + HEADER +assert {0: None}.__sizeof__() == 208 + HEADER +assert {str(i): None for i in range(6)}.__sizeof__() == 256 + HEADER +assert dict.fromkeys(range(11)).__sizeof__() == 616 + HEADER for typ in (set, frozenset): - assert typ().__sizeof__() == 200 - assert typ(range(4)).__sizeof__() == 200 - assert typ(range(5)).__sizeof__() == 712 - assert typ(range(19)).__sizeof__() == 2248 + assert typ().__sizeof__() == 200 + HEADER + assert typ(range(4)).__sizeof__() == 200 + HEADER + assert typ(range(5)).__sizeof__() == 712 + HEADER + assert typ(range(19)).__sizeof__() == 2248 + HEADER print("OK") diff --git a/pyre/pyre-interpreter/include/pyre3.14/Python.h b/pyre/pyre-interpreter/include/pyre3.14t/Python.h similarity index 100% rename from pyre/pyre-interpreter/include/pyre3.14/Python.h rename to pyre/pyre-interpreter/include/pyre3.14t/Python.h diff --git a/pyre/pyre-interpreter/src/eval.rs b/pyre/pyre-interpreter/src/eval.rs index 569f3625018..5e15cc70311 100644 --- a/pyre/pyre-interpreter/src/eval.rs +++ b/pyre/pyre-interpreter/src/eval.rs @@ -8420,7 +8420,7 @@ result = ( 24, 24, 24, 24] and [allocation(item) for item in sources] == [6, 6, 6, 8, 8, 8, 8, 8, 8] and allocation(list(HintTwenty())) == 8 - and sort_seen == [(0, 32), (0, 32), (0, 32)] + and sort_seen == [(0, 56), (0, 56), (0, 56)] and sorted_value == [1, 2, 3] and allocation(sorted_value) == 4 and noop_sorted == [1, 2, 3] diff --git a/pyre/pyre-interpreter/src/importing.rs b/pyre/pyre-interpreter/src/importing.rs index 195620eeee1..9777da34f18 100644 --- a/pyre/pyre-interpreter/src/importing.rs +++ b/pyre/pyre-interpreter/src/importing.rs @@ -849,9 +849,8 @@ fn init_string_module(ns: PyObjectRef) { /// `sysconfig._init_non_posix` SUBSCRIPTS `Py_GIL_DISABLED` and `Py_DEBUG` to /// spell `ABIFLAGS`, so on Windows a missing key is a `KeyError` out of the /// first `get_config_var` call rather than the `None` the `.get()` readers -/// take. Pyre runs its mutators under a global interpreter lock -/// (`majit-gc/src/rgil.rs`, the `thread_gil.c` port), so `Py_GIL_DISABLED` is -/// 0 and the derived `ABIFLAGS` is empty; `Py_DEBUG` is 0. +/// take. Pyre runs its mutators without a global interpreter lock, so +/// `Py_GIL_DISABLED` is 1 and the derived `ABIFLAGS` is `t`; `Py_DEBUG` is 0. /// /// `EXT_SUFFIX` and `SOABI` name pyre's own cpyext ABI, never CPython's ABI /// tag. They are build metadata rather than a claim that an extension loader @@ -869,7 +868,7 @@ fn init_sysconfig_stub(ns: PyObjectRef) { let vars = pyre_object::w_dict_new(); let so_ext = extension_abi_suffix(); unsafe { - for (name, value) in [("Py_DEBUG", 0), ("Py_GIL_DISABLED", 0)] { + for (name, value) in [("Py_DEBUG", 0), ("Py_GIL_DISABLED", 1)] { pyre_object::w_dict_store( vars, pyre_object::w_str_new(name), @@ -1087,14 +1086,12 @@ fn init_sysconfigdata(ns: PyObjectRef) { )); let vars = pyre_object::w_dict_new(); - // Empty, matching the `Py_GIL_DISABLED` of 0 that `_init_non_posix` - // derives the non-posix spelling from, and matching the lower-case - // `abiflags` that forms the include and site-packages directory names — - // a separate variable read from `sys.abiflags` (`sysconfig:545`). The - // release tree has no `t` suffix on its stdlib directory for - // `site.py:409` to find, and `sysconfig`'s `abi_thread` must name the - // same directory `site.py` does. - store_str(vars, "ABIFLAGS", ""); + // `_init_non_posix` derives the same `t` from `Py_GIL_DISABLED` below. + // The lower-case `abiflags` that forms the include and site-packages + // directory names is a separate variable, read from `sys.abiflags` + // (`sysconfig:545`), and stays empty: the release tree has no `t` suffix + // on its stdlib directory for `site.py:409` to find. + store_str(vars, "ABIFLAGS", "t"); store_str(vars, "SOABI", &soabi); // Deprecated in Python 3, kept for backward compatibility. store_str(vars, "SO", &so_ext); @@ -1114,13 +1111,11 @@ fn init_sysconfigdata(ns: PyObjectRef) { store_str(vars, "EXE", ""); store_str(vars, "VERSION", "3.14"); store_str(vars, "LDVERSION", "3.14"); - // cpyext never uses Py_DEBUG. Pyre runs its mutators under a global - // interpreter lock (`majit-gc/src/rgil.rs` ports `thread_gil.c`), so it is - // not a free-threaded build: `test.support.Py_GIL_DISABLED` reads this key - // and skips whole suites on it. Py_ENABLE_SHARED at 1 would add a python - // shared object to link lines as `-lpython3.x`. + // cpyext never uses Py_DEBUG. Pyre runs its mutators without a global + // interpreter lock. Py_ENABLE_SHARED at 1 would add a python shared + // object to link lines as `-lpython3.x`. store_int(vars, "Py_DEBUG", 0); - store_int(vars, "Py_GIL_DISABLED", 0); + store_int(vars, "Py_GIL_DISABLED", 1); store_int(vars, "Py_ENABLE_SHARED", 0); // Pyre currently has neither a CPython-compatible C API nor a separately // linkable runtime library. Keep the build ABI metadata above for wheel @@ -1781,7 +1776,7 @@ pub(crate) struct StartupPathConfig { pub base_prefix: PathBuf, /// Bootstrap search entries in PyPy's order. A source checkout keeps /// `lib_pypy` before `lib-python/3`; an installed tree has one merged - /// `lib/pyre3.14` entry. + /// `lib/pyre3.14t` entry. pub stdlib_paths: Vec, /// The language stdlib root (the entry containing `os.py` / `site.py`). /// This is exposed as `sys._stdlib_dir` and is deliberately distinct from @@ -1913,7 +1908,7 @@ fn find_invoked_executable() -> PathBuf { } /// Recognize both PyPy-style source trees (`lib-python/3`) and the packaged -/// Pyre layout (`lib/pyre3.14`). The latter is the installation shape pip and +/// Pyre layout (`lib/pyre3.14t`). The latter is the installation shape pip and /// venv will consume; accepting the former keeps the repository executable as /// the untranslated/development oracle, matching PyPy's two /// `compute_stdlib_path_*` arms. @@ -1950,7 +1945,7 @@ fn stdlib_at_prefix(prefix: &Path) -> Option<(Vec, Option)> { // `compute_stdlib_path`: release packaging merges the two source trees // into one implementation-version directory. for packaged in [ - prefix.join("lib").join("pyre3.14"), + prefix.join("lib").join("pyre3.14t"), // cargo-dist's Homebrew formula installs non-binary archive contents // into `pkgshare` (`/share/pyrex`). This is still a single // interpreter-owned prefix, analogous to PyPy's macOS bundle search @@ -1959,7 +1954,7 @@ fn stdlib_at_prefix(prefix: &Path) -> Option<(Vec, Option)> { .join("share") .join("pyrex") .join("lib") - .join("pyre3.14"), + .join("pyre3.14t"), ] { if packaged.join("site.py").is_file() { let mut paths = vec![packaged.clone()]; @@ -2081,7 +2076,7 @@ fn prefix_from_stdlib(stdlib: &Path) -> PathBuf { .is_some_and(|name| name == "lib") { // cargo-dist Homebrew data lives at - // `/share/pyrex/lib/pyre3.14`, while the executable still lives + // `/share/pyrex/lib/pyre3.14t`, while the executable still lives // at `/bin/pyre`. An explicit PYRE_STDLIB pointing at that data // must recover the keg, not claim `/share/pyrex` as sys.prefix. if let Some(pyre_share) = parent.and_then(Path::parent) @@ -5299,12 +5294,12 @@ mod tests { PathBuf::from("/src/pyre") ); assert_eq!( - prefix_from_stdlib(Path::new("/opt/pyre/lib/pyre3.14")), + prefix_from_stdlib(Path::new("/opt/pyre/lib/pyre3.14t")), PathBuf::from("/opt/pyre") ); assert_eq!( prefix_from_stdlib(Path::new( - "/opt/homebrew/Cellar/pyrex/0.0.2/share/pyrex/lib/pyre3.14" + "/opt/homebrew/Cellar/pyrex/0.0.2/share/pyrex/lib/pyre3.14t" )), PathBuf::from("/opt/homebrew/Cellar/pyrex/0.0.2") ); @@ -5322,7 +5317,7 @@ mod tests { #[test] fn stdlib_layout_prefers_the_versioned_zip_without_claiming_a_directory() { let tree = tempfile::tempdir().unwrap(); - let packaged = tree.path().join("lib/pyre3.14"); + let packaged = tree.path().join("lib/pyre3.14t"); std::fs::create_dir_all(&packaged).unwrap(); std::fs::write(packaged.join("site.py"), "").unwrap(); let zip = tree.path().join("python314.zip"); @@ -5343,7 +5338,7 @@ mod tests { let tree = tempfile::tempdir().unwrap(); let base = tree.path().join("base"); let base_executable = base.join("bin/pyre"); - let base_stdlib = base.join("lib/pyre3.14"); + let base_stdlib = base.join("lib/pyre3.14t"); std::fs::create_dir_all(base_executable.parent().unwrap()).unwrap(); std::fs::create_dir_all(&base_stdlib).unwrap(); std::fs::write(&base_executable, "").unwrap(); diff --git a/pyre/pyre-interpreter/src/module/sys/vm.rs b/pyre/pyre-interpreter/src/module/sys/vm.rs index 644bd85afe3..c485582ba23 100644 --- a/pyre/pyre-interpreter/src/module/sys/vm.rs +++ b/pyre/pyre-interpreter/src/module/sys/vm.rs @@ -71,24 +71,21 @@ fn get_sizeof(w_obj: PyObjectRef) -> crate::PyResult { )); } - // `_PyType_PreHeaderSize(Py_TYPE(o))` adds its two components - // independently: a two-word GC header for tracked objects, plus a two-word - // managed dict/weakref prefix where the instance type requests it. A - // tracked builtin such as list has only the first; a normal heap instance - // has both; an untracked heap-derived value may have only the second. + // `_PyType_PreHeaderSize(Py_TYPE(o))` — the two-word managed dict/weakref + // prefix an instance type requests. Its other term, a `PyGC_Head` ahead of + // every tracked object, is compiled out of a build without a global + // interpreter lock: the collector keeps its bits in the object header + // there. That is why the four-word header those builds carry costs a + // tracked type nothing over the two-word one, while an untracked leaf pays + // the whole two words. // - // Both terms are read off the type. Asking instead which heap the instance + // The term is read off the type. Asking instead which heap the instance // landed in would make the answer depend on the allocation that produced // it: a `str` folded into a code constant sits outside the collector's // ranges and one built at run time does not, so the same value reported two // different sizes. let word = std::mem::size_of::() as u64; - let gc_header = if crate::typedef::cpython_object_is_gc(current()) { - 2 * word - } else { - 0 - }; - let managed_prefix = crate::typedef::r#type(current()).map_or(0, |tp| unsafe { + let pre_header = crate::typedef::r#type(current()).map_or(0, |tp| unsafe { if pyre_object::w_type_is_heaptype(tp.as_ptr()) && (pyre_object::w_type_get_hasdict(tp.as_ptr()) || pyre_object::w_type_get_weakrefable(tp.as_ptr())) @@ -98,7 +95,6 @@ fn get_sizeof(w_obj: PyObjectRef) -> crate::PyResult { 0 } }); - let pre_header = gc_header + managed_prefix; let total = (size as u64) .checked_add(pre_header) .expect("Py_ssize_t plus the fixed pre-header fits in size_t"); @@ -1238,11 +1234,11 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { ); // sys.winver — the "major.minor" tag Windows uses for the per-user site // directory and the PythonCore registry keys. site.getusersitepackages - // reads it to build USER_SITE. It carries no `t`: that suffix is how - // Windows spells what `sys.abiflags` spells elsewhere, and pyre holds a - // global interpreter lock. + // reads it to build USER_SITE. A build without a global interpreter lock + // carries the `t` here, which is how Windows spells what `sys.abiflags` + // spells elsewhere. #[cfg(windows)] - module_ns_store(ns, "winver", w_str_new("3.14")); + module_ns_store(ns, "winver", w_str_new("3.14t")); // sys.dllhandle — the handle of the DLL exporting the Python C API, // published beside `winver` because both come from the same `MS_COREDLL` // block. `ctypes/__init__.py:562` builds `pythonapi` out of it with no @@ -2047,15 +2043,11 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { Err(unsafe { crate::PyError::from_exc_object(exc) }) }), ); - // sys.abiflags — empty, because pyre runs its mutators under a global - // interpreter lock (`majit-gc/src/rgil.rs`, the `thread_gil.c` port), so - // the `t` that spells a free-threaded ABI does not describe this build. - // `site.py:409` reads the flag to name the site-packages directory and - // `sysconfig` derives `abi_thread` from `Py_GIL_DISABLED` for the same - // name, so the two must agree. The attribute is absent on Windows, where - // the flag is spelled in `sys.winver`; every reader guards with `hasattr`, - // so an empty string answers the same. - module_ns_store(ns, "abiflags", w_str_new("")); + // sys.abiflags — `t` for a build without a global interpreter lock. The + // attribute is absent on Windows, where the flag is spelled in + // `sys.winver` and neither the `nt` scheme nor `site._get_path` reads it; + // every reader guards with `hasattr`, so an empty string answers the same. + module_ns_store(ns, "abiflags", w_str_new(if cfg!(windows) { "" } else { "t" })); // sys.argv — pick up pending argv from set_sys_argv if available. let pending = crate::importing::take_pending_sys_argv(); let argv = if pending.is_null() { diff --git a/pyre/pyre-interpreter/src/typedef.rs b/pyre/pyre-interpreter/src/typedef.rs index 32bb6904d38..c432b2d1796 100644 --- a/pyre/pyre-interpreter/src/typedef.rs +++ b/pyre/pyre-interpreter/src/typedef.rs @@ -4943,10 +4943,14 @@ fn list_descr_sizeof(args: &[PyObjectRef]) -> Result())); + // `list_sort_impl` temporarily writes -1 while the items are detached, and + // that sentinel is what the modification check reads back. It is not part + // of the size a build without a global interpreter lock reports: a list + // observed mid-sort answers its `tp_basicsize` and nothing more, so the + // detached state contributes no slots rather than wrapping in `size_t`. + let slots = allocated.max(0) as usize; + let size = + (basicsize as usize).wrapping_add(slots.wrapping_mul(std::mem::size_of::())); Ok(w_int_new(size as i64)) } @@ -5511,15 +5515,19 @@ fn init_str_type(ns: PyObjectRef) { 4 }; let word = std::mem::size_of::(); - // unicodeobject.c:unicode_sizeof_impl. Exact ASCII uses - // PyASCIIObject (5 words), exact non-ASCII uses - // PyCompactUnicodeObject (7 words), and Unicode - // subclasses use the two-block PyUnicodeObject (8 words). + // unicodeobject.c:unicode_sizeof_impl, over the four-word + // object header a build without a global interpreter lock + // carries (thread id, flags, mutex, gc bits, local and + // shared refcounts, type) rather than the two-word one. + // Exact ASCII then uses PyASCIIObject (7 words), exact + // non-ASCII uses PyCompactUnicodeObject (9 words), and + // Unicode subclasses the two-block PyUnicodeObject + // (10 words). let base = if pyre_object::pyobject::is_exact_type(args[0], &pyre_object::STR_TYPE) { - if maxchar < 0x80 { 5 * word } else { 7 * word } + if maxchar < 0x80 { 7 * word } else { 9 * word } } else { - 8 * word + 10 * word }; let size = base.checked_add((len + 1).checked_mul(kind).ok_or_else(|| { @@ -10702,10 +10710,16 @@ fn make_getset_property_full( /// `tp_is_gc` where the type installs one. /// /// This is a property of the type, not of the object's current collector -/// state: `()` is untracked yet `sys.getsizeof` still charges it a -/// `PyGC_Head`, because `tuple` carries the flag. Reading pyre's own GC -/// ownership instead answers a different question — pyre's nursery owns +/// state: `()` is untracked yet `tuple` carries the flag. Reading pyre's own +/// GC ownership instead answers a different question — pyre's nursery owns /// `object()` and `b""`, which `object` and `bytes` never declare. +/// +/// Nothing consumes it at present. `sys.getsizeof` used to, for the +/// `PyGC_Head` that `_PyType_PreHeaderSize` charges a tracked instance, but a +/// build without a global interpreter lock keeps the collector's bits in the +/// object header and so charges none. The flag itself remains the answer +/// `type.__flags__` owes for `Py_TPFLAGS_HAVE_GC`, which it does not yet +/// report. pub(crate) fn cpython_object_is_gc(w_obj: PyObjectRef) -> bool { let Some(tp) = r#type(w_obj) else { return false; @@ -10772,6 +10786,13 @@ fn cpython_type_has_gc_flag(w_type: PyObjectRef) -> bool { /// Logical CPython 3.14 `tp_basicsize` / `tp_itemsize` values ported so far. /// These belong to the type object, not to its Python namespace: CPython's /// `type_members` exposes both through read-only data descriptors. +/// +/// The sizes are the ones a build without a global interpreter lock reports, +/// which is what `sys.abiflags` and the `Py_GIL_DISABLED` config var advertise +/// here. Its object header is four words rather than two, so most of these run +/// two words above their counterpart in a build that keeps the lock; `type` +/// and `PyWeakReference` grow by three, carrying a field the lock makes +/// unnecessary. pub(crate) fn cpython_type_layout(w_type: PyObjectRef) -> Option<(i64, i64)> { if w_type.is_null() || !unsafe { pyre_object::is_type(w_type) } { return None; @@ -10780,59 +10801,62 @@ pub(crate) fn cpython_type_layout(w_type: PyObjectRef) -> Option<(i64, i64)> { let layout = unsafe { pyre_object::w_type_get_layout(w_type) }; let is = |candidate: *const PyType| std::ptr::eq(layout, candidate); let (base, item) = if is(&pyre_object::INSTANCE_TYPE) { - (2 * word, 0) + (4 * word, 0) } else if is(&pyre_object::TYPE_TYPE) { - (117 * word, 5 * word) + // `PyHeapTypeObject`, which keeps a unique-id word here that a build + // holding the lock has no use for. + (120 * word, 5 * word) } else if is(&pyre_object::INT_TYPE) || is(&pyre_object::LONG_TYPE) || is(&pyre_object::BOOL_TYPE) { - (3 * word, 4) + (5 * word, 4) } else if is(&pyre_object::FLOAT_TYPE) { - (3 * word, 0) + (5 * word, 0) } else if is(&pyre_object::COMPLEX_TYPE) { - (4 * word, 0) + (6 * word, 0) } else if is(&pyre_object::STR_TYPE) { - (8 * word, 0) + (10 * word, 0) } else if is(&pyre_object::bytesobject::BYTES_TYPE) { - (4 * word + 1, 1) + (6 * word + 1, 1) } else if is(&pyre_object::bytearrayobject::BYTEARRAY_TYPE) { - (7 * word, 0) + (9 * word, 0) } else if is(&pyre_object::LIST_TYPE) { - (5 * word, 0) + (7 * word, 0) } else if is(&pyre_object::TUPLE_TYPE) { - (4 * word, word) + (6 * word, word) } else if is(&pyre_object::DICT_TYPE) { - (6 * word, 0) + (8 * word, 0) } else if is(&pyre_object::setobject::SET_TYPE) || is(&pyre_object::setobject::FROZENSET_TYPE) { - (25 * word, 0) + (27 * word, 0) } else if is(&pyre_object::functional::RANGE_TYPE) { - (6 * word, 0) + (8 * word, 0) } else if is(&pyre_object::sliceobject::SLICE_TYPE) { - (5 * word, 0) + (7 * word, 0) } else if is(&pyre_object::memoryview::MEMORYVIEW_TYPE) { - (18 * word, word) + (20 * word, word) } else if is(&pyre_object::functional::MAP_TYPE) { - (5 * word, 0) + (7 * word, 0) } else if is(&pyre_object::functional::FILTER_TYPE) || is(&pyre_object::functional::REVERSED_TYPE) { - (4 * word, 0) - } else if is(&pyre_object::functional::ZIP_TYPE) { (6 * word, 0) + } else if is(&pyre_object::functional::ZIP_TYPE) { + (8 * word, 0) } else if is(&pyre_object::functional::ENUMERATE_TYPE) { - (7 * word, 0) + (9 * word, 0) } else if is(&pyre_object::weakref::WEAKREF_LAYOUT_TYPE) { // CPython 3.14 `PyWeakReference`: object header, doubly-linked - // weakref list, callback, hash/cache word and vectorcall slot. + // weakref list, callback, hash/cache word and vectorcall slot, plus + // the word that serialises the referent without the lock. // PyPy likewise gives W_WeakrefBase/W_Weakref their own typedef; // subclasses append their declared slots to this prefix. - (8 * word, 0) + (11 * word, 0) } else { // CPython's ordinary fixed-size heap instance begins with // PyObject_HEAD; user slots are appended below just like the // specialized builtin prefixes above. - (2 * word, 0) + (4 * word, 0) }; // PyPy typeobject.py:103-129 keeps the total slot count on Layout, whose // typedef identifies the fixed builtin prefix. CPython appends one pointer @@ -10867,23 +10891,26 @@ fn cpython_type_offsets(w_type: PyObjectRef) -> Option<(i64, i64)> { let layout = unsafe { pyre_object::w_type_get_layout(w_type) }; let is = |candidate: *const PyType| std::ptr::eq(layout, candidate); let (mut dict, mut weakref) = if is(&pyre_object::TYPE_TYPE) { - (33 * word, 46 * word) + (35 * word, 48 * word) } else if is(&pyre_object::setobject::SET_TYPE) || is(&pyre_object::setobject::FROZENSET_TYPE) { - (0, 24 * word) + (0, 26 * word) } else if is(&pyre_object::memoryview::MEMORYVIEW_TYPE) { - (0, 17 * word) + (0, 19 * word) } else { (0, 0) }; // Python 3.14 managed dict/weakref storage lives in the negative // pre-header. Preserve a builtin's positive inline offset when it already // owns the slot; otherwise heap types use the managed sentinel/offset. + // Without the lock that pre-header is the managed dict pair alone — the + // collector keeps its bits in the object header instead of a `PyGC_Head` + // ahead of it — so the weakref word sits two words back, not four. if unsafe { pyre_object::w_type_is_heaptype(w_type) } { if dict == 0 && unsafe { pyre_object::w_type_get_hasdict(w_type) } { dict = -1; } if weakref == 0 && unsafe { pyre_object::w_type_get_weakrefable(w_type) } { - weakref = -4 * word; + weakref = -2 * word; } } Some((dict, weakref)) @@ -10938,15 +10965,17 @@ fn init_type_type(ns: PyObjectRef) { let size = if pyre_object::w_type_is_heaptype(args[0]) { // CPython 3.14 typeobject.c:type___sizeof___impl: // PyHeapTypeObject plus the cached-keys table carried - // by a managed instance dictionary. - 117 * word + // by a managed instance dictionary. The struct sizes + // are the ones a build without a global interpreter + // lock reports; the keys table is the same either way. + 120 * word + if pyre_object::w_type_get_hasdict(args[0]) { 96 * word } else { 0 } } else { - 52 * word + 54 * word }; Ok(w_int_new(size)) }, @@ -19918,7 +19947,7 @@ fn init_object_type(ns: PyObjectRef) { .expect("every Python object has a type") .as_ptr(); let (basicsize, itemsize) = cpython_type_layout(w_type) - .unwrap_or((2 * std::mem::size_of::() as i64, 0)); + .unwrap_or((4 * std::mem::size_of::() as i64, 0)); let nitems = if itemsize == 0 { 0 } else if unsafe { @@ -30286,7 +30315,7 @@ mod tests { crate::module::_weakref::interp__weakref::proxy_type(), crate::module::_weakref::interp__weakref::callable_proxy_type(), ] { - assert_eq!(super::cpython_type_layout(w_type), Some((8 * word, 0))); + assert_eq!(super::cpython_type_layout(w_type), Some((11 * word, 0))); } } diff --git a/pyre/pyre-object/src/typeobject.rs b/pyre/pyre-object/src/typeobject.rs index 0bde55b2321..d0c67bfd342 100644 --- a/pyre/pyre-object/src/typeobject.rs +++ b/pyre/pyre-object/src/typeobject.rs @@ -240,9 +240,12 @@ pub struct W_TypeObject { /// `ModuleDictStrategy.version?`, the tree's other `?` declaration, the way /// upstream's one `QuasiImmut` class serves every quasi-immutable field. pub quasi_immut_watchers: crate::quasiimmut::QuasiImmutField, - /// `Py_TPFLAGS_HAVE_GC` (`1 << 14`) — whether instances of this type carry - /// the collector's two-word pre-header, which is what - /// `_PyType_PreHeaderSize` charges and `sys.getsizeof` therefore adds. + /// `Py_TPFLAGS_HAVE_GC` (`1 << 14`) — whether instances of this type join + /// the collector's traversal. A build that keeps the global interpreter + /// lock gives them a two-word `PyGC_Head` for it, which is what + /// `_PyType_PreHeaderSize` charges; one without the lock keeps the same + /// bits in the object header and charges nothing, so the flag records the + /// type property alone. /// /// A property of the type, not of the heap an instance landed in: the same /// `str` value must report one size whether it was folded into a code diff --git a/pyre/pyrex/tests/cpyext_smoke.rs b/pyre/pyrex/tests/cpyext_smoke.rs index 23b18342c9a..a443ba6bbb7 100644 --- a/pyre/pyrex/tests/cpyext_smoke.rs +++ b/pyre/pyrex/tests/cpyext_smoke.rs @@ -63,7 +63,7 @@ fn imports_single_phase_module_created_through_c_api() { let init_marker = out_dir.join("initialized"); let unload_marker = out_dir.join("unloaded"); let source = root.join("pyre/pyrex/tests/fixtures/cpyext_smoke.c"); - let include = root.join("pyre/pyre-interpreter/include/pyre3.14"); + let include = root.join("pyre/pyre-interpreter/include/pyre3.14t"); let mut cc = Command::new(std::env::var_os("CC").unwrap_or_else(|| "cc".into())); cc.arg(&source) .arg("-I") diff --git a/scripts/stage-stdlib.py b/scripts/stage-stdlib.py index 2b6e3d47e5f..77aba3b2208 100644 --- a/scripts/stage-stdlib.py +++ b/scripts/stage-stdlib.py @@ -30,12 +30,10 @@ ROOT = Path(__file__).resolve().parent.parent -# package.py:221 `IMPLEMENTATION = 'pypy{}'.format(python_ver)`. No trailing -# `t`: that is the free-threaded ABI flag, `sysconfig` derives `abi_thread` -# from `Py_GIL_DISABLED` and the posix schemes put it in the directory name, -# and pyre runs its mutators under a global interpreter lock -# (`majit-gc/src/rgil.rs`). -IMPLEMENTATION = "pyre3.14" +# package.py:221 `IMPLEMENTATION = 'pypy{}'.format(python_ver)`. The trailing +# `t` is the free-threaded ABI flag: `sysconfig` derives `abi_thread` from +# `Py_GIL_DISABLED` and the posix schemes put it in the directory name. +IMPLEMENTATION = "pyre3.14t" def ignored(_directory: str, names: list[str]) -> set[str]: