The increase in size from 32 bytes to 48 bytes when you switch from using an unsigned long long
to a PyObject*
in your cdef
class in Cython is primarily due to the addition of fields required for Python’s cyclic garbage collector (GC).
Structure Layout in C and Cython:
In C, when you define the structs directly, the size of int_mod1
and int_mod2
structs is 32 bytes. This is expected because:
unsigned long long
is 8 bytes.struct
has two unsigned long long
types, making it 16 bytes.refcount
and type
(assuming a 64-bit architecture, where pointers are 8 bytes each) adds another 16 bytes.sizeof(int_mod1) = 32 bytes
and sizeof(int_mod2) = 32 bytes
in C, which aligns with your observation.Cython Classes and Python's Memory Management:
In Cython, when you declare a cdef class
, it is a Python object that participates in Python's memory management system. Python objects have additional overhead due to:
PyObject_HEAD
, which includes reference counting and type information.Cyclic Garbage Collector (GC) Overhead:
When you change an attribute from unsigned long long
to PyObject*
, it introduces the potential for Python reference cycles because a PyObject*
could reference other Python objects, creating a cycle that the GC must handle.
To manage this, Python’s GC adds additional overhead:
PyGC_Head
is an additional structure used for objects tracked by the garbage collector. This structure adds 16 bytes on a typical 64-bit system (it contains three pointers: gc.gc_next
, gc.gc_prev
, and gc.gc_refs
).int_mod2
now contains a PyObject*
pointer (int_mod2_ctx
), which may involve Python object references and potential cycles, it is automatically placed under GC tracking.Alignment Considerations: While alignment can play a role in struct sizes, the key factor here is not alignment but the presence of additional GC fields. The structures’ sizes in pure C are controlled by the size of their members and the alignment requirements, but Cython-generated Python objects have added GC tracking, leading to the observed size increase.
Size Calculation Breakdown:
int_mod1
:PyObject_HEAD
(16 bytes) + unsigned long long val
(8 bytes) + unsigned long long mod
(8 bytes) = 32 bytes.int_mod2
:PyObject_HEAD
(16 bytes) + unsigned long long val
(8 bytes) + PyObject* ctx
(8 bytes) + GC overhead (16 bytes) = 48 bytes.The reason int_mod2
is 48 bytes instead of 32 bytes is due to the inclusion of garbage collector overhead when you change an attribute to a PyObject*
. This addition is required to properly manage objects that could participate in reference cycles, which pure C structs do not need to handle. This overhead increases the size of the Cython class, as reflected by sys.getsizeof
.
--
---
You received this message because you are subscribed to the Google Groups "cython-users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to cython-users...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/cython-users/CAHVvXxSJu2nj%2BdL28%2BQDeD9QAA_Uhmsfuj2OU6%2BZTc%2BvpcpeeQ%40mail.gmail.com.
PyGC_Head
Structure SizeThe PyGC_Head
structure is used by CPython to manage objects that are tracked by the cyclic garbage collector (GC). This structure does indeed contain three pointers, but let's understand how this fits into 16 bytes on a typical 64-bit system.
PyGC_Head
On a 64-bit system, each pointer typically takes 8 bytes. The PyGC_Head
structure is defined as follows (simplified for explanation):
ctypedef struct _gc_head { struct _gc_head *gc_next; // Pointer to the next GC-tracked object struct _gc_head *gc_prev; // Pointer to the previous GC-tracked object Py_ssize_t gc_refs; // Reference count (an integer type) } PyGC_Head;
Now, on a 64-bit system:
gc_next
is 8 bytes (pointer)gc_prev
is 8 bytes (pointer)gc_refs
is typically a Py_ssize_t
, which is also 8 bytes on a 64-bit systemSo, why does this fit into 16 bytes?
The trick lies in how CPython manages memory for small objects. The gc_refs
field does not necessarily use a full 8 bytes. Instead:
gc_refs
field, particularly when storing reference counts or special marker values (like flags for GC stages). This allows all three members to fit into 16 bytes effectively.The actual memory layout might look like this:
gc_next
(pointer).gc_prev
(pointer).gc_refs
(using the lower 4 bytes for gc_refs
and higher bytes for flags or markers).Thus, while logically there are three fields, in practice, CPython's internal structures might compact these into a smaller memory footprint, fitting all necessary data into 16 bytes on systems where pointer size is 8 bytes.
PyGC_Head
in Struct Definitions?PyGC_Head
is not directly visible in the struct
definitions of Python objects like int_mod1
or int_mod2
because:
Separation of Concerns: The garbage collector structures (PyGC_Head
) are typically managed separately from the user-defined struct in the source code. When an object is allocated in a garbage-collected pool, the GC metadata is stored "before" the actual memory used by the Python object.
Memory Layout: The PyGC_Head
is usually allocated just before the Python object itself in memory. This means that while sys.getsizeof()
gives you the size of the Python object (based on its tp_basicsize
), it does not include the GC header in its calculation.
sys.getsizeof()
and tp_basicsize
tp_basicsize
: This field in the PyTypeObject
structure represents the size of the object itself (i.e., the size of the user-defined struct
including its members but not including any GC-related overhead).
sys.getsizeof()
: This function returns the memory size of the Python object as calculated by tp_basicsize
. It does not account for additional overhead like PyGC_Head
because that memory is considered part of the garbage collector's internal management and not the object itself.
Why You Don’t See PyGC_Head
in sys.getsizeof()
Output:
sys.getsizeof()
is designed to measure the actual size of the object data structure and does not include the overhead for garbage collection metadata (PyGC_Head
). This GC metadata is a separate concern and managed outside the object's tp_basicsize
.Example Memory Layout of a GC-Tracked Object:
If we visualize the memory layout:
python[ PyGC_Head (16 bytes) | Python object (tp_basicsize) ]
PyGC_Head
(16 bytes): Contains the garbage collector management information.Python object
: Contains the user-defined fields (as described by tp_basicsize
).PyGC_Head
compacts three pointers into 16 bytes using bit-packing and efficient memory alignment techniques.sys.getsizeof()
returns the size of the Python object itself (tp_basicsize
), excluding any garbage collection overhead such as PyGC_Head
.tp_basicsize
.To view this discussion on the web visit https://groups.google.com/d/msgid/cython-users/CAHVvXxRBoLaq72Q3rejD22AC%3DAxY%3D0VwYtVwG7%3DriROSD9QHww%40mail.gmail.com.
My other question is how is it that I can't see this overhead in the struct definitions? Is it not the case that sys.getsizeof returns tp_basicsize?
Hi Oscar,
sys.getsizeof does also add in any extra "pre-header" storage. That's the GC head and also managed dict and managed weakref (although Cython doesn't use the latter two):
For reference the current definition of PyGC_Head is at
It is indeed 16 bits, although not quite in the layout that Salih says.
/* "flint/types/nmod.pyx":403 * raise ValueError("cannot coerce integers mod n with different n") * r = nmod.__new__(nmod) * r.ctx = s.ctx # <<<<<<<<<<<<<< * r.val = nmod_mul(s.val, t.val, s.ctx.mod) * return r */ __pyx_t_2 = ((PyObject *)__pyx_v_s->ctx); __Pyx_INCREF(__pyx_t_2); __Pyx_GIVEREF(__pyx_t_2); __Pyx_GOTREF((PyObject *)__pyx_v_r->ctx); __Pyx_DECREF((PyObject *)__pyx_v_r->ctx); __pyx_v_r->ctx = ((struct __pyx_obj_5flint_5types_4nmod_nmod_ctx *)__pyx_t_2); __pyx_t_2 = 0; I'm not sure what all of those macros are doing. I imagined that this operation just needs one INCREF, one DECREF and one pointer copy like: Py_INCREF(new); Py_DECREF(old); __pyx_v_r->ctx = new; Maybe some of the macros are actually no-ops and the compiler reduces it down a bit?
Yes this is right. These macros are no-ops.
Thet're used in our test-suite and can be manually enabled (by defining the C macro CYTHON_REFNANNY) in order to do some sanity checking of the reference counting. If you haven't done that deliberately then they'll just get removed by the C preprocessor.
So __PYX_GOTREF and __PYX_GIVEREF aren't worth worrying about.
PyGC_Head
in the Struct Definitions?The PyGC_Head
is not visible in the struct definitions of your Python objects because it is part of the internal memory management and garbage collection mechanisms in CPython, not the Python object itself. Here’s a more detailed explanation:
Separation of Object Data and GC Metadata: The PyGC_Head
is a separate header used by CPython's garbage collector to manage objects that can participate in reference cycles. This header is stored before the actual memory of the Python object in memory but is not part of the object’s own data structure as defined in the code.
Object Layout in Memory: In memory, an object tracked by the garbage collector is stored with the PyGC_Head
immediately before the actual object data. This means the PyGC_Head
is located in memory adjacent to the Python object, but it is not defined in the struct
of the Python object itself.
Here’s a simplified representation:
arduino[ PyGC_Head | PyObject_HEAD | User-defined struct fields... ]
In this layout:
PyGC_Head
is the GC-related metadata (not shown in the struct
).PyObject_HEAD
is a part of the Python object structure itself, visible in your struct
definitions.val
and mod
).sys.getsizeof()
Return tp_basicsize
?sys.getsizeof()
Returns tp_basicsize
: Yes, sys.getsizeof()
primarily returns the size of the object as defined by its tp_basicsize
. This includes the size of the object’s internal data but not the GC metadata (PyGC_Head
).
tp_basicsize
: This field in the PyTypeObject
struct represents the size of the Python object’s data structure. It accounts for the object's fields as declared in the object’s struct (like PyObject_HEAD
and any custom fields).
sys.getsizeof()
Behavior: When you call sys.getsizeof()
on an object, it returns the size defined by tp_basicsize
plus some extra for Python object-specific overhead, if any, but not the PyGC_Head
size. This is because sys.getsizeof()
is meant to measure the memory directly allocated for the Python object, not the additional GC headers or other overhead that CPython might manage separately.
PyGC_Head
is Not Visible in tp_basicsize
or sys.getsizeof()
:Invisible GC Overhead: The PyGC_Head
is part of Python's internal garbage collection system. It is not part of the object's tp_basicsize
because tp_basicsize
only includes the object-specific data, not any memory management or GC metadata.
Memory Management Separation: CPython separates object data from garbage collection and memory management metadata. This separation allows objects to be managed more flexibly by the GC system without affecting the size calculations for user-defined Python objects.
Consistency in sys.getsizeof()
: sys.getsizeof()
reports the size of the object from the perspective of Python code. Including GC overhead would make this function less consistent, as different objects might have different GC requirements not directly tied to their data layout.
sys.getsizeof()
reflects the size of a Python object based on its tp_basicsize
, which does not include the PyGC_Head
.PyGC_Head
is used by CPython's GC but is stored separately in memory, outside the user-visible struct definitions.sys.getsizeof()
) remain consistent and separate from GC metadata.--
---
You received this message because you are subscribed to the Google Groups "cython-users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to cython-users...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/cython-users/c6d1f924-4b28-40e3-a6db-65c2e695fcf8%40d-woods.co.uk.
Hi Salih,
Not sure why you insist on this. Da-wood's code snippet is quite conclusive that the GC head size is included in sys.getsizeof().
-- PG
To view this discussion on the web visit https://groups.google.com/d/msgid/cython-users/CA%2B7veeaW8guQ8U8e0qqSyzKSsN73wnzuKLhMssT1c8e9YXPN8g%40mail.gmail.com.
So it seems that in the .pxd file no_gc is ignored. I would prefer it to give an error or a warning or otherwise to have the intended effect.
Yeah, agree with that. It sounds like a bug/omission.
To me it seemed natural that the decorator belongs in the .pxd right where the struct fields are defined.
Maybe... it possibly doesn't matter to users of the pxd file (not completely sure about this and how it interacts with derived classes) so it might make sense for it to be in the implementation instead.
But either way, what we're doing now doesn't sound ideal.