llvm-context: modularize compiler builtin functions (#234)

- Add the revive runtime function interface to minimize boiler plate
code.
- Outline heavily repeated code into dedicated functions to bring down
code size.
- The code size tests builds optimized for size.
- Function attributes are passed as slices.

This significantly brings down the code size for all OpenZeppelin wizard
contracts (using all possible features) compiled against OpenZeppelin
`v5.0.0` with size optimizations.

|contract|| `-Oz` main | `-Oz` PR || `-O3` main | `-O3` PR |
|-|-|-|-|-|-|-|
|erc1155.sol||100K|67K||114K|147K|
|erc20.sol||120K|90K||160K|191K|
|erc721.sol||128K|101K||178K|214K|
|governor.sol||226K|165K||293K|349K|
|rwa.sol||116K|85K||154K|185K|
|stable.sol||116K|86K||155K|192K|

On the flip side this introduces a heavy penalty for cycle optimized
builds. Setting the no-inline attributes for cycle optimized builds
helps a lot but heavily penalizes runtime speed (LLVM does not yet
inline everything properly - to be investigated later on).

Next steps:
- Modularize more functions
- Refactor the YUL function arguments to use pointers instead of values
- Afterwards check if LLVM still has trouble inline-ing properly on O3
or set the no-inline attribute if it does not penalize runtime
performance too bad.
This commit is contained in:
xermicus
2025-02-25 16:47:01 +01:00
committed by GitHub
parent 7ffe64ed7c
commit a07968205b
32 changed files with 1444 additions and 655 deletions
@@ -0,0 +1,113 @@
//! The revive compiler runtime function interface definition.
//!
//! Common routines should not be inlined but extracted into smaller functions.
//! This benefits contract code size.
use crate::optimizer::settings::size_level::SizeLevel;
use crate::polkavm::context::function::declaration::Declaration;
use crate::polkavm::context::function::Function;
use crate::polkavm::context::Attribute;
use crate::polkavm::context::Context;
use crate::polkavm::Dependency;
/// The revive runtime function interface simplifies declaring runtime functions
/// and code emitting by providing helpful default implementations.
pub trait RuntimeFunction<D>
where
D: Dependency + Clone,
{
/// The function name.
const NAME: &'static str;
const ATTRIBUTES: &'static [Attribute] = &[
Attribute::NoFree,
Attribute::NoRecurse,
Attribute::WillReturn,
];
/// The function type.
fn r#type<'ctx>(context: &Context<'ctx, D>) -> inkwell::types::FunctionType<'ctx>;
/// Declare the function.
fn declare(&self, context: &mut Context<D>) -> anyhow::Result<()> {
let function = context.add_function(
Self::NAME,
Self::r#type(context),
0,
Some(inkwell::module::Linkage::External),
)?;
let mut attributes = Self::ATTRIBUTES.to_vec();
attributes.extend_from_slice(match context.optimizer_settings().level_middle_end_size {
SizeLevel::Zero => &[],
_ => &[Attribute::OptimizeForSize, Attribute::MinSize],
});
Function::set_attributes(
context.llvm(),
function.borrow().declaration(),
&attributes,
true,
);
Ok(())
}
/// Get the function declaration.
fn declaration<'ctx>(context: &Context<'ctx, D>) -> Declaration<'ctx> {
context
.get_function(Self::NAME)
.unwrap_or_else(|| panic!("runtime function {} should be declared", Self::NAME))
.borrow()
.declaration()
}
/// Emit the function.
fn emit(&self, context: &mut Context<D>) -> anyhow::Result<()> {
context.set_current_function(Self::NAME, None)?;
context.set_basic_block(context.current_function().borrow().entry_block());
let return_value = self.emit_body(context)?;
self.emit_epilogue(context, return_value);
context.pop_debug_scope();
Ok(())
}
/// Emit the function body.
fn emit_body<'ctx>(
&self,
context: &mut Context<'ctx, D>,
) -> anyhow::Result<Option<inkwell::values::BasicValueEnum<'ctx>>>;
/// Emit the function return instructions.
fn emit_epilogue<'ctx>(
&self,
context: &mut Context<'ctx, D>,
return_value: Option<inkwell::values::BasicValueEnum<'ctx>>,
) {
let return_block = context.current_function().borrow().return_block();
context.build_unconditional_branch(return_block);
context.set_basic_block(return_block);
match return_value {
Some(value) => context.build_return(Some(&value)),
None => context.build_return(None),
}
}
/// Get the nth function paramater.
fn paramater<'ctx>(
context: &Context<'ctx, D>,
index: usize,
) -> inkwell::values::BasicValueEnum<'ctx> {
let name = Self::NAME;
context
.get_function(name)
.unwrap_or_else(|| panic!("runtime function {name} should be declared"))
.borrow()
.declaration()
.function_value()
.get_nth_param(index as u32)
.unwrap_or_else(|| panic!("runtime function {name} should have parameter #{index}"))
}
}