inko-lang / inko

A language for building concurrent software with confidence
http://inko-lang.org/
Mozilla Public License 2.0
855 stars 38 forks source link

Cache and reuse object files and/or LLVM bitcode when compiling to LLVM #520

Closed yorickpeterse closed 8 months ago

yorickpeterse commented 1 year ago

Description

https://github.com/inko-lang/inko/pull/508 introduces a native code compiler uses LLVM, replacing the bytecode interpreter. The compiler compiles everything from scratch every time. While our own parts of the compiler (e.g. generating MIR) are plenty fast, LLVM is quite slow, and easily takes up over 90% of the compilation time.

One way we can improve this is by caching object files, and only lowering MIR modules to LLVM IR if they have changed compared to the cached files. I briefly experimented with this, and in the best case it can drastically cut down the compile times.

A challenge here is that I'm not sure if LLVM can optimise across the LLVM module boundary. If so, then caching the modules may hinder optimisations, because LLVM wouldn't be aware of them. If LLVM only optimises on a per-module basis, then we can cache the modules.

I did also experiment with caching/loading LLVM bitcode, but this doesn't seem to improve compile times much, as most of the time is spent generating the object files; not generating and optimising the LLVM IR.

The object cache would be applied in addition to any other future caching/incremental compilation techniques, should there be a need for them in the future.

Related work

Rust uses incremental compilation: https://github.com/rust-lang/rustc-dev-guide/blob/master/src/queries/incremental-compilation-in-detail.md

Tasks

Related issues

yorickpeterse commented 8 months ago

I've been looking into this starting this week.

To effectively cache LLVM object files, we need to change how we generate symbol names. Specifically, we have to stop using type IDs and instead use the type shapes to generate unique names for specialized methods and types. This way it doesn't matter in which order the object files are generated, as the symbol X always refers to the same thing.

Specialization makes things tricky: consider modules A, B, and C. We store specialized types/methods in the module the generic base type/method originated from. Let's say this is A. If B creates a new specialization but A itself didn't change, and B did, we have to somehow flag A as "changed" such that we generate the new object code for it. This requires that we can somehow track if specialized types are new compared to the last run, or if they're the same. We can't just blindly flag A as changed if B changed, because that could result in unnecessary cache invalidations of A.

Further, if/when we swap out LLVM with a different backend, code generation may become fast enough to warrant a caching strategy that we can scale to different compiler stages (e.g. caching MIR). I'm also not yet sure how to approach that.

Given the goal is to make compilation faster, and Cranelift is becoming more mature, perhaps I should first take another look at using Cranelift instead of LLVM.

yorickpeterse commented 8 months ago

83713aa9 decouples the symbol names from the type IDs, meaning the order in which modules are processed is no longer relevant when generating object files.