# Breakthrough Emulation Bottleneck in zkVM Today, zkVMs are becoming increasingly the bedrock of the Ethereum blockchain network, on both the consensus layer and the execution layer. However, one bottleneck plaguing these zkVMs is RISC-V VM emulation. This process is essential for generating traces, which are used to create the ZK proof. This article aims to address how this bottleneck can be broken. Here is a more technical overview of this problem. We've got a program we would like to create a proof for, alongside some input to this program, the Rust compiler has compiled this program to RISC-V bytecode. Running the RISC-V bytecode on the emulator, we now obtain a trace that we can use to create a proof. We aim to make more efficient the process of emulation more efficient alongside trace generation. To achieve this goal, we have two options; 1. Optimize the emulator 2. Introduce dynamic binary recompilation ### Optimized Emulator: **Concept:** This is about making your current instruction fetching, decoding, and execution loop as efficient as possible. **Techniques:** - **Lookup Tables for Decoding:** Instead of complex `if/else` or `switch` statements for every instruction, pre-calculate instruction properties (operand types, functional unit, etc.) into a lookup table indexed by opcode bits. This speeds up the decode phase. - **Direct Threaded Code / Jump Tables:** For the execution phase, after decoding, instead of a large switch, jump directly to the code that handles that instruction type. In C/C++, this can be done with an array of function pointers. - **Micro-optimizations:** Pay attention to memory access patterns (cache locality), avoid unnecessary memory allocations, use faster data structures for registers and memory, and eliminate redundant computations in your interpreter loop. - **Dedicated Register Handling:** Map RISC-V registers directly to host registers if possible, or keep them in a contiguous array to minimize memory access. - **Fast Memory Access:** Implement an efficient virtual-to-physical memory translation if your RISC-V code uses virtual memory. Use host OS features like `mmap` if you're emulating a large memory space. ### Dynamic Recompilation (JIT Compilation for Emulators): **Concept:** The emulator identifies frequently executed blocks of RISC-V machine code, translates them *on-the-fly* into the *host CPU's native machine code*, stores these translated blocks in an executable memory cache, and then executes the native code directly. **Why it's powerful:** Your host CPU executes native instructions directly, bypassing the decode-execute cycle of the interpreter for those compiled blocks. This is often an order of magnitude faster. #### **Core Idea:** - **Basic Blocks:** Identify sequences of RISC-V instructions that have one entry point and one or more exit points (e.g., branches, jumps, returns). - **Translation:** When a basic block is entered for the first time, or if it's executed frequently, translate it. For example, a RISC-V `add x1, x2, x3` instruction might translate to host `mov rax, [reg_x2]; add rax, [reg_x3]; mov [reg_x1], rax` (or more optimally, directly using host registers if possible). - **Code Cache:** Store the generated host machine code in a dynamically allocated, executable memory region. - **Execution:** Instead of interpreting, jump directly to the compiled host code block. - **Fallback:** If a jump or branch goes to an uncompiled region, or if an exception/interrupt occurs, return control to the interpreter. Using these methods, the speed of execution would be increased by in order of magnitude, but sad,ly in the case of zkVM, we can only employ "Optimized Emulator" and not "Dynamic Recompilation". Dynamic Recompilation introduces an abstraction between the RISC-V bytecode and it execution. This fuse, skips, reorders, or optimizes instructions, doing this, the execution trace does not match the RISCV semantics the prover is built towards, having the classic case where the bytecode been proven is not in fact the bytecode generating this trace that is eventally been proven. I guess a solution for this would be to build an x86_64 zkVM which is the native machine's native code (x86_64), the RISCV code is being transpiled to, but this would be a lot of work. At this moment, you would be so disappointed finding out that this article didn't provide the "breakthrough" strategy over this bottleneck... my not-so-sincere apologies. The team at ZISK has done some innovation solving this problem; ### The ZisK Way: Ahead-of-Time (AOT) Compilation ZisK uses a technique called Ahead-of-Time (AOT) compilation to create a highly optimized emulator. 1. Translate Before Running: Before executing the RISC-V program, the ZisK compiler takes the entire program and translates it directly into the native language of the host CPU (in this case, x86 assembly, the language of Intel and AMD processors). 2. Custom, Hyper-Optimized Translation: This isn't a generic translation. They've written custom code to ensure that each RISC-V instruction is converted into the smallest, fastest possible sequence of x86 instructions. The text states this is just "3 or 4 x86 instructions per RISC-V instruction." This is incredibly efficient.