Yea, that has been the case, additionally, if you switch the order, the results will change too. Imo, benchmarking and profiling in Rust are not mature yet, especially for async code. There is a built in benchmark feature in nightly similar to Golang's bench, but because it is in nightly, it is not stable yet and looks like does not support async. The problem I encountered is that simply reversing the order of the written code, from Bench A then Bench B, to Bench B then Bench A, swapped the results, so where A was previously faster, B became faster instead. For serious benchmarking, consider getting a CPU that supports overclocking so the CPU frequency remains constant, setting it to a lower fixed speed to avoid high temperatures, and then warming up the cache by running the first few iterations without timing them
Since my CPU does not support overclocking, the first thing I do is read the assembly, which is the most deterministic way to identify performance differences. You can isolate the code into separate functions, annotate them with #[unsafe(no_mangle)], install cargo-asm, and run cargo asm --release. You can pass target cpu native to see full wide SIMD or potential other optimization. Or passing target CPU flags to cross compile, for example inspecting x86 64 assembly from ARM 64 (I did this from Termux). Even better, you can use Godbolt, as it will point out which specific line of code produced each line of assembly
From there, you can determine which parts can be optimized. For instance, you may frequently see the compiler fail to remove bounds checks because the values are evaluated at runtime, then you can apply the optimization like if you are certain the value will always stay within the bounds, you can use unsafe get_unchecked. You might also see that the code is not compiled into SIMD instructions, so you can edit it, like what I mostly do is removing branching code inside a loop until it stops generating jump instructions and vectorizes properly. If SIMD only utilizes half of the CPU's SIMD lanes, you can try passing target native or doing a manual unroll with fixed chunk sizes, though you should watch out because manual unroll can sometimes spill to the stack in different SIMD lanes (use the target flag to cross compile to inspect it) because stack is slower. Based on the assembly after all optimizations, you can also determine if now the execution becomes memory bound, then changing allocation strategy and allocator, trying memory prefetching, or configure NUMA binding. If you are ever confused by the assembly, simply paste it into AI tool and ask for a line by line breakdown, over time you will understand it naturally, which is how I learned
However, in a large codebase, you still need to profile it first to know which specific section of code requires assembly inspection. That is where I struggle, as I have not found an easy to use profiling tool for Rust, and existing tools produce graphs that are difficult for beginners to interpret. I think the tooling team and ecosystem should focus on creating easy to use profiling and benchmarking tools that yield easily understandable results, stay resilient against measurement noise, and support all async runtimes. Investing in these tools will ensure optimization actually produce expected results. Because bad profiling and benchmarking tools will misguide optimizations
The second is using Perf (Linux) metrics that are unaffected by CPU frequency scaling, such as branch misses, cache misses, CPU migrations, false sharing, etc
The third is, increasing the input data size and the total run so the workload is bigger and then the different can also be seen bigger, making it is easier to determine