Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Compiler: only have freeze_type in select AST nodes #12428

Merged
merged 2 commits into from
Aug 30, 2022
Merged

Conversation

asterite
Copy link
Member

When you have code like this:

def foo : Int32
  "hello"
end

the way the compiler implements this is by setting the freeze_type property of a Def to Int32. Then before trying to set the method's type to String it will check if there's a freeze_type set. If so, it will check String against Int32 and produce a compile-time error.

This freeze_type concept was implemented generically in all AST nodes: any node can be "frozen", and whenever its type changes we check against freeze_type.

The thing is, there are only four AST nodes that need this freeze_type property: methods, blocks, and special nodes called "meta vars" or "meta type vars" that correspond to local vars and instance/class vars respectively.

This PR makes it so that only those four nodes have a freeze_type instance var. All other nodes still respond to freeze_type by retuning nil.

With this, all AST nodes except those four will now take 8 bytes less in memory. The compiler creates a lot of AST nodes! And whenever a method is instantiated (its arguments get a concrete type) the entire method with all its AST nodes is cloned. So 8 bytes per nodes is a significant amount of memory.

It seems that compiling the compiler before this PR needed 2367.61MB. With this PR it goes down to 2240.64MB (and this is unrelated to the ZeroOneOrMany PR!)

Compilation times doesn't noticeably go down, at least not on my machine.

There are further optimizations to make here: instead of storing freeze_type for a Def, we could actually make freeze_type return the return type AST node type's. But I'd like to make that change in a separate PR. I'm trying to make each change in the smallest way possible.

@asterite asterite changed the title Only have freeze_type in select AST nodes Compiler: only have freeze_type in select AST nodes Aug 30, 2022
@asterite
Copy link
Member Author

Here's an extract output of tool hierarchy for the old compiler:

- class Object (4 bytes)
  |
  +- class Reference (4 bytes)
     |
     +- class Crystal::ASTNode (72 bytes)
        .   @location       : (Crystal::Location | Nil)       (8 bytes)
        .   @end_location   : (Crystal::Location | Nil)       (8 bytes)
        .   @dependencies   : (Array(Crystal::ASTNode) | Nil) (8 bytes)
        .   @freeze_type    : (Crystal::Type | Nil)           (8 bytes)
        .   @observers      : (Array(Crystal::ASTNode) | Nil) (8 bytes)
        .   @enclosing_call : (Crystal::Call | Nil)           (8 bytes)
        .   @type           : (Crystal::Type | Nil)           (8 bytes)
        .   @dirty          : Bool                            (0 bytes)
        |
        +- class Crystal::Alias (112 bytes)
        |      @name          : Crystal::Path              (8 bytes)
        |      @value         : Crystal::ASTNode+          (8 bytes)
        |      @doc           : (String | Nil)             (8 bytes)
        |      @resolved_type : (Crystal::AliasType | Nil) (8 bytes)
        |      @visibility    : Crystal::Visibility        (1 bytes)
        |
        +- class Crystal::Annotation (104 bytes)
        |      @path       : Crystal::Path                         (8 bytes)
        |      @args       : Array(Crystal::ASTNode)               (8 bytes)
        |      @named_args : (Array(Crystal::NamedArgument) | Nil) (8 bytes)
        |      @doc        : (String | Nil)                        (8 bytes)
        |
        +- class Crystal::AnnotationDef (96 bytes)
        |      @name          : Crystal::Path             (8 bytes)
        |      @doc           : (String | Nil)            (8 bytes)
        |      @name_location : (Crystal::Location | Nil) (8 bytes)
        |
        +- class Crystal::Arg (128 bytes)

Here it is with this PR:

- class Object (4 bytes)
  |
  +- class Reference (4 bytes)
     |
     +- class Crystal::ASTNode (64 bytes)
        .   @location       : (Crystal::Location | Nil)       (8 bytes)
        .   @end_location   : (Crystal::Location | Nil)       (8 bytes)
        .   @dependencies   : (Array(Crystal::ASTNode) | Nil) (8 bytes)
        .   @observers      : (Array(Crystal::ASTNode) | Nil) (8 bytes)
        .   @enclosing_call : (Crystal::Call | Nil)           (8 bytes)
        .   @type           : (Crystal::Type | Nil)           (8 bytes)
        .   @dirty          : Bool                            (0 bytes)
        |
        +- class Crystal::Alias (104 bytes)
        |      @name          : Crystal::Path              (8 bytes)
        |      @value         : Crystal::ASTNode+          (8 bytes)
        |      @doc           : (String | Nil)             (8 bytes)
        |      @resolved_type : (Crystal::AliasType | Nil) (8 bytes)
        |      @visibility    : Crystal::Visibility        (1 bytes)
        |
        +- class Crystal::Annotation (96 bytes)
        |      @path       : Crystal::Path                         (8 bytes)
        |      @args       : Array(Crystal::ASTNode)               (8 bytes)
        |      @named_args : (Array(Crystal::NamedArgument) | Nil) (8 bytes)
        |      @doc        : (String | Nil)                        (8 bytes)
        |
        +- class Crystal::AnnotationDef (88 bytes)
        |      @name          : Crystal::Path             (8 bytes)
        |      @doc           : (String | Nil)            (8 bytes)
        |      @name_location : (Crystal::Location | Nil) (8 bytes)
        |
        +- class Crystal::Arg (120 bytes)

You can see ASTNode went from 72 bytes to 64 bytes, and that difference is also kept on (most) subclasses.

Comment on lines 9 to 10
module Freezable
property freeze_type : Type?
Copy link
Member

Choose a reason for hiding this comment

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

I'm wondering if this even warrants a dedicated module. There's little benefit from that (at least for now, maybe you're already planning something else with it?) and we could just add the property directly to each class.

Copy link
Member Author

Choose a reason for hiding this comment

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

Makes sense. Especially if the definition of freeze_type might change for some nodes (like Def).

Comment on lines 19 to 23
{% for name in %w(Block MetaVar MetaTypeVar Def) %}
class {{name.id}}
include Freezable
end
{% end %}
Copy link
Member

Choose a reason for hiding this comment

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

It's nice to have the binding-related things all defined here. But on the other hand, reopening a couple of classes just to include a module and define a property is also adding an extra code location you need to look at for understanding the properties of a AST type.
In semantics/ast.cr we're already reopening all those ASTNode classes to add semantics functionality.
Perhaps it could be an option to just drop in property freeze_type : Type? (or include Freezable) there in the respective places?

Copy link
Member Author

Choose a reason for hiding this comment

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

Done!

@straight-shoota straight-shoota added this to the 1.6.0 milestone Aug 30, 2022
@asterite asterite merged commit a056069 into master Aug 30, 2022
@asterite asterite deleted the opt/freeze branch August 30, 2022 17:38
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