"JVM не підтримує оптимізацію зворотного виклику, тому я прогнозую багато вибухаючих стеків"
Той, хто говорить це (1), не розуміє оптимізацію хвостових викликів, або (2) не розуміє JVM, або (3) обидва.
Почну з визначення хвостових дзвінків з Вікіпедії (якщо вам не подобається Вікіпедія, ось альтернатива ):
В інформатиці хвостовий виклик - це виклик підпрограми, який відбувається всередині іншої процедури як остаточної дії; він може створити повернене значення, яке потім негайно повертається процедурою виклику
У наведеному нижче коді заклик до bar()
хвостового виклику foo()
:
private void foo() {
// do something
bar()
}
Оптимізація зворотного дзвінка відбувається, коли мовна реалізація, бачачи хвостовий виклик, не використовує звичайний виклик методу (який створює фрейм стека), а натомість створює гілку. Це оптимізація, тому що кадр стека потребує пам'яті, і він вимагає, щоб цикли процесора підштовхували інформацію (наприклад, адресу повернення) на кадр, і тому, що пара виклику / повернення вимагає більше циклів процесора, ніж безумовний стрибок.
ТСО часто застосовують для рекурсії, але це не єдине його використання. Він також не застосовується до всіх рекурсій. Наприклад, простий рекурсивний код для обчислення факторіалу не може бути оптимізований для виклику хвоста, тому що останнє, що відбувається в функції, - це операція множення.
public static int fact(int n) {
if (n <= 1) return 1;
else return n * fact(n - 1);
}
Щоб здійснити оптимізацію хвостових викликів, вам потрібно дві речі:
- Платформа, яка підтримує розгалуження, крім викликів підпрограми.
- Статичний аналізатор, який може визначити, чи можлива оптимізація виклику хвоста.
Це воно. Як я вже зауважував, JVM (як і будь-яка інша архітектура Тьюрінга) має гото. Це, мабуть, безумовна перехідна програма , але функціональність може бути легко реалізована за допомогою умовної гілки.
Елемент статичного аналізу - це те, що хитро. У межах однієї функції це не проблема. Наприклад, ось рекурсивна функція Scala для підсумовування значень у List
:
def sum(acc:Int, list:List[Int]) : Int = {
if (list.isEmpty) acc
else sum(acc + list.head, list.tail)
}
Ця функція перетворюється на наступний байт-код:
public int sum(int, scala.collection.immutable.List);
Code:
0: aload_2
1: invokevirtual #63; //Method scala/collection/immutable/List.isEmpty:()Z
4: ifeq 9
7: iload_1
8: ireturn
9: iload_1
10: aload_2
11: invokevirtual #67; //Method scala/collection/immutable/List.head:()Ljava/lang/Object;
14: invokestatic #73; //Method scala/runtime/BoxesRunTime.unboxToInt:(Ljava/lang/Object;)I
17: iadd
18: aload_2
19: invokevirtual #76; //Method scala/collection/immutable/List.tail:()Ljava/lang/Object;
22: checkcast #59; //class scala/collection/immutable/List
25: astore_2
26: istore_1
27: goto 0
Зверніть увагу goto 0
на кінець. Для порівняння, еквівалентна функція Java (яка повинна використовувати a Iterator
для імітації поведінки розбиття списку Scala на голову та хвіст) перетворюється на наступний байт-код. Зверніть увагу , що останні дві операції тепер Invoke , а потім явним поверненням значення , отримане з допомогою цього рекурсивного виклику.
public static int sum(int, java.util.Iterator);
Code:
0: aload_1
1: invokeinterface #64, 1; //InterfaceMethod java/util/Iterator.hasNext:()Z
6: ifne 11
9: iload_0
10: ireturn
11: iload_0
12: aload_1
13: invokeinterface #70, 1; //InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
18: checkcast #25; //class java/lang/Integer
21: invokevirtual #74; //Method java/lang/Integer.intValue:()I
24: iadd
25: aload_1
26: invokestatic #43; //Method sum:(ILjava/util/Iterator;)I
29: ireturn
Оптимізація однієї функції виклику хвоста тривіальна: компілятор може побачити, що немає коду, який використовує результат виклику, тому він може замінити виклик а goto
.
Там, де життя стає складним, якщо у вас є кілька методів. Інструкції з розгалуження JVM, на відміну від процесорів загального призначення, таких як 80x86, обмежуються одним методом. Це все ще досить просто, якщо у вас є приватні методи: компілятор вільний вкладати ці методи за необхідності, тому може оптимізувати хвостові виклики (якщо вам цікаво, як це може працювати, розгляньте загальний метод, який використовує a switch
для керування поведінкою). Ви навіть можете поширити цю методику на кілька публічних методів в одному класі: компілятор накреслює органи методів, надає загальнодоступні методи мосту, а внутрішні виклики перетворюються на стрибки.
Але ця модель руйнується, коли ви розглядаєте публічні методи в різних класах, особливо з огляду на інтерфейси та навантажувачі класів. Компілятор рівня джерела просто не має достатньо знань для впровадження оптимізації хвостових викликів. Однак, на відміну від реалізацій «голого металу», * JVM (має інформацію для цього у формі компілятора Hotspot (принаймні, колишній компілятор Sun). Я не знаю, чи реально він працює оптимізація хвістних дзвінків, і підозрюваного немає, але це могло б .
Що підводить мене до другої частини Вашого запитання, яку я перефразую як "нам слід хвилюватись?"
Ясна річ, якщо ваша мова використовує рекурсію як єдиний примітив для ітерації, то вам все одно. Але мови, які потребують цієї функції, можуть її реалізувати; єдине питання полягає в тому, чи може компілятор для цієї мови створювати клас, який може викликати і викликати довільним класом Java.
Поза межами цього випадку я збираюся запросити молодих людей, сказавши, що це не має значення. Більшість рекурсивного коду, який я бачив (і я працював з великою кількістю графічних проектів) , не піддається оптимізації хвостових позицій . Як і простий фактор, він використовує рекурсію для побудови стану, а хвіст - комбінація.
Що стосується коду, який можна оптимізувати при виклику хвоста, часто перевести цей код в ітерабельну форму. Наприклад, та sum()
функція, яку я показав раніше, може бути узагальнена як foldLeft()
. Якщо ви подивитесь на джерело , то побачите, що воно насправді реалізується як ітеративна операція. Йорг W Міттаг мав приклад стану машини, реалізованої за допомогою викликів функцій; існує безліч ефективних (і ремонтоздатних) державних машинних реалізацій, які не покладаються на переклики функцій, що переводяться у стрибки.
Я закінчу щось зовсім інше. Якщо ви перейдете до виносок у SICP, ви можете опинитися тут . Я особисто вважаю, що це набагато цікавіше місце, ніж замінити мого компілятора JSR
на JUMP
.