My understanding is that 2. and 3. are essentially the same
No, in fact 1. and 2. are essentially the same, but 3. is different.
In both 1. and 2. my_string is an lvalue, since names of variables are always lvalues. In both cases the string object lives until the end of the scope. In case of 1. because that is the scope of the object and in 2. because of the lifetime extension rules on reference binding.
In 3. foo() is a prvalue (a kind of rvalue). The temporary object materialized from it and to which the reference parameter of the function binds lives until the end of the initialization of x.
So if bar had different overloads taking lvalue references or rvalue references, in 1. and 2. the lvalue reference overload would be chosen and in 3. the rvalue reference overload.
Since C++17, none of the three methods make unnecessary copies. There will be only one std::string object, either the temporary one in case of 2. and 3. or the named one in case of 1.
Before C++17, there could be an additional copy in case of 1. to copy from the return value of foo into my_string. However the compiler was allowed to elide this copy and that probably almost always happened in practice. Since C++17 this elision is mandatory.
while in 2., I tell the compiler that I don't need my_string any more after passing it to bar,
Aside from this applying to 3., not 2., the compiler actually doesn't care about this at all, the lifetime of the object is not affected.
It is the function you are passing to that cares. If a function takes an rvalue reference, by convention, that means that the function is allowed to use and modify the object's state in whatever way it suits it. Your function doesn't have a rvalue overload, so it doesn't matter how you pass the string to it. bar should never modify the state of the object.
So to make the distinction relevant to your case, you would either add an overload
int bar(std::string&&);
that will be called instead of the first overload for 3. and somehow makes use of the implied permission to put the string object into an unspecified state, or you would use a single overload
int bar(std::string);
in which case before C++17 the parameter will be constructed through the copy constructor for 1. and 2. or the move constructor for 3. The move constructor makes use of the rvalue convention and will reuse the passed string's allocations. Or in case of 3., since C++17 definitively and optionally before that, the copy/move is elided and the parameter is directly constructed by the function foo().