本文深入探讨了 Cairo 1 的核心概念,如可变变量、引用和快照,以及函数内联。通过分析 Sierra 代码,解释了 Cairo 中可变变量实际上是 Sierra 中的影子变量,引用通过 ref 关键字传递并隐式返回,快照则允许在保持对象所有权的同时确保其不可修改。此外,文章还讨论了函数内联作为一种优化技术,可以提高代码执行效率。

在本系列的第一篇博文中,我们介绍了 Sierra,这是一种中间语言,旨在通过确保部署在 Starknet 上的所有代码在运行时不会产生错误,从而简化 Starknet 合约的开发过程。在第二篇文章中,我们分析了 Sierra 程序的结构,以全面了解 Sierra 以及为什么它是 Cairo 代码的安全中间表示。在我们结束的文章中,我们将通过分析 Sierra 代码来更深入地研究 Cairo 1 中引入的一些核心和新颖的概念。
Cairo 引入了一个新的快照概念,该概念经常与引用混淆。在 Cairo 中,有三种不同的方式在函数调用中传递变量:按值传递,调用函数拥有变量的所有权;按引用传递,调用函数“借用”变量,并在执行后将所有权返回给调用方上下文;以及按快照传递,你创建一个值的快照,它是值的不可变视图,并将其传递给函数,以便保留基础值的所有权。
ref 和 @(快照)在 Rust 中的等价物是 ref <=> &mut, @ <=> &,但有一些细微之处需要注意。必须特别注意理解如何将可变引用作为函数参数传递以及如何改变值。快照是 Cairo 1 独有的概念,在其他语言中没有直接的等价物!查看 Sierra 代码将使我们更好地理解。
让我们从一个简单的概念开始 - 可变变量。在传统的编程语言中,每个变量都与一个特定的内存单元格相关联,该单元格是计算机内存中存储变量数据的位置。当变量被赋值时,该值存储在与该变量关联的内存单元格中。然后,变量可以在整个程序执行过程中访问或修改存储在该内存单元格中的值。
然而,在 Cairo 中,不可能修改已经写入的内存单元格的内容。分析编译后的 Sierra 代码,让我们看看在 Cairo 程序中将一个变量声明为 mut 时到底会发生什么。让我们考虑以下 Cairo 程序,其中变量 x 被声明为 mut,而 y 没有,并且我们屏蔽了先前对 y 的声明。
fn main() {
let mut x = 3;
x = 5;
let y = 30;
let y = 50;
}
它编译成的 Sierra 代码是:
type felt = felt;
type Unit = Struct<ut@Tuple>;
libfunc felt_const<3> = felt_const<3>;
libfunc drop<felt> = drop<felt>;
libfunc felt_const<5> = felt_const<5>;
libfunc felt_const<30> = felt_const<30>;
libfunc felt_const<50> = felt_const<50>;
libfunc struct_construct<Unit> = struct_construct<Unit>;
libfunc store_temp<Unit> = store_temp<Unit>;
felt_const<3>() -> ([0]);
drop<felt>([0]) -> ();
felt_const<5>() -> ([1]);
drop<felt>([1]) -> ();
felt_const<30>() -> ([2]);
drop<felt>([2]) -> ();
felt_const<50>() -> ([3]);
drop<felt>([3]) -> ();
struct_construct<Unit>() -> ([4]);
store_temp<Unit>([4]) -> ([5]);
return([5]);
example::main@0() -> (Unit);
如这个编译后的 Sierra 程序所示,可变变量是语法糖,使 Cairo 开发者能够轻松地在整个程序的执行过程中修改和更新数据值,而无需手动屏蔽先前声明的变量。当我们修改我们的可变变量 x 时,存储其值的相应 Sierra 变量首先被丢弃,因为它不再使用,然后创建一个具有更新值的新变量,如第 13-14 行所示。同样,对于我们的非可变变量 y,其值被屏蔽,Sierra 中的过程完全相同:先前的值被丢弃,并使用与 y 关联的更新值实例化一个新值,如第 17-18 行所示。然而,建议尽可能使用可变变量而不是屏蔽,因为它确保了类型的一致性。
声明的 Unit 类型表示一个空的 Struct,并且是默认情况下不返回值的函数返回的类型。
在传统语言中,“按引用传递”是一种将变量传递给函数的方法,其中函数接收对变量内存位置的引用。这允许函数直接修改变量的值。在 Cairo 中,通过在定义函数参数时使用 ref 修饰符来实现等效效果。然而,如前所述,重要的是要注意,一旦分配了变量值,就无法像在其他语言中那样在 Cairo 中直接修改。
考虑以下 Cairo 中的代码片段:
fn main() -> felt {
let mut x = 1;
increment(ref x);
x
}
fn increment(ref x: felt) {
x+=1;
}
在这个例子中,x 变量使用 mut 关键字定义为可变的,并且使用 ref 前缀将 x 的可变引用传递给 increment 函数。该函数直接增加 x 的值,并返回新值。
为了进一步理解它的底层运作方式,让我们分析相应的 Sierra 代码:
type felt252 = felt252;
type Unit = Struct<ut@Tuple>;
libfunc felt252_const<1> = felt252_const<1>;
libfunc store_temp<felt252> = store_temp<felt252>;
libfunc function_call<user@pass_by_ref::pass_by_ref::increment> = function_call<user@pass_by_ref::pass_by_ref::increment>;
libfunc drop<Unit> = drop<Unit>;
libfunc felt252_add = felt252_add;
libfunc struct_construct<Unit> = struct_construct<Unit>;
libfunc store_temp<Unit> = store_temp<Unit>;
felt252_const<1>() -> ([0]);
store_temp<felt252>([0]) -> ([3]);
function_call<user@pass_by_ref::pass_by_ref::increment>([3]) -> ([1], [2]);
drop<Unit>([2]) -> ();
store_temp<felt252>([1]) -> ([4]);
return([4]);
felt252_const<1>() -> ([1]);
felt252_add([0], [1]) -> ([2]);
struct_construct<Unit>() -> ([3]);
store_temp<felt252>([2]) -> ([4]);
store_temp<Unit>([3]) -> ([5]);
return([4], [5]);
pass_by_ref::pass_by_ref::main@0() -> (felt252);
pass_by_ref::pass_by_ref::increment@6([0]: felt252) -> (felt252, Unit);
这里首先要注意的是 increment 函数在 Cairo 中的声明的签名。正如预期的那样,该函数为没有返回值的函数返回默认的 Unit 类型,这是预料之中的。然而,它也返回一个 felt252。当函数参数被声明为 ref 时,编译器将生成代码来自动返回传递给函数的参数的更新值,而无需在高层代码中指定它。
这是 Cairo 提供语法糖以改善开发者体验的另一个例子。上面的代码在功能上等同于以下按值传递的代码,并且它们编译出大致相同的 Sierra 代码。
fn main() -> felt {
let mut x = 1;
increment(x)
}
fn increment(mut x: felt) -> felt {
x += 1;
x
}
type felt252 = felt252;
libfunc felt252_const<1> = felt252_const<1>;
libfunc store_temp<felt252> = store_temp<felt252>;
libfunc function_call<user@pass_by_value::pass_by_value::increment> = function_call<user@pass_by_value::pass_by_value::increment>;
libfunc rename<felt252> = rename<felt252>;
libfunc felt252_add = felt252_add;
felt252_const<1>() -> ([0]);
store_temp<felt252>([0]) -> ([2]);
function_call<user@pass_by_value::pass_by_value::increment>([2]) -> ([1]);
rename<felt252>([1]) -> ([3]);
return([3]);
felt252_const<1>() -> ([1]);
felt252_add([0], [1]) -> ([2]);
store_temp<felt252>([2]) -> ([3]);
return([3]);
pass_by_value::pass_by_value::main@0() -> (felt252);
pass_by_value::pass_by_value::increment@5([0]: felt252) -> (felt252);
在 Cairo 编程语言中,快照被引入作为一种包装器类型,用于在给定时间创建对象的不可变视图。当我们需要对像数组这样的不可复制的类型执行操作时,快照非常有用。在运行时实现中,由于 Cairo Assembly 的一次写入内存模型,快照是零成本抽象。
要了解更多关于快照是什么以及如何使用它们,让我们看看它们是如何编译成 Sierra 的。在下面的 Cairo 程序中,我们定义了一个变量 x,并将 x 的快照传递给 pass_by_snapshot 函数,该函数使用 desnap 运算符 * 返回 x 的值。
fn main() -> felt252 {
let x = 24;
pass_by_snapshot(@x);
x
}
fn pass_by_snapshot(value: @felt252) -> felt252 {
*value
}
type felt252 = felt252;
libfunc felt252_const<24> = felt252_const<24>;
libfunc snapshot_take<felt252> = snapshot_take<felt252>;
libfunc store_temp<felt252> = store_temp<felt252>;
libfunc function_call<user@snapshots::snapshots::pass_by_snapshot> = function_call<user@snapshots::snapshots::pass_by_snapshot>;
libfunc drop<felt252> = drop<felt252>;
libfunc rename<felt252> = rename<felt252>;
felt252_const<24>() -> ([0]);
snapshot_take<felt252>([0]) -> ([1], [2]);
store_temp<felt252>([2]) -> ([4]);
function_call<user@snapshots::snapshots::pass_by_snapshot>([4]) -> ([3]);
drop<felt252>([3]) -> ();
store_temp<felt252>([1]) -> ([5]);
return([5]);
rename<felt252>([0]) -> ([1]);
store_temp<felt252>([1]) -> ([2]);
return([2]);
snapshots::snapshots::main@0() -> (felt252);
snapshots::snapshots::pass_by_snapshot@7([0]: felt252) -> (felt252);
在分析这个程序时,我们观察到:
felt252。即使我们在 Cairo 程序中指定它应该接受一个快照作为参数,pass_by_snapshot 的签名也接受一个 felt252 作为参数。snapshot_take libfunc 接受一个 felt252 作为输入,并返回两个变量。它的签名与 dup libfunc 非常相似。* 不会生成任何 Sierra 代码为了更多地了解这里发生了什么,让我们深入研究编译器代码。在 cairo-lang-sierra crate 中,我们了解到快照只是一个围绕对象的包装器,它确保原始对象不被修改。如果该类型无法复制,则 snapshot_take libfunc 仅返回该类型的快照。可复制的类型是它们自己的快照 - 因为如果我们能够复制该值,则快照本身是无用的。快照的这个概念只存在于 Sierra 级别,并通过确保包装在快照中的对象无法被修改来使线性类型系统有效。
但是,在什么时候我们发现快照特别有用呢?特别是在处理像数组这样的不可复制类型时。在下面的代码中,函数 foo 接受一个数组 a 作为参数。该数组的快照被传递给两个函数,然后返回该数组。
fn foo(a: Array::<felt252>) -> Array::<felt252> {
bar(@a);
bar_2(@a);
a
}
fn bar(a: @Array::<felt252>) {}
fn bar_2(a: @Array::<felt252>) {}
type felt252 = felt252;
type Array<felt252> = Array<felt252>;
type Snapshot<Array<felt252>> = Snapshot<Array<felt252>>;
type Unit = Struct<ut@Tuple>;
libfunc snapshot_take<Array<felt252>> = snapshot_take<Array<felt252>>;
libfunc store_temp<Snapshot<Array<felt252>>> = store_temp<Snapshot<Array<felt252>>>;
libfunc function_call<user@snapshot_2::snapshot_2::bar> = function_call<user@snapshot_2::snapshot_2::bar>;
libfunc drop<Unit> = drop<Unit>;
libfunc function_call<user@snapshot_2::snapshot_2::bar_2> = function_call<user@snapshot_2::snapshot_2::bar_2>;
libfunc store_temp<Array<felt252>> = store_temp<Array<felt252>>;
libfunc drop<Snapshot<Array<felt252>>> = drop<Snapshot<Array<felt252>>>;
libfunc struct_construct<Unit> = struct_construct<Unit>;
libfunc store_temp<Unit> = store_temp<Unit>;
snapshot_take<Array<felt252>>([0]) -> ([1], [2]);
store_temp<Snapshot<Array<felt252>>>([2]) -> ([4]);
function_call<user@snapshot_2::snapshot_2::bar>([4]) -> ([3]);
drop<Unit>([3]) -> ();
snapshot_take<Array<felt252>>([1]) -> ([5], [6]);
store_temp<Snapshot<Array<felt252>>>([6]) -> ([8]);
function_call<user@snapshot_2::snapshot_2::bar_2>([8]) -> ([7]);
drop<Unit>([7]) -> ();
store_temp<Array<felt252>>([5]) -> ([9]);
return([9]);
drop<Snapshot<Array<felt252>>>([0]) -> ();
struct_construct<Unit>() -> ([1]);
store_temp<Unit>([1]) -> ([2]);
return([2]);
drop<Snapshot<Array<felt252>>>([0]) -> ();
struct_construct<Unit>() -> ([1]);
store_temp<Unit>([1]) -> ([2]);
return([2]);
snapshot_2::snapshot_2::foo@0([0]: Array<felt252>) -> (Array<felt252>);
snapshot_2::snapshot_2::bar@10([0]: Snapshot<Array<felt252>>) -> (Unit);
snapshot_2::snapshot_2::bar_2@14([0]: Snapshot<Array<felt252>>) -> (Unit);
在生成的 Sierra 代码中,我们注意到快照类型的声明。与我们之前的示例不同,snapshot_take libfunc 同时返回原始对象和原始对象的快照 - 我们对象的包装器类型。然后将此快照传递给我们的函数。如果你尝试调用修改数组对象的函数,例如 array_append libfunc,则 Sierra 程序将不会编译为 CASM,因为在编译时会检测到类型不匹配。这是因为你正在尝试附加到 Snapshot<Array<T>> 类型,但 array_append libfunc 需要 Array<T> type。
总而言之,当函数使用 @ 获取值的快照时,它只能读取该值而不能修改它。它的行为类似于 Rust 中使用 & 的不可变借用,它允许多个程序部分同时读取相同的值,同时确保它不被修改。当处理不可复制的对象时,使用快照允许你在调用上下文中保留对象的所有权,同时确保对象保持不变。
函数内联是一种编译器优化技术,它用被调用函数的实际代码替换函数调用。它通过将函数的代码直接集成到调用函数中来消除函数调用的开销。
Cairo 编译器会自动将对标记为内联的函数的调用直接替换为它们的 Sierra 代码。这种优化对于经常调用的小函数特别有用。内联可以减少函数调用的开销,并导致更快、更优化的执行,因为值不需要推送到内存。考虑 Cairo 程序,其中第一个函数具有 [inline(always)] 属性,而第二个函数没有。
fn main() {
inlined();
not_inlined();
}
#[inline(always)]
fn inlined() -> felt {
1 + 1
}
fn not_inlined() -> felt {
2 + 2
}
type felt252 = felt252;
type Unit = Struct<ut@Tuple>;
libfunc felt252_const<1> = felt252_const<1>;
libfunc store_temp<felt252> = store_temp<felt252>;
libfunc felt252_add = felt252_add;
libfunc drop<felt252> = drop<felt252>;
libfunc function_call<user@inline::inline::not_inlined> = function_call<user@inline::inline::not_inlined>;
libfunc struct_construct<Unit> = struct_construct<Unit>;
libfunc store_temp<Unit> = store_temp<Unit>;
libfunc felt252_const<2> = felt252_const<2>;
felt252_const<1>() -> ([0]);
felt252_const<1>() -> ([1]);
store_temp<felt252>([0]) -> ([0]);
felt252_add([0], [1]) -> ([2]);
drop<felt252>([2]) -> ();
function_call<user@inline::inline::not_inlined>() -> ([3]);
drop<felt252>([3]) -> ();
struct_construct<Unit>() -> ([4]);
store_temp<Unit>([4]) -> ([5]);
return([5]);
felt252_const<1>() -> ([0]);
felt252_const<1>() -> ([1]);
store_temp<felt252>([0]) -> ([0]);
felt252_add([0], [1]) -> ([2]);
store_temp<felt252>([2]) -> ([3]);
return([3]);
felt252_const<2>() -> ([0]);
felt252_const<2>() -> ([1]);
store_temp<felt252>([0]) -> ([0]);
felt252_add([0], [1]) -> ([2]);
store_temp<felt252>([2]) -> ([3]);
return([3]);
inline::inline::main@0() -> (Unit);
inline::inline::inlined@10() -> (felt252);
inline::inline::not_inlined@16() -> (felt252);
在这个程序生成的 Sierra 代码中,编译器没有使用 function_call libfunc 来执行第 15 行的 inline 函数,而是将代码直接集成到 main 函数中。
但是,由于每个内联函数调用的代码重复,使用函数内联可能会增加整体程序大小。因此,建议仅对具有有限数量指令的频繁调用的函数使用内联。
在这篇文章中,我们探讨了 Cairo 1 的一些核心概念,如可变变量、引用和快照。我们已经看到 Cairo 中的可变变量等同于 Sierra 中的阴影变量,以及 Cairo 中的引用如何使用 ref 前缀来传递变量并隐式返回它们。此外,我们还看到了 Cairo 中的快照是如何一个独特的概念,它允许开发者保持对象的所有权,同时确保原始值保持未修改。最后,我们探讨了开发者如何使用函数内联作为一种优化技术。
理解 Cairo 堆栈的核心概念对于成为 Starknet 生态系统中更好的开发者至关重要,我们希望本系列文章为你提供了宝贵的见解和知识来提高你的技能。保持 Starknet 的奇异和繁荣!
Nethermind 是一个由世界一流的构建者和研究人员组成的团队。我们赋能全球的企业和开发者,使他们能够访问去中心化网络并在此基础上进行构建。我们的工作涉及 web3 生态系统的每个部分,从我们的 Nethermind 节点到基础密码学研究和 Starknet 生态系统的基础设施。了解我们的 Starknet 工具套件:Warp, the Solidity to Cairo compiler; Voyager,一个 StarkNet 区块链浏览器;Horus, the open-source formal verification tool for Starknet smart contracts;Juno , Starknet client implementation, and Cairo Smart Contracts Security Auditing services.
如果你有兴趣解决区块链中一些最困难的问题,请访问我们的招聘板 !
本文档是为了读者的总体信息和理解而编写的,并不表示 Nethermind 对任何特定资产、项目或团队的认可,也不保证其安全性。Nethermind 不对上述文章中包含的信息或观点的准确性或完整性作出任何明示或暗示的陈述或保证。任何第三方不应以任何方式依赖本文,包括但不限于作为财务、投资、税务、监管、法律或其他建议,或将本文解释为任何形式的建议。
- 原文链接: medium.com/nethermind-et...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!