Proposal: Simplify How Pragmas are stored

March 24, 2021 ☼ Pharo

Priviously, I showed how we speed up Pragma access in Pharo 9 (see Part I and PartII). This post describes how to further improve things, this time with regard to how Pragamas are stored in the method objects. I hope to do this early in when we start developing Pharo 10.

Describe the problem Pragmas are stored mixed with associations in the AdditionalMethodState. I propose to store all Pragmas as one pre-allocated array as a property #pragmas instead.

This will speedup both pragma access and property access and will radically simplify the code.

Classes involved CompiledMethod, AdditionalMethodState, Compiler backed

Current Situation

This is how Pragmas are now stored. It is a bit strange:

This means that all code very odd as it has to always check:


 propertyAt: aKey ifAbsent: aBlock
    "Answer the property value associated with aKey or, if aKey isn't found, answer the result of evaluating aBlock."
    1 to: self basicSize do: [:i |
        | propertyOrPragma "<Association|Pragma>" |
        propertyOrPragma := self basicAt: i.
        (propertyOrPragma isAssociation
         and: [propertyOrPragma key == aKey]) ifTrue:
            [^propertyOrPragma value]].
    ^aBlock value

while we have a second api that gives us either a pragma or a property, whatever it finds first:

   at: aKey ifAbsent: aBlock
    "Answer the property value or pragma associated with aKey or,
     if aKey isnt found, answer the result of evaluating aBlock."

    1 to: self basicSize do:
        [:i |
        | propertyOrPragma "<Association|Pragma>" |
        (propertyOrPragma := self basicAt: i) key == aKey ifTrue:
            [^propertyOrPragma isAssociation
                ifTrue: [propertyOrPragma value]
                ifFalse: [propertyOrPragma]]].
    ^aBlock value

and if you ask for #pragmas (which is the main API), it has to create the array, iterating and checking for each entry:

    pragmas
    "Return the Pragma objects. Properties are stored as Associations"
    ^ Array new: self basicSize streamContents: [ :pragmaStream | 
          1 to: self basicSize do: [ :i | 
              | propertyOrPragma "<Association|Pragma>" |
              (propertyOrPragma := self basicAt: i) isAssociation ifFalse: [ 
                  pragmaStream nextPut: propertyOrPragma ] ] ]  

This is slow for pragmas, the array is created on demand. This is slow for properties, as we need to check an skip pragmas when searching.

For speed, the code for pragmas on CompiledMethod has to do that:

  pragmas
    | selectorOrProperties |
    ^(selectorOrProperties := self penultimateLiteral) isMethodProperties
        ifTrue: [selectorOrProperties pragmas]
        ifFalse: [#()]

Just to avoid creating an empty array via the streamContents: method. Which, if you ask for pragmas of all methods, does matter. And this is what Senders Of” does…

And this is very, very strange. In the past this lead to very odd things like sender of” for #isFFIMethod giving all method that had the property #isFFIMethod set. (I fixed that already)

I propose to clean this up.

Proposal

I want to have a clear layer: properties are there to add state to CompiledMethods, the Pragma implementation just uses this lower level layer.

AdditionalMethodState should have no code related to Pragmas.

So instead of putting pragmas into the AdditionalMethodState directly, we just add one property #pragmas for those methods where pragmas are used. The compiler then pre-allocates the array and puts it there. (Pragmas are statically known, properties can be set and removed at runtime).

Which means that on CompiledMethod, we just have:

pragmas
    ^ self propertyAt: #pragmas ifAbsent: [ #() ]

pragmas and pragmasDo: on AdditionalMethodState can be removed, we can remove the propertyAt* API there and just use the Dictionary API (which makes sense, as AdditionalMethodState is just a property dictionary with a backpointer)

The only thing we need to do for this to work is to change the code in the compiler backend to be:

addPragma: aPragma

    properties ifNil: [ properties := AdditionalMethodState new ].
    properties
        at: #pragmas
        ifAbsent: [ properties := properties copyWith: #pragmas -> #(  ) ].
    properties
        at: #pragmas
        put: ((properties at: #pragmas) copyWith: aPragma)

(bit ugly… need to use copyWith: as the high level API needs the compiledMethod set in the AdditionalMethodState)

And some smaller changes here and there (e.g. setting the backpointer in pragmas after the compiledMethod is created).

I did first implementation, but this showed that we have to take care with the bootrap. (see here for the closed PR)