Java JIT 是不是曾经优化递归方法调用?

Posted

技术标签:

【中文标题】Java JIT 是不是曾经优化递归方法调用?【英文标题】:Does the Java JIT ever optimize away recursive method calls?Java JIT 是否曾经优化递归方法调用? 【发布时间】:2021-11-15 06:56:30 【问题描述】:

我知道 Java 尚未添加尾调用消除优化,并打算将其作为 Project Loom 的后期添加。相反,我的问题是:JIT 是否会完全优化递归方法调用并将它们转换为迭代形式?名义上这似乎是可能的,但相对困难,所以我猜他们是否会在某些文档中进行强烈描述,但我正在努力追踪有关该主题的任何内容。

作为后续,如果 JIT 确实消除了某种形式的递归调用,超出了 Loom 中描述的形式,那么它如何出现在堆栈跟踪中?

【问题讨论】:

Loom 没有计划尾调用优化。 在显式标记的代码位置从堆栈中弹出几帧并不是人们通常理解的术语“尾调用优化”。所以另一个引用的句子是这里的相关句子,“实现自动尾调用优化不是这个项目的意图。” 我不知道为什么在要点中包含了尾调用,因为它们既不是必需的,也与提供虚拟线程和延续的实际目标无关。在那里看到它们令人惊讶,因为没有其他文件将它们命名为该项目的目标。该页面本身说“我们设想消除尾声”,这不像宣布实际计划 并且“实现”部分说“此功能的实现需要对 VM、VM 规范(字节码)以及可能的前端 Java 编译器 (javac) 进行横向更改.因此,为了不耽误 continuations 和 Fiber 的完成,我们只会在项目处于更高级阶段时才开始指定和实施此功能。”这实际上是在回答您的问题。跨度> 对我来说,它看起来更像是一个不错的选择,考虑到那里提到的严重问题,不太可能不依赖它的项目会因此而延迟。换句话说,我怀疑它会作为这个项目的一部分出现,而是“Loom 2”或完全不同的名称。但无论如何,那里提到的障碍解释了为什么目前没有消除尾声。哪个回答了你的问题。 【参考方案1】:

您的问题“它是如何出现在堆栈跟踪上的?”是 JVM 中尾调用优化的未解决问题之一。对 Java 代码的可管理性有一个期望,尤其是当代码在递归算法中花费很长时间(或者甚至处于无休止的递归中)并且开发人员想要了解发生了什么时。

所以简而言之,JVM(当前版本的 HotSpot)没有尾调用优化。但它能够内联有限数量的递归调用,就像它可以内联其他调用一样。

当我们使用

public class Recursion 
    public static void main(String[] args) 
        runs: for(int run = 0; run < 10; run++) 
            for(int i = 1; i < 1_000_000_000; i++) try 
                int counted = recursiveCounter(i, 0);
                if(i != counted) throw new AssertionError();
             catch(***Error e) 
                System.out.println("limit reached at " + i);
                continue runs;
            
        
    

    private static int recursiveCounter(int limit, int count) 
        return limit == 0? count: recursiveCounter(limit - 1, count + 1);
    

我们得到的值类似于Why is the max recursion depth I can reach non-deterministic? Running with -XX:CompileCommand=print,Recursion.recursiveCounter-XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining 打印中讨论的值

@ 18   Recursion::recursiveCounter (18 bytes)   inline
  @ 14   Recursion::recursiveCounter (18 bytes)   inline
    @ 14   Recursion::recursiveCounter (18 bytes)   recursive inlining too deep

  0x0000026398871220:   mov     dword ptr [rsp+0ffffffffffff9000h],eax
  0x0000026398871227:   push    rbp
  0x0000026398871228:   sub     rsp,10h                     ;*synchronization entry
                                                            ; - Recursion::recursiveCounter@-1 (line 16)
  0x000002639887122c:   test    edx,edx
  0x000002639887122e:   je      26398871254h                ;*ifne reexecute=0 rethrow=0 return_oop=0
                                                            ; - Recursion::recursiveCounter@1 (line 16)
  0x0000026398871230:   cmp     edx,1h
  0x0000026398871233:   je      26398871259h                ;*ifne reexecute=0 rethrow=0 return_oop=0
                                                            ; - Recursion::recursiveCounter@1 (line 16)
                                                            ; - Recursion::recursiveCounter@14 (line 16)
  0x0000026398871235:   add     r8d,2h                      ;*iadd reexecute=0 rethrow=0 return_oop=0
                                                            ; - Recursion::recursiveCounter@13 (line 16)
                                                            ; - Recursion::recursiveCounter@14 (line 16)
  0x0000026398871239:   add     edx,0fffffffeh              ;*isub reexecute=0 rethrow=0 return_oop=0
                                                            ; - Recursion::recursiveCounter@10 (line 16)
                                                            ; - Recursion::recursiveCounter@14 (line 16)
  0x000002639887123c:   nop
  0x000002639887123f:   call    26398871220h                ; ImmutableOopMap 
                                                            ;*invokestatic recursiveCounter reexecute=0 rethrow=0 return_oop=0
                                                            ; - Recursion::recursiveCounter@14 (line 16)
                                                            ; - Recursion::recursiveCounter@14 (line 16)
                                                            ;   static_call
  0x0000026398871244:   add     rsp,10h
  0x0000026398871248:   pop     rbp
  0x0000026398871249:   mov     r10,qword ptr [r15+110h]
  0x0000026398871250:   test    dword ptr [r10],eax         ;   poll_return
  0x0000026398871253:   ret
  0x0000026398871254:   mov     eax,r8d
  0x0000026398871257:   jmp     26398871244h
  0x0000026398871259:   mov     eax,r8d
  0x000002639887125c:   inc     eax                         ;*iadd reexecute=0 rethrow=0 return_oop=0
                                                            ; - Recursion::recursiveCounter@13 (line 16)
  0x000002639887125e:   jmp     26398871244h                ;*invokestatic recursiveCounter reexecute=0 rethrow=0 return_oop=0
                                                            ; - Recursion::recursiveCounter@14 (line 16)
                                                            ; - Recursion::recursiveCounter@14 (line 16)
  0x0000026398871260:   mov     rdx,rax
  0x0000026398871263:   add     rsp,10h
  0x0000026398871267:   pop     rbp
  0x0000026398871268:   jmp     26390ea4d00h                ;*ireturn reexecute=0 rethrow=0 return_oop=0
                                                            ; - Recursion::recursiveCounter@17 (line 16)
                                                            ; - Recursion::recursiveCounter@14 (line 16)
                                                            ;   runtime_call _rethrow_Java

有趣的部分是匹配我们的limit == 0 条件的test edx,edx 指令。如果这个条件不满足,下面有一个cmp edx,1h测试,有效地测试下一个递归调用将测试什么,如果这个条件也没有满足,代码将执行add r8d,2hadd edx,0fffffffeh,增加@987654333 @ 减 2,limit 减 2。

所以我们清楚地看到了内联一级递归和融合操作的效果。内联诊断告诉我们已经超过了一个限制,因此值得研究在指定时会发生什么,例如-XX:MaxRecursiveInlineLevel=4:

@ 18   Recursion::recursiveCounter (18 bytes)   inline
  @ 14   Recursion::recursiveCounter (18 bytes)   inline
    @ 14   Recursion::recursiveCounter (18 bytes)   inline
      @ 14   Recursion::recursiveCounter (18 bytes)   inline
        @ 14   Recursion::recursiveCounter (18 bytes)   inline
          @ 14   Recursion::recursiveCounter (18 bytes)   recursive inlining too deep
  0x0000025345d31620:   mov     dword ptr [rsp+0ffffffffffff9000h],eax
  0x0000025345d31627:   push    rbp
  0x0000025345d31628:   sub     rsp,10h                     ;*synchronization entry
                                                            ; - Recursion::recursiveCounter@-1 (line 16)
  0x0000025345d3162c:   test    edx,edx
  0x0000025345d3162e:   je      25345d31660h                ;*ifne reexecute=0 rethrow=0 return_oop=0
                                                            ; - Recursion::recursiveCounter@1 (line 16)
  0x0000025345d31630:   cmp     edx,1h
  0x0000025345d31633:   je      25345d31665h                ;*ifne reexecute=0 rethrow=0 return_oop=0
                                                            ; - Recursion::recursiveCounter@1 (line 16)
                                                            ; - Recursion::recursiveCounter@14 (line 16)
  0x0000025345d31635:   cmp     edx,2h
  0x0000025345d31638:   je      25345d3166ch                ;*ifne reexecute=0 rethrow=0 return_oop=0
                                                            ; - Recursion::recursiveCounter@1 (line 16)
                                                            ; - Recursion::recursiveCounter@14 (line 16)
                                                            ; - Recursion::recursiveCounter@14 (line 16)
  0x0000025345d3163a:   cmp     edx,3h
  0x0000025345d3163d:   je      25345d31674h                ;*ifne reexecute=0 rethrow=0 return_oop=0
                                                            ; - Recursion::recursiveCounter@1 (line 16)
                                                            ; - Recursion::recursiveCounter@14 (line 16)
                                                            ; - Recursion::recursiveCounter@14 (line 16)
                                                            ; - Recursion::recursiveCounter@14 (line 16)
  0x0000025345d3163f:   cmp     edx,4h
  0x0000025345d31642:   je      25345d3167ch                ;*ifne reexecute=0 rethrow=0 return_oop=0
                                                            ; - Recursion::recursiveCounter@1 (line 16)
                                                            ; - Recursion::recursiveCounter@14 (line 16)
                                                            ; - Recursion::recursiveCounter@14 (line 16)
                                                            ; - Recursion::recursiveCounter@14 (line 16)
                                                            ; - Recursion::recursiveCounter@14 (line 16)
  0x0000025345d31644:   add     r8d,5h                      ;*iadd reexecute=0 rethrow=0 return_oop=0
                                                            ; - Recursion::recursiveCounter@13 (line 16)
                                                            ; - Recursion::recursiveCounter@14 (line 16)
                                                            ; - Recursion::recursiveCounter@14 (line 16)
                                                            ; - Recursion::recursiveCounter@14 (line 16)
                                                            ; - Recursion::recursiveCounter@14 (line 16)
  0x0000025345d31648:   add     edx,0fffffffbh
  0x0000025345d3164b:   call    25345d31620h                ; ImmutableOopMap 
                                                            ;*invokestatic recursiveCounter reexecute=0 rethrow=0 return_oop=0
                                                            ; - Recursion::recursiveCounter@14 (line 16)
                                                            ; - Recursion::recursiveCounter@14 (line 16)
                                                            ; - Recursion::recursiveCounter@14 (line 16)
                                                            ; - Recursion::recursiveCounter@14 (line 16)
                                                            ; - Recursion::recursiveCounter@14 (line 16)
                                                            ;   static_call
  0x0000025345d31650:   add     rsp,10h
  0x0000025345d31654:   pop     rbp
  0x0000025345d31655:   mov     r10,qword ptr [r15+110h]
  0x0000025345d3165c:   test    dword ptr [r10],eax         ;   poll_return
  0x0000025345d3165f:   ret
  0x0000025345d31660:   mov     eax,r8d
  0x0000025345d31663:   jmp     25345d31650h
  0x0000025345d31665:   mov     eax,r8d
  0x0000025345d31668:   inc     eax                         ;*iadd reexecute=0 rethrow=0 return_oop=0
                                                            ; - Recursion::recursiveCounter@13 (line 16)
  0x0000025345d3166a:   jmp     25345d31650h
  0x0000025345d3166c:   mov     eax,r8d
  0x0000025345d3166f:   add     eax,2h                      ;*iadd reexecute=0 rethrow=0 return_oop=0
                                                            ; - Recursion::recursiveCounter@13 (line 16)
                                                            ; - Recursion::recursiveCounter@14 (line 16)
  0x0000025345d31672:   jmp     25345d31650h
  0x0000025345d31674:   mov     eax,r8d
  0x0000025345d31677:   add     eax,3h                      ;*iadd reexecute=0 rethrow=0 return_oop=0
                                                            ; - Recursion::recursiveCounter@13 (line 16)
                                                            ; - Recursion::recursiveCounter@14 (line 16)
                                                            ; - Recursion::recursiveCounter@14 (line 16)
  0x0000025345d3167a:   jmp     25345d31650h
  0x0000025345d3167c:   mov     eax,r8d
  0x0000025345d3167f:   add     eax,4h                      ;*iadd reexecute=0 rethrow=0 return_oop=0
                                                            ; - Recursion::recursiveCounter@13 (line 16)
                                                            ; - Recursion::recursiveCounter@14 (line 16)
                                                            ; - Recursion::recursiveCounter@14 (line 16)
                                                            ; - Recursion::recursiveCounter@14 (line 16)
  0x0000025345d31682:   jmp     25345d31650h                ;*invokestatic recursiveCounter reexecute=0 rethrow=0 return_oop=0
                                                            ; - Recursion::recursiveCounter@14 (line 16)
                                                            ; - Recursion::recursiveCounter@14 (line 16)
                                                            ; - Recursion::recursiveCounter@14 (line 16)
                                                            ; - Recursion::recursiveCounter@14 (line 16)
                                                            ; - Recursion::recursiveCounter@14 (line 16)
  0x0000025345d31684:   mov     rdx,rax
  0x0000025345d31687:   add     rsp,10h
  0x0000025345d3168b:   pop     rbp
  0x0000025345d3168c:   jmp     2533e364d00h                ;*ireturn reexecute=0 rethrow=0 return_oop=0
                                                            ; - Recursion::recursiveCounter@17 (line 16)
                                                            ; - Recursion::recursiveCounter@14 (line 16)
                                                            ;   runtime_call _rethrow_Java

我们现在可以看到,编译后的代码已经学会了一次加到五个。该应用程序还将打印更大的嵌套调用限制。指令后面的 cmets 显示有关嵌套调用的元信息仍然存在,即使没有与调用级别关联的单独指令。这意味着仍然可以构建代表整个原始调用链的堆栈跟踪。

当然,这与实际的尾调用优化不同。不管我们设置多高的限制,它仍然内联有限的调用次数,并且已经认识到将限制设置得太高会产生不合理的代码大小。

所以对于“Java JIT 是否曾经优化掉递归方法调用”这个字面问题的答案?是“是的,确实如此”。但不是整个递归而是有限次数的调用,换句话说,不是尾调用优化的形式。

【讨论】:

以上是关于Java JIT 是不是曾经优化递归方法调用?的主要内容,如果未能解决你的问题,请参考以下文章

JVM之JIT编译器实战

JIT

使用hsdis查看jit生成的汇编代码

浅谈Java JIT编译器概念

浅谈Java JIT编译器概念

在 Julia 中使用递归调用减少 JIT 时间