Эффективна ли вложенная функция?
В языках программирования, таких как Scala или Lua, мы можем определить вложенные функции, такие как
function factorial(n)
function _fac(n, acc)
if n == 0 then
return acc
else
return _fac(n-1, acc * n)
end
end
return _fac(n, 1)
end
Этот подход вызывает какую-либо неэффективность, потому что экземпляр вложенной функции определяется или создается каждый раз при вызове внешней функции?
Ответы
Ответ 1
Этот подход вызывает любую неэффективность, поскольку вложенная функция экземпляр определяется или создается каждый раз, когда мы вызываем внешний функция?
Эффективность - большая и широкая тема. Я предполагаю, что с помощью "неэффективного" вы имеете в виду "вызывает ли рекурсивно вызов метода каждый раз с накладными расходами"?
Я могу говорить только от имени Scala, а именно от вкуса, нацеленного на JVM, поскольку другие вкусы могут действовать по-разному.
Мы можем разделить этот вопрос на два, в зависимости от того, что вы на самом деле имели в виду.
Вложенные методы (локальная область) в Scala - это функция лексической области видимости, что означает, что они предоставляют вам доступ к внешним значениям метода, но как только мы выпускаем байт-код, они определяются на уровне класса, так же, как простая java метод.
Для полноты, знайте, что Scala также имеет значения функций, которые являются гражданами первого класса, что означает, что вы можете передавать их другим функциям, тогда они будут нести накладные расходы на распределение, поскольку они реализованы с использованием классов.
Факториал может быть записан хвостовым рекурсивным образом, как вы его написали в своем примере. Компилятор Scala достаточно интеллектуальный, так что он заметит, что ваш метод рекурсивно хвост и превращает его в итеративный цикл, избегая вызова метода для каждой итерации. Он также может, если это окажется возможным, попытаться встроить метод factorial
, избегая накладных расходов на дополнительное распределение кадров стека.
Например, рассмотрим следующую факториальную реализацию в Scala:
def factorial(num: Int): Int = {
@tailrec
def fact(num: Int, acc: Int): Int = num match {
case 0 => acc
case n => fact(n - 1, acc * n)
}
fact(num, 1)
}
На первый взгляд мы имеем рекурсивный метод. Посмотрим, как выглядит байт-код JVM:
private final int fact$1(int, int);
Code:
0: iload_1
1: istore 4
3: iload 4
5: tableswitch { // 0 to 0
0: 24
default: 28
}
24: iload_2
25: goto 41
28: iload 4
30: iconst_1
31: isub
32: iload_2
33: iload 4
35: imul
36: istore_2
37: istore_1
38: goto 0
41: ireturn
Мы видим здесь, что рекурсия превратилась в итеративный цикл (a tablewitch + инструкция перехода).
Что касается создания экземпляра метода, если наш метод не был хвостовым рекурсивным, время выполнения JVM должно было бы интерпретировать его для каждого вызова, пока C2 compiler находит достаточным такое, чтобы JIT скомпилировал его и снова использовал машинный код для каждого вызова метода.
В общем, я бы сказал, что это не должно вас беспокоить, если вы не заметили, что метод находится на пути к вашему горячему пути и профилирование кода побудило вас задавать этот вопрос.
В заключение, эффективность - очень тонкая, конкретная тема для конкретного случая. Я думаю, что нам не хватает информации, чтобы сказать вам, из упрощенного примера, который вы предоставили, если это лучший вариант для вашего варианта использования. Я говорю еще раз, если это не то, что появилось на вашем профилировщике, не беспокойтесь об этом.
Ответ 2
Позвольте проверить его в Lua с/без вложенных функций.
Вариант 1 (внутренний функциональный объект создается при каждом вызове)
local function factorial1(n)
local function _fac1(n, acc)
if n == 0 then
return acc
else
return _fac1(n-1, acc * n)
end
end
return _fac1(n, 1)
end
Вариант 2 (функции не вложены)
local function _fac2(n, acc)
if n == 0 then
return acc
else
return _fac2(n-1, acc * n)
end
end
local function factorial2(n)
return _fac2(n, 1)
end
Код бенчмаркинга (рассчитать 12!
10 млн. раз и отобразить время работы процессора в секундах):
local N = 1e7
local start_time = os.clock()
for j = 1, N do
factorial1(12)
end
print("CPU time of factorial1 = ", os.clock() - start_time)
local start_time = os.clock()
for j = 1, N do
factorial2(12)
end
print("CPU time of factorial2 = ", os.clock() - start_time)
Выход для Lua 5.3 (интерпретатор)
CPU time of factorial1 = 8.237
CPU time of factorial2 = 6.074
Выход для LuaJIT (JIT-компилятор)
CPU time of factorial1 = 1.493
CPU time of factorial2 = 0.141
Ответ 3
Ответ зависит, конечно, от языка.
В частности, что происходит в Scala, это то, что внутренние функции скомпилированы, поскольку они стояли вне сферы действия функции, в которой они определены.
Таким образом, язык позволяет вам вызывать их из лексической области, где они определены, но на самом деле не создает экземпляр функции несколько раз.
Мы можем легко проверить это, скомпилировав два варианта
Первый - довольно верный порт вашего кода Lua:
class Function1 {
def factorial(n: Int): Int = {
def _fac(n: Int, acc: Int): Int =
if (n == 0)
acc
else
_fac(n-1, acc * n)
_fac(n, 1)
}
}
Второй вариант более или менее одинаковый, но хвостовая рекурсивная функция определена вне области factorial
:
class Function2 {
def factorial(n: Int): Int = _fac(n, 1)
private final def _fac(n: Int, acc: Int): Int =
if (n == 0)
acc
else
_fac(n-1, acc * n)
}
Теперь мы можем скомпилировать эти два класса с помощью scalac
, а затем использовать javap
, чтобы посмотреть вывод компилятора:
javap -p Function*.scala
который даст следующий выход
Compiled from "Function1.scala"
public class Function1 {
public int factorial(int);
private final int _fac$1(int, int);
public Function1();
}
Compiled from "Function2.scala"
public class Function2 {
public int factorial(int);
private final int _fac(int, int);
public Function2();
}
Я добавил ключевые слова private final
, чтобы свести к минимуму разницу между ними, но главное заметить, что в обоих случаях определения появляются на уровне класса, причем внутренние функции автоматически определяются как private
и final
и с небольшим украшением, чтобы гарантировать отсутствие класса имени (например, если вы определяете внутреннюю функцию loop
внутри двух разных).
Не уверен относительно Lua или других языков, но я могу ожидать, что, по крайней мере, большинство скомпилированных языков используют подобный подход.
Ответ 4
Да (или он использовался), о чем свидетельствует попытка Lua повторного использования значений функций, когда выполнение проходит через определение функции несколько раз.
Lua 5.2 Изменения
Равенство между значениями функций изменилось. Теперь функция определение не может создавать новое значение; он может повторно использовать некоторые предыдущие значение, если нет заметной разницы с новой функцией.
Поскольку вы закодировали (предположив Lua) функцию, назначенную глобальному или локальному, объявленному в более высокой области, вы можете закодировать короткое замыкание самостоятельно (предполагая, что никакой другой код не устанавливает его на что-либо иное, кроме nil
или false
):
function factorial(n)
_fac = _fac or function (n, acc)
…
end
…
end
Ответ 5
Я не знаю о lua, но в Scala очень распространены и используются в рекурсивных функциях для обеспечения оптимальной оптимизации:
def factorial(i: Int): Int = {
@tailrec
def fact(i: Int, accumulator: Int): Int = {
if (i <= 1)
accumulator
else
fact(i - 1, i * accumulator)
}
fact(i, 1)
}
Дополнительная информация о хвостохранилище и рекурсии здесь