You are on page 1of 50

Как поймать рекурсию


за хвост
Владимир Парфиненко, Excelsior @ Huawei
telegram/twitter: @cypok

How to catch recursion by the tail


Beauty of recursion

2
Stack overflow happens

int factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}

3
Stack overflow happens

int factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}

3
Tail recursion to the rescue!

int factorial(int n) {
return factorialTR(n, 1);
}

int factorialTR(int n, int acc) {


if (n <= 1) return acc;
return factorialTR(n - 1, acc * n);
}

4
Tail recursion to the rescue!

int factorial(int n) {
return factorialTR(n, 1);
}

int factorialTR(int n, int acc) {


if (n <= 1) return acc;
return factorialTR(n - 1, acc * n);
}

4
Tail recursion to the rescue!

int factorial(int n) {
return factorialTR(n, 1);
}

int factorialTR(int n, int acc) {


if (n <= 1) return acc;
return factorialTR(n - 1, acc * n);
}

4
Tail Recursion Optimization (TRO)

int factorialTR(int n, int acc) {

if (n <= 1) return acc;

return factorialTR(n - 1, acc * n);

5
Tail Recursion Optimization (TRO)

int factorialTR(int n, int acc) {


start:
if (n <= 1) return acc;

acc = acc * n; n = n - 1;
goto start;
}

5
Tail Recursion Optimization (TRO)

int factorialTR(int n, int acc) {


while (true) {
if (n <= 1) return acc;

acc = acc * n; n = n - 1;
}
}

5
IDEA inspection

6
IDEA inspection

6
Some science in IDEA
static int gcdI(int n, int m) {
while (true) {
if (m == 0) return n;
n = m;
m = n % m;
}
static int gcd(int n, int m) { }
if (m == 0) return n;
return gcd(m, n % m); static int gcdI(int n, int m) {
} while (true) {
if (m == 0) return n;
int n1 = n;
n = m;
m = n1 % m;
}
}

7
IDEA inspection is not a panacea
static void countDown(int start) {
if (start < 0) return;
System.out.println(start); IDEA-288086
countDown(start - 1);
}

static boolean contains(int n, Predicate<Integer> p) {


return n >= 0 && (p.test(n) || contains(n - 1, p)); IDEA-291810
}
if (n < 0) return false;
if (p.test(n)) return true;
return contains(n - 1, p);

int workHard() {
return workHard(); IDEA-288321
}

8
9
Bytecode transformation
static int triangular(int, int);
0: iload_0
1: iconst_1
2: if_icmpgt 7 // if (n <= 1)
5: iload_1
6: ireturn // return acc;
7: iload_0
8: iconst_1
9: isub // (n - 1)
10: iload_1
11: iload_0
12: iadd // (acc + n)
13: invokestatic triangular:(II)I
16: ireturn

10
Bytecode transformation
static int triangular(int, int);
0: iload_0
1: iconst_1
2: if_icmpgt 7 // if (n <= 1)
5: iload_1
6: ireturn // return acc;
7: iload_0
8: iconst_1
9: isub // (n - 1)
10: iload_1
11: iload_0
12: iadd // (acc + n)
13: istore_1 ibessonov/java-tailrec-agent

14: istore_0 Sipkab/jvm-tail-recursion


15: goto 0

10
Short circuit operators
0: getstatic weAreThere:Z
3: ifne 12
static volatile boolean weAreThere;

static boolean areWeThereYet() {


return weAreThere || areWeThereYet();
6: invokestatic areWeThereYet:()Z
}
9: ifeq 16

static boolean areWeThereYet();


0: getstatic weAreThere:Z
6: invokestatic areWeThereYet:()Z
9: ior 12: iconst_1
10: ireturn 13: goto 17

16: iconst_0

17: ireturn

11
12
Optimization predictability

@tailrec def triangular(n: Int, acc: Int): Int = {


if (n <= 1) return acc
triangular(n - 1, acc + n)
}

static int triangular(int n, int acc) {


if (n <= 1) return acc;
return triangular(n - 1, acc + n);
}

tailrec fun triangular(n: Int, acc: Int): Int {


if (n <= 1) return acc
return triangular(n - 1, acc + n)
}

13
Non-annotated tail-recursive functions

def workHard(): Unit = { fun workHard() {


workHard() workHard()
} }

void workHard(); final void workHard();


0: goto 0 0: aload_0
1: invokevirtual workHard:()V
4: return
discuss.kotlinlang.org/t/3-tailrec-questions/3981

14
Annotated non-tail-recursive functions

@tailrec def a(m: Int, n: Int): Int = tailrec fun a(m: Int, n: Int): Int =
if (m < 1) n + 1 if (m < 1) n + 1
else if (n < 1) a(m - 1, 1) else if (n < 1) a(m - 1, 1)
else a(m - 1, a(m, n - 1)) else a(m - 1, a(m, n - 1))

Error: could not optimize @tailrec Warning: Recursive call is not a tail call
annotated method a:
it contains a recursive call
not in tail position
tailrec open fun workHard() { KT-18533
workHard()
}
Error: Tailrec is not allowed on open members

15
Short-circuit operators

@tailrec def areWeThereYet(): Boolean = tailrec fun areWeThereYet(): Boolean =


weAreThere || areWeThereYet() weAreThere || areWeThereYet()

Warning: Recursive call is not a tail call


boolean areWeThereYet();
0: aload_0
1: invokevirtual weAreThere:()Z boolean areWeThereYet(); KT-24151
4: ifne 10 0: aload_0
7: goto 0 1: astore_1
10: iconst_1 2: aload_1
11: goto 14 3: astore_2
14: ireturn 4: aload_2
5: getfield weAreThere:Z
8: ifne 16
11: aload_2
12: astore_1
13: goto 2
16: iconst_1
17: ireturn

16
This substitution

class MyList(value: Int, next: MyList) {

@tailrec
final def printAll(): Unit = {
println(value) KT-15341
if (next != null) next.printAll()
}

17
Kotlin’s imperfection

KT-14389 in inlined lambda


KT-20075 implicit return

KT-27897 tailrec and inline

KT-31391 elvis operator

KT-38402 Nothing return type

KT-39500 in catch

18
19
Real solution for virtual calls
class A {

void workHard() {
/* virtual */ workHard();
}

20
Real solution for virtual calls
class A {

void workHard() {
if (this.getClass() == A.class) {
/* special */ workHard();
} else {
/* virtual */ workHard();
}
}

20
Real solution for virtual calls
class A {

void workHard() {
while (true) {
if (this.getClass() == A.class) {
continue;
} else {
/* virtual */ workHard();
}
}
}

20
Real solution for virtual calls
class A {

void workHard() {
if (this.getClass() == A.class) {
while (true) {
// infinite loop
}
} else {
/* virtual */ workHard();
}
}

20
Real solution for virtual calls
class A {

void workHard() {
if (this.vmt[idx] == A::workHard) {
while (true) {
// infinite loop
}
} else {
/* virtual */ workHard();
}
}

class B extends A { }

20
Real solution for virtual calls
class A {

void workHard() {
if (this.vmt[idx] == A::workHard) {
while (true) {
// infinite loop
}
} else {
/* virtual */ workHard();
}
}

class B extends A {
void workHard() { /* ... */ }
}
20
JVMs with TRO

✘ HotSpot, GraalVM

Консервативность? softwareengineering.stackexchange.com/a/272086

✔ OpenJ9

21
22
23
eclipse-openj9/openj9#1126

24
JVMs with TRO

✘ HotSpot, GraalVM

Консервативность? softwareengineering.stackexchange.com/a/272086

✔ OpenJ9

Есть проблемы с предсказуемостью



(иногда помогают -XX:-EnableHCR или -Xjit:optlevel=warm)

25
JVMs with TRO

✘ HotSpot, GraalVM

Консервативность? softwareengineering.stackexchange.com/a/272086

✔ OpenJ9

Есть проблемы с предсказуемостью



(иногда помогают -XX:-EnableHCR или -Xjit:optlevel=warm)

✔ Excelsior JET

Есть проблемы с закрытием в 2019

25
Real motivation for TRO

bit.ly/cypok-tailrec-bench
26
Stack traces problem
static int fact(int n, int acc) { static int fact(int n, int acc) {
if (n <= 1) return acc; while (true) {
return fact(n - 1, Math.multiplyExact(n, acc)); if (n <= 1) return acc;
} acc = Math.multiplyExact(n, acc);
n = n - 1;
}
}

Exception in thread "main" Exception in thread "main"


java.lang.ArithmeticException: integer overflow java.lang.ArithmeticException: integer overflow
at java.lang.Math.multiplyExact(Math.java:964) at java.lang.Math.multiplyExact(Math.java:964)
at Main.fact(Main.java:8) at Main.fact(Main.java:9)
at Main.fact(Main.java:8) at Main.main(Main.java:72)
at Main.fact(Main.java:8)
at Main.fact(Main.java:8)
at Main.fact(Main.java:8)
at Main.fact(Main.java:8)
at Main.fact(Main.java:8)
at Main.fact(Main.java:8)
at Main.fact(Main.java:8)
at Main.fact(Main.java:8)
at Main.fact(Main.java:8)
at Main.main(Main.java:69)

27
What spec says?

/**
* ...
* Some virtual machines may, under some
* circumstances, omit one or more stack JAVA ПОЗВОЛЯЕТ
* frames from the stack trace. ВЫКИДЫВАТЬ ФРЕЙМЫ JVM ЖЕ ТАК НЕ
* In the extreme case, a virtual machine ИЗ СТЕК ТРЕЙСОВ. ДЕЛАЮТ, ВЕРНО?
* that has no stack trace information
* concerning this throwable is permitted
* to return a zero-length array from
* this method.
* ...
*/
public StackTraceElement[] getStackTrace()

ВЕРНО?

28
Everything has its limit

Exception in thread "main" java.lang.StackOverflowError static void foo() { bar(); }


at Main.baz(Main.java:66) static void bar() { baz(); }
at Main.bar(Main.java:65) static void baz() { foo(); }
at Main.foo(Main.java:64)
at Main.baz(Main.java:66)
. . .
at Main.bar(Main.java:65)
at Main.foo(Main.java:64)
at Main.baz(Main.java:66)

HotSpot’s

-XX:MaxJavaStackTraceDepth=1024

29
Everything has its limit

Exception in thread "main" java.lang.StackOverflowError static void foo() { bar(); }


at Main.baz(Main.java:66) static void bar() { baz(); }
at Main.bar(Main.java:65) static void baz() { foo(); }
at Main.foo(Main.java:64)
at Main.baz(Main.java:66)
. . .
at Main.bar(Main.java:65)
at Main.foo(Main.java:64)
at Main.baz(Main.java:66)
. . .
at Main.baz(Main.java:66)
HotSpot’s

at Main.bar(Main.java:65)
at Main.foo(Main.java:64) -XX:MaxJavaStackTraceDepth=0
at Main.main(Main.java:81)

29
Everything has its limit

Exception in thread "main" java.lang.StackOverflowError static void foo() { bar(); }


static void bar() { baz(); }
static void baz() { foo(); }

HotSpot’s

-XX:-StackTraceInThrowable

29
Not so exceptional exception

for (...) { java.lang.ArithmeticException: / by zero


int a = random.nextInt(1_000_000); at Main.benchDiv(Main.java:86)
int b = random.nextInt(1_000_000); at Main.main(Main.java:102)
try { java.lang.ArithmeticException: / by zero
blackHole = a / b; at Main.benchDiv(Main.java:86)
} catch (ArithmeticException e) { at Main.main(Main.java:102)
e.printStackTrace(); java.lang.ArithmeticException: / by zero
} at Main.benchDiv(Main.java:86)
} at Main.main(Main.java:102)
java.lang.ArithmeticException
java.lang.ArithmeticException
java.lang.ArithmeticException
java.lang.ArithmeticException
java.lang.ArithmeticException
. . .

30
Not so exceptional exception

for (...) { java.lang.ArithmeticException: / by zero


int a = random.nextInt(1_000_000); at Main.benchDiv(Main.java:86)
int b = random.nextInt(1_000_000); at Main.main(Main.java:102)
try { java.lang.ArithmeticException: / by zero
blackHole = a / b; at Main.benchDiv(Main.java:86)
} catch (ArithmeticException e) { at Main.main(Main.java:102)
e.printStackTrace(); java.lang.ArithmeticException: / by zero
} at Main.benchDiv(Main.java:86)
} at Main.main(Main.java:102)
java.lang.ArithmeticException: / by zero
at Main.benchDiv(Main.java:86)
at Main.main(Main.java:102)
java.lang.ArithmeticException: / by zero
HotSpot’s
 at Main.benchDiv(Main.java:86)
-XX:-OmitStackTraceInFastThrow at Main.main(Main.java:102)
. . .

30
JVM with TRO

static int fact(int n, int acc) {


if (n <= 1) return acc;
return fact(n - 1, Math.multiplyExact(n, acc));
}

Exception in thread "main" java.lang.ArithmeticException: integer overflow


at java.lang.Math.multiplyExact(Math.java:964)
at Main.fact(Main.java:9)
at Main.main(Main.java:72)

31
32
32
The end.
Владимир Парфиненко, Excelsior @ Huawei
telegram/twitter: @cypok

You might also like