# 运行与性能分析

> 从 Rust 或命令行在远地虚拟机的模拟器中运行客户程序，与你的宿主机构建比较，并在为证明付出代价之前弄清周期都花在了哪里。

运行客户程序（guest）几乎没有成本；证明它的成本则与它运行的周期数成正比。所以先运行，与你的宿主机（host）构建比较，并在证明任何东西之前先看一看周期分布。

## 在 Rust 中：模拟器

`emulator::run` 在宿主程序代码中，以给定的公开输入和证明者提示（advice）执行一个已加载的映像，不生成证明：

```rust title="运行客户程序，并与宿主机构建的结果对比"
let elf = std::fs::read(elf_path)?;
let image = loader::load_elf(&elf).expect("the ELF loads");
let io = emulator::GuestIo { input: b"hi".to_vec(), advice: Vec::new() };
let run = emulator::run(&image, &io).expect("no fatal error");

assert_eq!(run.exit_code, 0);
assert_eq!(run.io.output, my_app::run(b"hi", &[]).unwrap()); // the host build agrees
println!("{} cycles", run.cycle_count);
```

`run` 返回一个 `Execution`：最终的寄存器、退出状态、周期数和公开值。非零退出状态仍然是一次执行，而不是错误，它通过 `exit_code` 返回。致命的执行器错误，例如 `OutOfBounds`、`Misaligned` 或 `NotAnInstruction`，以 `EmuError` 返回，这样的运行没有证明（[故障排查](https://apogee.gweb3networks.com/docs/launch/troubleshooting#fatal)）。

模拟器是映像和输入的纯函数：没有时钟、没有随机数、没有线程。相同的输入给出逐周期相同的执行，这也正是证明者能够执行两遍并切出相同分片的原因。

## 在命令行中：周期分析器

```sh
cargo run --release -p profiler -- elf <elf> [--input <file>] [--advice <file>] [--top <n>] [--json ]
```

它以能容纳客户程序代码的最小表高度，用给定的文件运行客户程序，并打印一份报告。报告中的数字是已执行周期的计数，在任何机器上都相同。

```text
workload
  label                        hello
  guest cycles                 114
  exit status                  0
  journal bytes                13

cycles by semantic workload
  core runtime                             94   82.46%
  unattributed                             20   17.54%

cycles by family
  ADD_SUB_LUI_AUIPC            64
  JUMP_BRANCH_SLT              21
  MEM_WORD                     3
  MEM_SUBWORD                  26

top functions
            94   82.46%          1 calls        94.0 c/call  guest_sdk::commit  [core runtime]
             8    7.02%          1 calls         8.0 c/call  main  [unattributed]
```

如何阅读这份报告：

- **按电路族统计的周期**（cycles by family）就是你要付费的部分。每个有行的电路族，至少要花费一个其高度的分片；一个电路族中的周期越多，它的分片就越多。
- **热点函数**（top functions）把每个函数自身的周期记在它名下，包括编译器内联进它的一切，但不包括它调用的函数。调用次数在函数的第一条指令处计数。
- **按语义工作负载统计的周期**（cycles by semantic workload）按名称把函数归入十四个类别，例如哈希、签名和核心运行时。未归类部分的占比和助记符分布用来检验这种归类，因为任何符号表都不可能把它们标错。
- **加速候选项**（accelerator candidates）为未来版本可能新增的委托定价，给出的是上限：参见[委托](https://apogee.gweb3networks.com/docs/launch/delegations#pricing)。

周期分析器还有两个子命令，用于以太坊工作负载：`block <stem>` 在一份录制好的测试数据上运行 revm 客户程序，`record <number|latest>` 从 `ETH_RPC_URL` 录制一个区块并运行它。

## 降低成本

通常最划算的顺序：

1. **用 `--release` 构建。** 优化能去掉客户程序四分之一到一半以上的指令。
2. **委托哈希和曲线运算。** 使用 `guest_sdk::keccak256`、`sha256`、`ec_add` 以及 vendored 的 `k256` 和 `ark-ff`，而不是把软件实现编译进客户程序。
3. **不要在循环中分配。** 每次分配都要花费指令，而在 bump 分配器下，它还是你永远收不回来的内存（[堆](https://apogee.gweb3networks.com/docs/launch/write#heap)）。
4. **检查，而不是计算。** 如果一个结果求出来昂贵、验证起来便宜，例如排序顺序、平方根或一条穿过树的路径，就让证明者把它作为证明者提示提供，由客户程序来验证。
5. **避免浮点运算。** 它会编译成软件例程；整数和定点运算要便宜得多。

然后再测一次。周期数精确且可重复，所以每一处改动都会体现为一个数字。
