Мы структуру возвращаем во временный объект. Но опять же следует
разделить, то что как бы понарошку, и что получается на самом деле.
Этот временный объект -- он существует только в голове у тебя,
компилятор там как-то аллоцирует регистры и куда-то положит лучше
тебя, не твоя забота. И этот временный объект живёт до точки с
запятой (';'), потом уничтожается. Ты можешь его присвоить именованной переменной ( struct S = f() ), или можешь сразу адресовать поля этого объекта ( f().a ). В первом случае дабы оптимизировать лишние перекладывания байтов туда-сюда-обратно компилятор может подогнать адрес var так, -тоб она лежала прямо в месте временного объекта. 0 тактов и всё решено. Но если ты так в цикле будешь делать с двумя структурами, ясно, уже так не получится, пример:
Compiler Explorer
Видно, что компилятор вынужден перекладывать возвращаемые значения в другие ячейки, и работа с возвратом значения в структуру переданную по ссылке здесь была бы эффективней.
То-есть возврат структур по-значению хорошо работает в тривиальных случаях, когда возвращённы значения тут же используются, например. Или когда по крайней мере результат кладётся во вновь продекларированную переменную (а не использованную ранее: тогда можно постоянно заводить на стеке новые временные значения и двигать его вниз). А когда приходится переписывать возвращаемым значением уже ранее существующую переменную (пример с циклом), или когда вершина стека может быть занята другими вычислениями (опять же цикл не позволяет двигать стек вниз) -- случай нетривиальный и возврат структуры по значению не эффективен.