Skip to content

Free a hidden class and stop it copying a dispatch table - #416

Draft
DTW-Thalion wants to merge 3 commits into
gnustep:masterfrom
DTW-Thalion:perf/hidden-class-shared-dtable
Draft

Free a hidden class and stop it copying a dispatch table#416
DTW-Thalion wants to merge 3 commits into
gnustep:masterfrom
DTW-Thalion:perf/hidden-class-shared-dtable

Conversation

@DTW-Thalion

@DTW-Thalion DTW-Thalion commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

A hidden class is made with uninstalled_dtable and keeps it until the object is next messaged. A method is recorded in the class only as it is installed into a dtable, so the class_addMethod that puts .cxx_destruct on a new hidden class never reaches the field. deallocHiddenClass never runs, and the class, its reference list and every association the object holds are never freed.

The field is recorded when the class is made, and .cxx_destruct is no longer added as a method, which keeps it out of a shared table.

The class holds no methods of its own, so it answers sends from its superclass's table rather than copying one it would never change. It gets its own when a method is added through object_addMethod_np or object_replaceMethod_np, and does not free a table it does not own.

The reference list drops from ten slots to four. WinObjC's key value observing puts one association on an observed instance.

Resident bytes per object with one association: 6723 to 322 messaged again, 579 to 322 where it is not.

Test/AssociatedObjectCleanup.m covers the first change. Suite passes, 202 of 202.

A hidden class is made with uninstalled_dtable and keeps it until the
object is next messaged. A method is recorded in the class as it is
installed into a dtable, in installMethodInDtable, and
objc_update_dtable_for_class returns immediately for a class that has no
dtable, so the class_addMethod that puts .cxx_destruct on a new hidden
class does not reach the field.

class->cxx_destruct therefore stays null, call_cxx_destruct finds nothing
to call, and deallocHiddenClass never runs. The hidden class, its
reference list and every association the object holds are never freed. An
object that is messaged again after gaining an association gets a dtable,
and with it the cleanup, which is why this is not seen more often.

The field is recorded when the hidden class is made.

Test/AssociatedObjectCleanup.m gives an object a retained association,
hands the last reference to it, and destroys the object without messaging
it again. The associated object's dealloc does not run before this change
and does run after it.
A hidden class holds the association list and adds no methods of its own,
but it built a copy of its superclass's dispatch table the first time the
object was messaged after gaining an association, and never changed it.
The copy is most of what an association costs.

It now answers sends from its superclass's table, and gets one of its own
only when a method is added to it through object_addMethod_np or
object_replaceMethod_np. deallocHiddenClass does not free a table the
class does not own.

The .cxx_destruct is recorded in the class rather than added as a method,
which the previous commit already does. Adding it as a method here would
install deallocHiddenClass in the table the superclass is using, and every
instance of that class would run it.

Resident bytes per object holding one association, over 100000 objects:
6723 to 516 where the object is messaged again after associating, and 579
to 516 where it is not. The two cases now cost the same.

Suite passes, 202 of 202.
The reference list is allocated with the hidden class, zeroed there and
walked in full when the object is destroyed, so its length is paid by
every object that carries an association. Ten slots was a guess, and the
Microsoft fork of this runtime still carries the same ten.

WinObjC's key value observing, which is the heavy user of associations,
puts one association on an observed instance: NSKVOSupport.mm has a single
instance key holding the observation info, and the selector map in
NSKVOSwizzling.mm is associated with the class rather than the instance.

Four slots costs 293 bytes per object holding one association against 440,
and an object with more than four gains a second block rather than a
larger first one.

ns per objc_getAssociatedObject, reading the key added last, and resident
bytes per object over 50000 objects:

           size 2   size 4   size 6   size 10
bytes         246      293      346       440
get of 4     2.90     2.90     2.68      2.68
get of 8     3.34     3.12     3.12      3.12
get of 16    5.29     4.29     4.05      3.88

Reading is unchanged up to eight associations. Suite passes, 202 of 202 at
every size measured.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant