Приводит ли использование карри к снижению производительности в F #? - PullRequest
0 голосов
/ 28 ноября 2018

При написании функции, которая может принимать карри, вы можете написать ее как функцию с одним аргументом, которая возвращает функцию.Например,

let add x =
    let inner y = x + y
    inner

Так что вы можете сделать:

add 3 4

или:

let add3 = add 3
add3 4

Мой вопрос, потому что вы возвращаете функцию, выконцептуально вызывая функцию дважды (внешняя функция и внутренняя функция).Это медленнее, чем:

let add x y = x + y

или компилятор оптимизирует вызовы add 3 4 в определении карри?

Ответы [ 2 ]

0 голосов
/ 29 ноября 2018
let f x   = fun y -> x + y
let g x y = x + y

Просмотр этих определений функций в dnSpy для оптимизированной сборки показывает, что они выглядят следующим образом:

public static int f(int x, int y)
{
    return x + y;
}

public static int g(int x, int y)
{
    return x + y;
}

Это не так уж странно, поскольку g на самом деле является кратким определением для f который является общим случаем.В F # -подобных языках функции концептуально всегда принимают одно значение, возвращающее одно значение.Значения могут быть функциями.Это легче увидеть, если один из этих пунктов подписи функции для f и g

val f: int -> int -> int
// Actually is
// val f: int -> (int -> int)
// ie f is a function that takes a single int and returns a function that takes a single int and returns an int.

Чтобы заставить F # выполняться быстрее в .NET, физическое представление f в сборке:

public static int f(int x, int y)

Хотя это более естественное представление функции F #.

public static Func<int, int> f(int x)

будет работать плохо, хотя.

Обычно F # достаточно умен, чтобы избежатьнакладные расходы на абстракцию при оптимизации, как указано выше, и при вызове.Однако существуют ситуации, когда F # не может оптимизировать для вас.

Представьте, что вы реализуете fold

let rec fold f s vs =
  match vs with
  | v::vs -> fold f (f s v) vs
  | []    -> s

Здесь F # не может полностью оптимизировать f s v.Причина в том, что f может иметь более сложную реализацию, чем указанная выше, которая может возвращать другую функцию в зависимости от s.

Если вы посмотрите на dnSpy, вы заметите, что F # вызывает функцию, используя InvokeFast но это делает внутренний тест, чтобы увидеть, может ли он быть вызван быстро.Затем мы делаем этот тест для каждого значения, даже если это одна и та же функция.

По этой причине иногда можно увидеть fold, написанное так:

let fold f s vs =
  let f = OptimizedClosures.FSharpFunc<_, _, _>.Adapt f
  let rec loop s vs =
    match vs with
    | v::vs -> loop (f.Invoke (s, v)) vs
    | []    -> s
  loop s vs

Adapt здесь проверяется перед циклом, если f действительно может быть оптимизирован, а затем возвращает эффективный адаптер.В общем случае это все еще может быть немного медленнее, но тогда именно это и задумал вызывающий объект.

Примечание;это потенциальное снижение производительности не происходит для простых значений функций, таких как 'T -> 'U.Это всегда может быть эффективно использовано.

Надеюсь, это поможет.

0 голосов
/ 28 ноября 2018

Я проверял это в LINQPad 5 .

Когда оптимизация компилятора отключена, компилятор F # будет выдавать разные IL для каждого фрагмента.Другими словами, если происходит какая-либо оптимизация, она оставляется на усмотрение JITter, и вполне может быть медленнее вызывать первую форму.

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

add 3 4

дает IL-эквивалент жестко-кодированного 7, при этом весь вызов функции оптимизирован:

ldc.i4.7

В другихИными словами, компилятор F # довольно тщательно подходит для оптимизации логически идентичных блоков кода.

Конечно, это не исчерпывающий ответ, и в некоторых случаях компилятор может по-разному трактовать их.

...