As a note, small {collection} optimization is less necessary in Rust than it is in a language like C++. C++ has default copy semantics, so passing around std::string as a function argument is safe (protecting you from shared mutation and/or use-after-move errors) but results in a lot of extra string copies. When you have a bunch of copies and most strings are ≤22 bytes, thus fitting in the stack space necessary for the string already (on 64bit targets), you get a huge benefit from making those string copies into trivial copies instead of heap duplication.
But in Rust, &str existed from day one, gets used by default instead of passing String by value, and does not suffer from requiring developer enforcement of lifetimes. Furthermore, when String is passed by value because indefinite ownership is required, it's an ownership transferring trivial move, and duplicating the heap memory requires a .clone() call.
Even comparing to a runtime for a scripting language like JavaScript or Lua, there roughly everything is a hashtable, so optimizing simple objects to not use the full dynamic machinery is a large cross-cutting performance improvement. But in Rust, those simple types won't be maps, they'll be their own static types, so collection types don't have to be optimized for the tiny use case.
It would be great if we could have e.g. Vec<T, SmallStorage<23>> and such so those cases that do benefit from small {collection} optimization can do so without abandoning the standard library's well tested API.
While this is a minor storage optimization, note that this likely defeats most peephole (i.e. LLVM) optimization, as now optimization can't easily track and know capacity information. Since the allocator is dynamically linked, its reported capacity might even change between queries, as far as the optimizations know!
Furthermore, Rust has made a conscious (and imho correct) choice for global general purpose de/allocation to always provide the size/align of the allocated slot. Any cooperation with the allocator would need to happen with some Vec<T, A> which is not Vec<T, Global>, and the guarantee only applies to the latter instantiation.
It is a form of it, yes. When you exceed the allocated inline space, all of the map is transferred to the heap. The Rust enum overlaps the Heap and Inline case, using an invalid value of one to mark when the other is in use so the size equals that of the larger variant.
Because Rust is statically typed, types must have a fixed stack size. HashMap is currently 6Ă—usize. You'll be hard-pressed to fit more than one key/value pair in that space. If you have a use case where most of your maps only have one int-string entry... maybe you shouldn't be using the general purpose stdlib HashMap.
As for layout optimization within the heap, hashbrown (the backing hashtable implementation for std) implements the SwissTable implementation, storing roughly Box<dyn<N: usize> ([(K, V); N], [CtrlByte; N])>. Rust was built with a default capability for trivial destructive moves, so there's nowhere near the default present in e.g. the C++ STL of defensively boxing things to provide address stability and guarantee cheap relocation (of the pointer in the collection).