Skip to content

Update MethodDictionary rebuild to speed up trait recompilation#19870

Open
jecisc wants to merge 41 commits into
pharo-project:Pharo14from
jecisc:kernel/trait-rebuild
Open

Update MethodDictionary rebuild to speed up trait recompilation#19870
jecisc wants to merge 41 commits into
pharo-project:Pharo14from
jecisc:kernel/trait-rebuild

Conversation

@jecisc

@jecisc jecisc commented Jul 8, 2026

Copy link
Copy Markdown
Member

This PR brings multiple speed up for large trait recompilation.

Recompiling large traits can be really slow because it needs to rebuild the method dictionary of each of the trait user.

This was done by iterating over the selectors of the trait composition and installing a copy of the method associated in the user. But we traversed multiple time the trait composition by doing this.

Now I build a compilation info object containing all the info directly preventing multiple visits of the trait composition.

Most of the time we still spend in the rebuild is spend in recompiling the methods if the user has a slot. The installation time of the method is much shorter.

For comparison, in a vanilla Moose image it was taking 72sec to add a slot to TEntityMetaLevelDependency. With this change it drops to 27sec.

There might be ways to speed up a little more operations, I might explore this later.

jecisc added 30 commits June 15, 2026 23:14

@Gabriel-Darbord Gabriel-Darbord left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this PR, this is great for Moose where traits are omnipresent :)
I have a few technical and quality suggestions.
I can't really comment on semantics and validity, but I don't see any blatant issue there.

Comment on lines +126 to +142
| innerInfos aliassed |
"I calculate the new aliases selectors"
innerInfos := inner methodsCompilationInfo.
aliassed := OrderedCollection new: self aliases size.

self aliases associations do: [ :association |
innerInfos
detect: [ :info | info selector = association value ]
ifFound: [ :info |
aliassed add: (TraitMethodCompilationInfo new
selector: association key;
definingTrait: info definingTrait;
compiledMethod: info compiledMethod;
sourceCode: (info compiledMethod getSourceReplacingSelectorWith: association key);
yourself) ] ].

^ innerInfos , aliassed

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aliassed -> aliased

Suggested change
| innerInfos aliassed |
"I calculate the new aliases selectors"
innerInfos := inner methodsCompilationInfo.
aliassed := OrderedCollection new: self aliases size.
self aliases associations do: [ :association |
innerInfos
detect: [ :info | info selector = association value ]
ifFound: [ :info |
aliassed add: (TraitMethodCompilationInfo new
selector: association key;
definingTrait: info definingTrait;
compiledMethod: info compiledMethod;
sourceCode: (info compiledMethod getSourceReplacingSelectorWith: association key);
yourself) ] ].
^ innerInfos , aliassed
| innerInfos aliased |
"I calculate the new aliases selectors"
innerInfos := inner methodsCompilationInfo.
aliased := OrderedCollection new: self aliases size.
self aliases associations do: [ :association |
innerInfos
detect: [ :info | info selector = association value ]
ifFound: [ :info |
aliased add: (TraitMethodCompilationInfo new
selector: association key;
definingTrait: info definingTrait;
compiledMethod: info compiledMethod;
sourceCode: (info compiledMethod getSourceReplacingSelectorWith: association key);
yourself) ] ].
^ innerInfos , aliased

innerInfos := inner methodsCompilationInfo.
aliassed := OrderedCollection new: self aliases size.

self aliases associations do: [ :association |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
self aliases associations do: [ :association |
self aliases associationsDo: [ :association |

TraitedClass allSlots) anySatisfy: [ :x | x name = e name ] ]

| slotNamesToReject |
"Save in a variable to not recomcatenate for each slot"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"Save in a variable to not recomcatenate for each slot"
"Save in a variable to not re-concatenate for each slot"

Comment on lines +134 to +150
| innerInfos traitInfos localSelectors methods |
"We first collect the compilation info for the local methods I have."
innerInfos := innerClass localMethods collect: [ :method |
TraitMethodCompilationInfo new
selector: method selector;
definingTrait: self;
compiledMethod: method;
sourceCode: method sourceCode;
yourself ].

"If I need to alias the talent initializer, I do it now."
innerInfos do: [ :info |
info selector = #initializeTalent ifTrue: [
info
selector: self initializeSelectorForMe;
sourceCode: (info compiledMethod getSourceReplacingSelectorWith: self initializeSelectorForMe) ] ].

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Memoize self initializeSelectorForMe since it's re-computed each time by TaCompositionElement?

Suggested change
| innerInfos traitInfos localSelectors methods |
"We first collect the compilation info for the local methods I have."
innerInfos := innerClass localMethods collect: [ :method |
TraitMethodCompilationInfo new
selector: method selector;
definingTrait: self;
compiledMethod: method;
sourceCode: method sourceCode;
yourself ].
"If I need to alias the talent initializer, I do it now."
innerInfos do: [ :info |
info selector = #initializeTalent ifTrue: [
info
selector: self initializeSelectorForMe;
sourceCode: (info compiledMethod getSourceReplacingSelectorWith: self initializeSelectorForMe) ] ].
| innerInfos selector traitInfos localSelectors methods |
"We first collect the compilation info for the local methods I have."
innerInfos := innerClass localMethods collect: [ :method |
TraitMethodCompilationInfo new
selector: method selector;
definingTrait: self;
compiledMethod: method;
sourceCode: method sourceCode;
yourself ].
"If I need to alias the talent initializer, I do it now."
selector := self initializeSelectorForMe.
innerInfos do: [ :info |
info selector = #initializeTalent ifTrue: [
info
selector: selector;
sourceCode: (info compiledMethod getSourceReplacingSelectorWith: selector) ] ].

Comment on lines +54 to +58
| preferedInfos |
preferedInfos := self preferedTrait methodsCompilationInfo.

implementations
detect: [ :info | preferedInfos includes: info ]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prefered -> preferred, but changing preferedTrait is outside the current scope of this PR.

Suggested change
| preferedInfos |
preferedInfos := self preferedTrait methodsCompilationInfo.
implementations
detect: [ :info | preferedInfos includes: info ]
| preferredInfos |
preferredInfos := self preferedTrait methodsCompilationInfo.
implementations
detect: [ :info | preferredInfos includes: info ]

| selectors removedSelectors modified |
| compilationInfos removedSelectors modified |
"During the creation of the class or after a change in the traitComposition, the whole method dictionary is calculated.
If I return true, my users should be updated""1. I recreate the local methodDict"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider formatting this method?
Also these comments have been unintentionally merged previously:

Suggested change
If I return true, my users should be updated""1. I recreate the local methodDict"
If I return true, my users should be updated"
"1. I recreate the local methodDict"

Comment on lines +260 to +265
removedSelectors := self methodDict keys reject: [ :aSelector |
(compilationInfos anySatisfy: [ :compilationInfo | compilationInfo selector = aSelector ]) or: [ self isSelectorToKeep: aSelector ] ].
modified := modified | removedSelectors isNotEmpty.
removedSelectors do: [ :aSelector |
self methodDict removeKey: aSelector.
self removeFromProtocols: aSelector ].
self methodDict removeKey: aSelector.
self removeFromProtocols: aSelector ].

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removedSelectors seems like an unnecessary intermediate collection:

	self methodDict keys do: [ :aSelector |
		((compilationInfos anySatisfy: [ :compilationInfo | compilationInfo selector = aSelector ]) or: [ self isSelectorToKeep: aSelector ]) ifTrue: [
			modified := true.
			self methodDict removeKey: aSelector.
			self removeFromProtocols: aSelector ] ].

Explicitly use keys do: instead of keysDo: to avoid modification during iteration.
You could also try swapping conditions to check isSelectorToKeep: first to be potentially faster.

self == anObject ifTrue: [ ^ true ].
self class = anObject class ifFalse: [ ^ false ].
^ selector = anObject selector and: [
definingTrait = anObject definingTrait and: [ sourceCode = anObject sourceCode and: [ self protocol = anObject protocol ] ] ]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Protocol comparison is faster than sourceCode comparison and we want to fail-fast:

Suggested change
definingTrait = anObject definingTrait and: [ sourceCode = anObject sourceCode and: [ self protocol = anObject protocol ] ] ]
definingTrait = anObject definingTrait and: [ self protocol = anObject protocol and: [ sourceCode = anObject sourceCode ] ] ]

super printOn: aStream.

aStream
nextPutAll: ' [ ';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More in-line with other printOn implementation, do not include a space after the bracket, though the space before is up to you (personally I prefer it compact).

Suggested change
nextPutAll: ' [ ';
nextPutAll: '[';

nextPutAll: '>>';
print: selector.

self compiledMethod ifNotNil: [ :cm | cm isRequired ifTrue: [ aStream nextPutAll: ' - Explicit requirement' ] ].

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If memoized and not afraid of concurrency (compiledMethod could change between nil check and block execution, but I don't think that's a concern for this data structure): cm isRequired -> self isExplicitRequirement

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants