These micro programs are very easy to JIT, so nearly identical performance is to be expected. It's when you get larger programs that C compilers with function inlining and better cache locality and whole program optimization leave JIT compiled languages in the dust. When you have a loop of a billion iterations the JIT compiler can instantly tell that it's worth optimizing whatever is in the loop body. When you have a…
Two things I see pretty consistently. First is that when there are no tall tent poles (20 functions take 5% of the time each), people don't know what to do, so they don't do anything. Second, and possibly easier to fix, is that people believe the perf analysis tool (the breakdown of where time is spent) is telling them the objective truth. Often it's wrong, which is why we try things, benchmark them, and revert changes if things get worse.
When there are no tall tent poles I switch to invocation count, which is the best secondary indicator of hotspots. There was one method that the perf tool told me was taking 10% of the run time. But the call count was fishy. Due to a bad call structure it was being called far more often than necessary. In the worst spot in the code two sequential calls were calling this function, so I flipped the code around so they could take the answer as an argument (memoization).
I reran the benchmarks. I had removed 50% of calls to a function that took 10% of our time, and the code overall was now 20% (twenty percent!) faster. Why?
Functions allocate memory. They evict cache lines in the data and instruction caches. They might even access constrained resources, like disk. And as you said, they change how the JIT decides to optimize things.
Sometimes, the symptom is that the code that runs immediately afterward gets blamed for problems they didn't create, and the profiler has no way of following the problem back to the root cause, so it assigns blame at the point of contention, not at the start of the contention.
The only tools I've found that helps with this are clean coding practices, and figuring out if your invocation counts match your expectations (I will run a call tree 100 times and then compare the call counts of everything to find things that were called 2+ times as often as they strictly should have been called)