Materializing compile-time values at run time
Mojo’s compile-time metaprogramming makes it easy to make calculations at compile time for later use. The process of making a compile-time value available at run time is called materialization. For types that can be trivially copied, this isn’t an issue. The compiler can simply insert the value into the compiled program wherever it’s needed.
comptime threshold: Int = some_calculation() # calculate at compile time
for i in range(1000):
my_function(i, threshold) # use value at runtimeHowever, Mojo also allows you to create instances of much more complex types at
compile-time: types that dynamically allocate memory, like List and Dict.
Re-using these values at run time presents some questions, like where the memory
is allocated, who owns the values, and when the values are destroyed.
When you use a comptime value at run time, you're explicitly or implicitly
copying the value into a run-time variable:
comptime comptime_value = 1000
var runtime_value = comptime_valueThis process of moving a compile-time value to a run-time variable is called
materialization. If the value is implicitly copyable, like an Int or Bool,
Mojo treats it as implicitly materializable as well.
But types that aren't implicitly copyable present other challenges. Consider the following code:
fn lookup_fn(count: Int):
comptime list_of_values = [1, 3, 5, 7]
for i in range(count):
# Some computation, doesn't matter what it is.
idx = dynamic_function(i)
# Look up another value
lookup = list_of_values[idx]
# Use the value
process(lookup)This looks reasonable, but compiling it produces an error on this line:
lookup = list_of_values[idx]cannot materialize comptime value of type 'List[Int]' to runtime
because it is not 'ImplicitlyCopyable'Just like Mojo forces you to explicitly copy() a value that’s expensive to
copy, it forces you to explicitly materialize values that are expensive to
materialize by calling the
materialize() function.
Here’s the code above with explicit materialization added:
fn lookup_fn(count: Int):
comptime list_of_values = [1, 3, 5, 7]
for i in range(count):
idx = dynamic_function(i)
# This is the problem
var tmp: List[Int] = materialize[list_of_values]()
lookup = tmp[idx]
# tmp is destroyed here
process(lookup)This code materializes the list of values inside of the loop, which includes
dynamically allocating heap memory and storing the four elements into that
memory. Because the last use of tmp is on the next line, the memory then gets
deallocated before the loop iterates. This creates and destroys the list on
every iteration of the loop, which is clearly wasteful.
A more efficient version would materialize the list outside of the loop:
fn lookup_fn(count: Int):
comptime list_of_values = [1, 3, 5, 7]
var list = materialize[list_of_values]()
for i in range(count):
idx = dynamic_function(i)
lookup = list[idx]
process(lookup)
# materialized list is destroyed hereGlobal lookup tables
Mojo doesn’t currently have a general-purpose mechanism for creating global
static data. This is a problem for some performance-sensitive code where you
want to use a static lookup table. Even if you declare the table as a comptime
value, you need to materialize it each time you want to use the data.
The global_constant() function
provides a solution for storing a compile-time value into static global storage,
so it can be accessed without repeatedly materializing the value. However, this
currently only works for self-contained values which don’t include pointers to
other locations in memory. That rules out using collection types like List and
Dict.
The easiest way to use it is with
InlineArray, which
allocates a statically sized array of elements on the stack. The following code
uses global_constant() to create a static lookup table.
from builtin.globals import global_constant
fn use_lookup(idx: Int) -> Int64:
comptime numbers = InlineArray[Int64, 10](
1, 3, 14, 34, 63, 101, 148, 204, 269, 343
)
ref lookup_table = global_constant[numbers]()
if idx >= len(lookup_table):
return 0
return lookup_table[idx]
def main():
print(use_lookup(3))
At compile time, Mojo allocates the numbers array, and then the
global_constant() method copies it into static constant memory, where the code
can reference it without requiring any dynamic logic to create or populate the
array. At run time, the lookup_table identifier receives an
immutable reference to this memory.
Note the use of ref lookup_table to bind the reference returned by
global_constant(). Using var lookup_table would cause a compiler error,
because it would trigger a copy, and InlineArray doesn’t support implicit
copying.
Materializing literals
Literal values, like string literals and numeric literals are also materialized to their run-time equivalents, but this is mostly handled automatically by the compiler:
comptime str_literal = "Hello" # at compile time, a StringLiteral
var str = str_literal # at run time, a String.
var static_str: StaticString = str_literal # or a StaticStringBoth String and StaticString can be implicitly created from a
StringLiteral, but without a type annotation, Mojo defaults to materializing
StringLiteral as a String.
You can define a "nonmaterializable" struct that can only exist at compile time,
and which is replaced by a different type at run time. For example, the standard
library IntLiteral type supports arbitrary-precision math at compile time, but
materializes as a standard Int in run-time code. For more information, see the
@nonmaterializable decorator.
Was this page helpful?
Thank you! We'll create more content like this.
Thank you for helping us improve!