Is it reliable to use cargo miri run instead of cargo run during development? Because it compiles much faster

Hello, I just had a random thought. If I want fast Rust compilation, using an interpreter might speed things up since it doesn't need to generate machine code. When I think about how fast JavaScript compiles, yup, it's because JS isn't compiled, it's interpreted, which is why running it in development is instant. Python and PHP are also interpreted, and they also run instantly during development. So I tried looking for a Rust interpreter, hoping to find something other than Miri. Turned out there isn't one. So I benchmarked cargo miri run and cargo run that uses Cranelift to see which one shows results in the terminal faster. For the benchmark, I edited the code, compiled it with cargo run, then edited the code again and used cargo miri run. The result? cargo miri run was way faster

Cargo run :

   Compiling tes v0.1.0 (/dev/tes)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.38s
     Running `target/debug/tes`
Hellobdkjsjtjo, world!

Cargo miri run :

   Compiling uwu v0.1.0 (/dev/tes)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.08s
     Running `/dev/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/bin/cargo-miri runner target/miri/x86_64-unknown-linux-gnu/debug/tes`
Hellowshbsjjkjwjo, world!

That is 17x faster for incremental compilation, which is good fit for applications like GUIs where need to rerun frequently to see visual changes

But are there any specific gotchas that make cargo miri run unsuitable for hot reloading during development? Because it's a bit confusing why focus on Cranelift instead of an interpreter if the goal is fast development compilation? Is there an inherent issue with using an interpreter?

Aside from fast compilation, another good of the Miri interpreter is that it can detect undefined behavior while the code is running

I'm aware of cargo check for finding errors without running the code. But sometimes we actually need to run it, like when building a backend or a GUI

What do you guys think?

EDIT :

Just tried it on a medium sized backend project, and it failed due to an unsupported call. Is this an inherent limitation of interpreters in general, or is it a limitation specific to Miri as in it simply hasn't been implemented in Miri yet?

error: unsupported operation: can't call foreign function `socket` on OS `linux`
  --> /dev/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/mio-1.1.1/src/sys/unix/mod.rs:8:28
   |
 8 | ...unsafe { libc::$fn($($arg, )*) };
   |             ^^^^^^^^^^^^^^^^^^^^^ unsupported operation occurred here
   |
  ::: /dev/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/mio-1.1.1/src/sys/unix/net.rs:33:18
   |
33 | ... = syscall!(socket(domain, socket_type, 0))?;
   |       ---------------------------------------- in this macro invocation
   |
   = help: this means the program tried to do something Miri does not support; it does not indicate a bug in the program

I do not use Cargo for sure, so I can't comment confidentially, but... My Rust compilation is blazing fast even on Raspberry Pi 4. It's a bit slow only for O4. So you can safely use any Cargo option making your compilation faster.

What do you actually use to compile Rust in development?

I use rustc. However I have not so many dependencies, just below 30. Any modern Rust project will use 100 or more of them, so forming a run command for rustc can be complex, and it's the reason people use cargo. Manually creation of rustc arguments can be also complicated for some crates having too many configuration options. It's another reason to use cargo.

Wait, what's the difference between using cargo run and rustc directly? Because cargo run uses rustc by default anyway, and rustc uses LLVM by default. It's this default LLVM backend that makes compile speeds in development slower compared to the Cranelift backend. I think you're misunderstanding something here

On many applications, Miri is excruciatingly slow to execute code, to the point where Miri is only usable for testing selected tiny test cases. That is the primary disadvantage of Miri.

From the Miri README:

By default, Miri ensures a fully deterministic execution by isolating the program from the host system. Some APIs that would usually access the host, such as gathering entropy for random number generators, environment variables, and clocks, are replaced by deterministic "fake" implementations. Set MIRIFLAGS="-Zmiri-disable-isolation" to access the real system APIs instead.

I am not sure if this enables networking. This older thread looks relevant: Strange Miri behavior

Miri is primarily intended as a reliable UB detector for test suites and experiments, not a tool for other kinds of development.

Even if the runtime speed is on par with Javascript/Python/PHP, it doesn't actually matter, because runtime speed in a debug build during the development phase isn't necessary. What's needed is for the speed from running the 'run' command to seeing the results to be fast

If Miri had a setting to disable all UB checks, meaning it acts as a pure interpreter with only the standard checks built into normal Rust (the checks when running cargo run normally), the compile speed should be be even faster

I tried using it to compile a medium sized project that doesn't depend on OS APIs to prevent the unsopported crashes, and the incremental times were consistently faster than Cranelift

I tried running it using the flags from that post :

MIRIFLAGS="-Zmiri-disable-isolation -Zmiri-native-lib=/lib/x86_64-linux-gnu/libc.so.6" cargo miri run

The result was a different error :

error: post-monomorphization error: the type `Self` does not have a fixed layout
  --> /root/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/hash/random.rs:71:9
   |
71 |         KEYS.with(|keys| {
   |         ^^^^ post-monomorphization error occurred here
   |                                                     = note: stack backtrace:
           0: std::hash::RandomState::new                            at /root/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/hash/random.rs:71:9: 71:13
           1: tokio::loom::std::rand::seed
               at /root/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.49.0/src/loom/std/mod.rs:41:26: 41:44
           2: tokio::util::rand::RngSeed::new
               at /root/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.49.0/src/util/rand.rs:37:24: 37:49
           3: tokio::runtime::Builder::new
               at /root/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.49.0/src/runtime/builder.rs:317:51: 317:65                                              4: tokio::runtime::Builder::new_multi_thread                                                                    at /root/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.49.0/src/runtime/builder.rs:263:9: 263:44
           5: main
               at src/main.rs:19:5: 19:9

Do you know of any other Rust interpreters besides Miri?

When I say that Miri is “excruciatingly slow” I mean infeasibly slow in many cases. If your application can run acceptably fast for development work under Miri, you’re lucky; this is not the typical experience.

This looks like all TLS usage causes "unsupported type for native call" when using `-Zmiri-native-lib` flag · Issue #4879 · rust-lang/miri · GitHub which should be fixed in 1.96.0.

I do not. (Well, there’s the const-evaluator in rustc, but that isn’t usable for running normal programs.)

I think what makes Miri slow at runtime is the runtime analysis and checks it performs, not because it is interpreter, because Miri does runtime analysis that the normal Rust compiler doesn't do. If there were an option to disable all of that, it may be fast just like any other language interpreter. After all, other interpreters can be fast enough for development process, Python and PHP were fast enough for development even before they had JIT. An interpreter cuts out the compiler backend process because it doesn't need to generate machine code

I updated nightly to the newest version, and now the error is undefined behavior like this :

error: Undefined Behavior: memory access failed: attempting to access 16 bytes, but got 0x560c1f8d22a0[noalloc] which is a dangling pointer (it has no provenance)
    --> /root/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/vec/mod.rs:1045:13
     |
1045 |             ptr::write(end, value);
     |             ^^^^^^^^^^^^^^^^^^^^^^ Undefined Behavior occurred here
     |
     = help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
     = help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
     = note: stack backtrace:
             0: std::vec::Vec::<(*mut u8, unsafe extern "C" fn(*mut u8)), std::alloc::System>::push_mut
                 at /root/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/vec/mod.rs:1045:13: 1045:35                               1: std::vec::Vec::<(*mut u8, unsafe extern "C" fn(*mut u8)), std::alloc::System>::push
                 at /root/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/vec/mod.rs:1004:17: 1004:37
             2: std::sys::thread_local::destructors::list::register                                                          at /root/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/sys/thread_local/destructors/list.rs:14:5: 14:26
             3: std::sys::thread_local::destructors::linux_like::register
                 at /root/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/sys/thread_local/destructors/linux_like.rs:52:13: 52:43
             4: std::thread::local_impl::EagerStorage::<tokio::runtime::context::Context>::initialize
                 at /root/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/sys/thread_local/native/eager.rs:49:13: 49:87
             5: std::thread::local_impl::EagerStorage::<tokio::runtime::context::Context>::get
                 at /root/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/sys/thread_local/native/eager.rs:38:40: 38:57
             6: tokio::runtime::context::CONTEXT::{constant#0}::{closure#0}
                 at /root/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/sys/thread_local/native/mod.rs:68:25: 68:54
             7: <{closure@tokio::runtime::context::CONTEXT::{constant#0}::{closure#0}} as std::ops::FnOnce<(std::option::Option<&mut std::option::Option<tokio::runtime::context::Context>>,)>>::call_once - shim
                 at /root/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ops/function.rs:250:5: 250:71
             8: std::thread::LocalKey::<tokio::runtime::context::Context>::try_with::<{closure@tokio::runtime::context::current::try_set_current::{closure#0}}, tokio::runtime::context::current::SetCurrentGuard>
                 at /root/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/thread/local.rs:461:37: 461:55
             9: tokio::runtime::context::current::try_set_current
                 at /root/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.49.0/src/runtime/context/current.rs:34:5: 34:52
             10: tokio::runtime::Handle::enter
                 at /root/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.49.0/src/runtime/handle.rs:88:27: 88:64
             11: tokio::runtime::Builder::build_threaded_runtime
                 at /root/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.49.0/src/runtime/builder.rs:1816:26: 1816:40
             12: tokio::runtime::Builder::build
                 at /root/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.49.0/src/runtime/builder.rs:989:34: 989:63
             13: main
                 at src/main.rs:19:5: 19:9

It's correct, I do not understand how things are working here. I said it upfront. I do not use Cargo and have no problems with compilation speed, so I did just an assumption that Cargo influence.

Cargo itself is just parsing and config files, checking timestamps, and running rustc, 90% of the time. Occasionally it needs to first download dependencies. The only real difference is that if you're using cargo it can become quite easy to pull in millions of lines of code without thinking about it.

In practice, building rust code is apparently about on par to a bit faster than equivalent C++ or Zig.

I've got a problem at work where a tiny Rust endpoint takes over 15 minutes to build and test on CI, simply because the dependences (AWS lambda) are so absurdly poorly designed for build times. I have I spike that reimplements it using basically the same tools like reqwest and tower/hyper/axum and it builds and tests in well under a minute (about 7 seconds clean on my now old work laptop), I just need to find the time to port to it.

I often encounter similar problems; Rust compilation speed is indeed a bit slow… On my previous laptop (Win AMD R7-5900HX), compiling a Rust project took half an hour. The most agonizing part for me when writing Rust code is the cargo build… it takes forever, then I fix the errors, then wait for the long compilation again, and it's a never-ending cycle.

Of course, this is just a rant; I still really like Cargo's dependency management...

Do you use another tool to help with the complication of calling rustc? "make" or something like that?

The reason your compilation is fast is more about having a low dependency count. Try compiling a GUI project like Tauri, GPUI, or a game. I compiled those kinds of projects on a device with better specs than a Raspberry Pi 4 in debug mode in development phase, and it took a long time, let alone on a Raspberry Pi 4 itself. For example, I just change a background color or padding and want to see how the UI looks, waiting for it for long

It's correct, I use a tool similar to "make", you can get its details here.

Yes, it is the major reason. Other help is that my making tool doesn't rescan dependencies, and all of them should be compiled first. It's one time effort and every compiled crate is shared between all projects using it. It also saves a lot compilation time. rustc checks anyway possible discrepancies, like one compiled crate depends on other, and the other has a newer compilation time. So there is no risk that the final build will be screwed.

Finally, I use micro services like approach helping to keep relatively small pieces of final compiled code.

I'm curious how this works in practice, since the Rust ABI isn't stable. In theory, between compiler updates (or even compilations on the same version), anything from a structs layout to a function's ABI could break. Unless that's not entirely true?

Like others, I'm using Miri only for specific tests to make sure there's no alias/UB issue, not for anything else. It's much slower because it's interpreted and runs other checks beside what the tested code does. It's possible that it's faster in some rare cases, but if your unit tests are thorough enough, it'll be much slower than compiled code without the overhead of those checks.

You should normally benefit from incremental compilation, so it's slower only the first time each day, unless you change a lot in the code base (checkout, ...).

Once compiled, all your tests benefit from the speed-up, so even if Miri is faster at the start in some situations, it'll fall behind as your code grows.

Miri is platform-independent; I remember having to disable it in a series of tests that were writing/reading files, for instance. It means you won't test platform-specific code, and since it's interpreted, you won't test the same produced code, either. You'd be artificially limiting your test coverage.

I tend to run tests under Miri (for crates with unsafe), but if it is a "slow" test with a lot of data or iterations, I reduce that for Miri using cfg. I think this example originally may be from the standard library tests:

#[test]
fn test_basic_large() {
    let mut map = BTreeMap::new();
    // Miri is too slow
    let size = if cfg!(miri) { 200 } else { 10000 };
...

Yeah, I know Miri will definetely be slower in runtime than a binary. What I mean is it compiles much faster, not it runs the code much faster. Compiling is equal to producing binary in the normal compiler. Running the code is equal to running the binary. That I want to try Miri without all the runtime safety analysis that maybe it will runs fast enough like Python if there are no runtime safety analysis to check UB. Eg a fork of Miri but without its runtime UB analysis. So fast compile and fast enough runtime

The default debug build already has incremental enabled, but the reality is still it is unusable I mean can not using it to develop heavy dependencies project like GUI in a state of enjoyable. Because just changing single line eg padding 10px requires many seconds to wait, that really consumes time. I had a random thought that if there are Rust interpreter designed for solely interpreter, not analysing safety violation at runtime, it may helps it because seeing how Python with interpreter can compile in instant and run fast enough for development process, same for PHP