我在一个Java程序中发现了一些奇怪的行为。我试图尽可能地删除代码,同时仍然能够复制行为。代码全部如下。
public class StrangeBehaviour {
static boolean recursionFlag = true;
public static void main(String[] args) {
long startTime = System.nanoTime();
for (int i = 0; i < 10000; i ++) {
functionA(6, 0);
}
long endTime = System.nanoTime();
System.out.format("%.2f seconds elapsed.\n", (endTime - startTime) / 1000.0 / 1000 / 1000);
}
static boolean functionA(int recursionDepth, int recursionSwitch) {
if (recursionDepth == 0) { return true; }
return functionB(recursionDepth, recursionSwitch);
}
static boolean functionB(int recursionDepth, int recursionSwitch) {
for (int i = 0; i < 16; i++) {
if (StrangeBehaviour.recursionFlag) {
if (recursionSwitch == 0) {
if (functionA(recursionDepth - 1, 1 - recursionSwitch)) return true;
} else {
if (!functionA(recursionDepth - 1, 1 - recursionSwitch)) return false;
}
} else {
// This block is never entered into.
// Yet commenting out one of the lines below makes the program run slower!
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
System.out.println("...");
}
}
return false;
}
}
我有两个函数,functionA()
和functionB()
递归调用彼此。这两个函数都使用recursionDepth
参数来控制递归的终止。 functionA()
functionB()
最多只调用recursionDepth
一次functionB()
。 functionA()
使用recursionDepth - 1
拨打functionA()
16次。使用recursionDepth
0
调用functionB()
时,递归会终止。
System.out.println()
的代码块包含多个boolean recursionFlag
次调用。永远不会输入此块,因为条目由true
变量控制,该变量设置为println()
并且在程序执行期间从不更改。但是,即使其中一个println()
调用注释掉也会导致程序运行速度变慢。在我的机器上,当所有{{1}}个呼叫都存在时,执行时间<0.2秒,当其中一个呼叫被注释掉时,执行时间> 2秒。
可能导致此行为的原因是什么?我唯一的猜测是,有一些天真的编译器优化是由与代码块长度相关的参数(或函数调用的数量等)触发的。任何进一步的见解将非常感谢!
编辑:我使用的是JDK 1.8。
答案 0 :(得分:41)
注释代码会影响内联的处理方式。 如果functionB变得更长/更大(更多字节码指令),它将不会内联到functionA。
所以@ J3D1能够使用VMOptions手动关闭functionB()的内联:
-XX:CompileCommand=dontinline,com.jd.benchmarking.StrangeBehaviour::functionB
这似乎消除了功能较短的延迟。
使用vm选项可以显示内联
-XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining
@ 8 StrangeBehaviour::functionB (326 bytes) callee is too large
@ 21 StrangeBehaviour::functionA (12 bytes)
@ 8 StrangeBehaviour::functionB (326 bytes) callee is too large
@ 35 StrangeBehaviour::functionA (12 bytes)
@ 8 StrangeBehaviour::functionB (326 bytes) callee is too large
@ 8 StrangeBehaviour::functionB (318 bytes) inline (hot)
@ 21 StrangeBehaviour::functionA (12 bytes) inline (hot)
@ 8 StrangeBehaviour::functionB (318 bytes) inline (hot)
@ 35 StrangeBehaviour::functionA (12 bytes) recursive inlining is too deep
@ 35 StrangeBehaviour::functionA (12 bytes) inline (hot)
@ 8 StrangeBehaviour::functionB (318 bytes) inline (hot)
@ 21 StrangeBehaviour::functionA (12 bytes) recursive inlining is too deep
@ 35 StrangeBehaviour::functionA (12 bytes) recursive inlining is too deep
@ 21 StrangeBehaviour::functionA (12 bytes) inline (hot)
@ 8 StrangeBehaviour::functionB (318 bytes) inline (hot)
@ 35 StrangeBehaviour::functionA (12 bytes) inline (hot)
@ 8 StrangeBehaviour::functionB (318 bytes) recursive inlining is too deep
@ 35 StrangeBehaviour::functionA (12 bytes) inline (hot)
@ 8 StrangeBehaviour::functionB (318 bytes) inline (hot)
@ 21 StrangeBehaviour::functionA (12 bytes) inline (hot)
@ 8 StrangeBehaviour::functionB (318 bytes) recursive inlining is too deep
@ 35 StrangeBehaviour::functionA (12 bytes) inline (hot)
@ 8 StrangeBehaviour::functionB (318 bytes) recursive inlining is too deep
主要是猜测,但更大/内联字节码会导致分支预测和缓存问题
答案 1 :(得分:21)
完整的答案是k5_和Tony的答案的组合。
OP发布的代码在执行基准测试之前省略了一个热启动循环以触发HotSpot编译;因此,当包含打印语句时,10倍(在我的计算机上)加速,结合了HotSpot花费的时间来编写字节码到CPU指令,以及CPU指令的实际运行。
如果我在定时循环之前添加一个单独的预热循环,那么print语句只有2.5倍的加速。
这表明HotSpot / JIT编译在内联方法时(如Tony解释的那样)需要更长时间,并且代码的运行需要更长时间,这可能是由于更糟糕的缓存或分支预测/流水线性能, k5_表示。
public static void main(String[] args) {
// Added the following warmup loop before the timing loop
for (int i = 0; i < 50000; i++) {
functionA(6, 0);
}
long startTime = System.nanoTime();
for (int i = 0; i < 50000; i++) {
functionA(6, 0);
}
long endTime = System.nanoTime();
System.out.format("%.2f seconds elapsed.\n", (endTime - startTime) / 1000.0 / 1000 / 1000);
}
答案 2 :(得分:18)
我和@ k5_一起,似乎存在确定是否内联函数的阈值。如果JIT编译器决定内联它,那将导致大量工作和时间,SQLitePCL.raw.SetProvider(new SQLitePCL.SQLite3Provider_sqlite3());
var dbName = Path.Combine(Path.GetTempPath(), "StackOverflow.db");
using (sqlite3 dbRaw = ugly.open(dbName))
{
dbRaw.create_function("distance", 4, null, UDFDistanceFunction);
double currentLatitude = 47.0;
double currentLongitude = -122.0;
var sql = $"SELECT * FROM barlocations WHERE distance('{currentLatitude.ToString()}', '{currentLongitude.ToString()}', barlocations.lat, barlocations.lng) <= 100 ;";
var locs = dbRaw.query<BarLocations>(sql);
foreach (var loc in locs)
{
Console.WriteLine(loc.name);
}
}
显示:
$scope.uploader.onCompleteItem = function (fileItem, response, status, headers) {
console.info('onCompleteItem', fileItem, response, status, headers);
console.info(response.name); // here I can get the response url
$scope.imgurl = response.name;
};
上面是没有注释的信息,以下是一个注释,它将方法大小从326字节减少到318字节。您可以注意到输出的第1列中的任务ID,后者中的任务ID更大,这会导致更多时间。
-XX:+PrintCompilation
如果您将代码更改为以下内容(添加两行并将打印行换行),您可以看到代码大小更改为326字节,并且现在运行得更快:
task-id
158 32 3 so_test.StrangeBehaviour::functionB (326 bytes) made not entrant
159 35 3 java.lang.String::<init> (82 bytes)
160 36 s 1 java.util.Vector::size (5 bytes)
1878 37 % 3 so_test.StrangeBehaviour::main @ 6 (65 bytes)
1898 38 3 so_test.StrangeBehaviour::main (65 bytes)
2665 39 3 java.util.regex.Pattern::has (15 bytes)
2667 40 3 sun.misc.FDBigInteger::mult (64 bytes)
2668 41 3 sun.misc.FDBigInteger::<init> (30 bytes)
2668 42 3 sun.misc.FDBigInteger::trimLeadingZeros (57 bytes)
2.51 seconds elapsed.
新时间和JIT编译器信息:
task-id
126 35 4 so_test.StrangeBehaviour::functionA (12 bytes)
130 33 3 so_test.StrangeBehaviour::functionA (12 bytes) made not entrant
131 36 s 1 java.util.Vector::size (5 bytes)
14078 37 % 3 so_test.StrangeBehaviour::main @ 6 (65 bytes)
14296 38 3 so_test.StrangeBehaviour::main (65 bytes)
14296 39 % 4 so_test.StrangeBehaviour::functionB @ 2 (318 bytes)
14300 40 4 so_test.StrangeBehaviour::functionB (318 bytes)
14304 34 3 so_test.StrangeBehaviour::functionB (318 bytes) made not entrant
14628 41 3 java.util.regex.Pattern::has (15 bytes)
14631 42 3 sun.misc.FDBigInteger::mult (64 bytes)
14632 43 3 sun.misc.FDBigInteger::<init> (30 bytes)
14632 44 3 sun.misc.FDBigInteger::trimLeadingZeros (57 bytes)
14.50 seconds elapsed.
总结:
<强>更新强>:
收到my latest trial,这个问题的答案并不那么容易:
正如我的代码示例所示,正常的内联优化将
但是在这个问题中,代码会导致很多JIT工作并减慢程序,这似乎是JIT的一个bug。目前还不清楚为什么会导致JIT如此多的工作。