Looking at the code more carefully, I'd actually bet its string that's slow, and not actually map.
unordered_map is actually known to have relatively bad performance on GCC and some other open-source implementations. I don't know if there's a spec-problem that favors a slower-hash table (rather than the modern open-addressing scheme, which is faster on modern computers), but I can believe unordered_map has an issue on common standard libraries today.
------------
> ret[string("id.")+to_string(i%500000)] = string("val.")+to_string(i);
Lets count the mallocs.
> string("id.")
One
> to_string(i%500000)
Two
> string("id.")+to_string(i%500000)
Three, then two frees() as the previous two strings are deallocated.
> ret[string("id.")+to_string(i%500000)]
Four. The ret[] operator searches for the string. Since it doesn't exist, it now has to malloc the key and insert the value in there. std::move() might solve that issue and cut this one out easily?
> string("val.")
Five
> to_string(i)
Six
> string("val.")+to_string(i)
Seven + 2x frees.
> ret[string("id.")+to_string(i%500000)] = string("val.")+to_string(i);
Eight, to place the value in there. Again, probably solved with std::move. +1 more free.
So eight mallocs and 6 frees?
---------
Yeah, C++ strings shoot yourself in the foot performance-wise. But they're easy to use and free of stupid malloc/free bugs, but it means that they will malloc/free a LOT more than you might guess with the untrained eye.
I know that Perl / PHP / etc. etc. were explicitly designed to be more favorable towards string manipulation. And I'd bet that there's some old C++98 stuff that might lock in the performance of string to this older, less performant model.
EDIT: I see you managed to avoid the problem of excessive mallocs/frees in the Rust code with "format". The C++ equivalent might be snprintf, though that dips down into C-style strings.