Какова цель дополнительного ldnull и хвоста. в реализации F # против С#?

Следующая функция С#:

T ResultOfFunc<T>(Func<T> f)
{
    return f();
}

неудивительно компилирует это:

IL_0000:  ldarg.1     
IL_0001:  callvirt    05 00 00 0A 
IL_0006:  ret  

Но эквивалентная функция F #:

let resultOfFunc func = func()

компилируется:

IL_0000:  nop         
IL_0001:  ldarg.0     
IL_0002:  ldnull      
IL_0003:  tail.       
IL_0005:  callvirt    04 00 00 0A 
IL_000A:  ret 

(Оба режима находятся в режиме освобождения). В начале есть дополнительный nop, который мне неинтересно, но интересная вещь - дополнительные инструкции ldnull и tail..

Мое предположение (возможно, неверное) заключается в том, что ldnull необходимо, если функция void, поэтому она все равно возвращает что-то (unit), но это не объясняет, что является целью tail. инструкция. И что произойдет, если функция действительно толкает что-то в стеке, не застрял ли он с лишним нулем, который не появляется?

Ответ 1

Варианты С# и F # имеют важное различие: функция С# не имеет никаких параметров, но версия F # имеет один параметр типа unit. Это значение unit - это то, что отображается как ldnull (поскольку null используется как представление единственного значения unit, ()).

Если вы должны были перевести вторую функцию на С#, она выглядела бы так:

T ResultOfFunc<T>( Func<Unit, T> f ) {
   return f( null );
}

Что касается команды .tail - это так называемая "оптимизация хвостового вызова".
Во время обычного вызова функции обратный адрес попадает в стек (стек процессора), а затем вызывается функция. Когда функция выполняется, она выполняет команду "return", которая выталкивает обратный адрес из стека и передает туда контроль.
Однако, когда функция A вызывает функцию B, а затем сразу возвращает функцию B возвращаемое значение, не делая ничего другого, CPU может пропустить нажатие дополнительного адреса возврата в стеке и выполнить "переход" на B вместо "вызова". Таким образом, когда B выполняет команду "return", CPU выдает адрес возврата из стека, и этот адрес указывает не на A, а на того, кто первым вызвал A.
Другой способ подумать об этом: function A вызывает функцию B не перед возвратом, а вместо возвращения, и, таким образом, делегирует честь вернуться к B.

Таким образом, эта магическая техника позволяет нам делать вызов, не потребляя пятно в стеке, а это означает, что вы можете выполнять произвольно много таких вызовов, не рискуя переполнением стека. Это очень важно в функциональном программировании, поскольку оно позволяет эффективно внедрять рекурсивные алгоритмы.

Он называется "tail call", потому что вызов B происходит, так сказать, в хвосте A.