我是java的新手,并且昨晚正在运行一些代码,这确实让我感到困扰。我当时正在构建一个简单的程序,以在for循环中显示每个X输出,当我将模数用作variable % variable
与variable % 5000
或其他参数时,我注意到性能大大下降。有人可以向我解释这是什么原因以及造成它的原因吗?这样我会更好...
这是“高效”的代码(很抱歉,如果我语法有点错误,我现在不在使用该代码的计算机上)
long startNum = 0;
long stopNum = 1000000000L;
for (long i = startNum; i <= stopNum; i++){
if (i % 50000 == 0) {
System.out.println(i);
}
}
这里是“无效代码”
long startNum = 0;
long stopNum = 1000000000L;
long progressCheck = 50000;
for (long i = startNum; i <= stopNum; i++){
if (i % progressCheck == 0) {
System.out.println(i);
}
}
请记住,我有一个日期变量来测量差异,一旦它变长了,第一个变量要花费50毫秒,而另一个变量要花费12秒或类似的时间。如果您的PC比我的PC效率高,或者不那么有效,则可能需要增加stopNum
或减少progressCheck
。
我在网上寻找了这个问题,但找不到答案,也许我只是问的不对。
编辑: 我没想到我的问题会如此受欢迎,我感谢所有答案。我确实在所花费的每个时间上进行了基准测试,效率低下的代码花费了更长的时间,即1/4秒vs. 10秒的付出或付出。尽管他们使用的是println,但是它们的用量相同,所以我不会想象这会造成很大的偏差,特别是因为差异是可重复的。至于答案,由于我是Java新手,所以现在让投票决定哪个答案是最好的。我会尽量在星期三之前选一个。
EDIT2: 今晚我将做另一个测试,它代替模数,它只是增加一个变量,当它到达progressCheck时,它将执行一个,然后将该变量重置为0。对于第三个选项。
EDIT3.5:
我使用了这段代码,下面我将显示结果。.谢谢大家的出色帮助!我还尝试将long的short值与0进行比较,因此我的所有新检查都发生过“ 65536”次,从而使其重复次数相等。
public class Main {
public static void main(String[] args) {
long startNum = 0;
long stopNum = 1000000000L;
long progressCheck = 65536;
final long finalProgressCheck = 50000;
long date;
// using a fixed value
date = System.currentTimeMillis();
for (long i = startNum; i <= stopNum; i++) {
if (i % 65536 == 0) {
System.out.println(i);
}
}
long final1 = System.currentTimeMillis() - date;
date = System.currentTimeMillis();
//using a variable
for (long i = startNum; i <= stopNum; i++) {
if (i % progressCheck == 0) {
System.out.println(i);
}
}
long final2 = System.currentTimeMillis() - date;
date = System.currentTimeMillis();
// using a final declared variable
for (long i = startNum; i <= stopNum; i++) {
if (i % finalProgressCheck == 0) {
System.out.println(i);
}
}
long final3 = System.currentTimeMillis() - date;
date = System.currentTimeMillis();
// using increments to determine progressCheck
int increment = 0;
for (long i = startNum; i <= stopNum; i++) {
if (increment == 65536) {
System.out.println(i);
increment = 0;
}
increment++;
}
//using a short conversion
long final4 = System.currentTimeMillis() - date;
date = System.currentTimeMillis();
for (long i = startNum; i <= stopNum; i++) {
if ((short)i == 0) {
System.out.println(i);
}
}
long final5 = System.currentTimeMillis() - date;
System.out.println(
"\nfixed = " + final1 + " ms " + "\nvariable = " + final2 + " ms " + "\nfinal variable = " + final3 + " ms " + "\nincrement = " + final4 + " ms" + "\nShort Conversion = " + final5 + " ms");
}
}
结果:
不足为奇,由于缺乏划分,短转换比“快速”方式快23%。这很有趣。如果您需要每256次(或大约256次)显示或比较某项内容,可以执行此操作,并使用
if ((byte)integer == 0) {'Perform progress check code here'}
最后一个有趣的注解,在“最终声明的变量”上使用65536(不是一个漂亮的数字)的模数,其速度是(慢于)固定值的一半。之前在哪里以接近相同的速度进行基准测试。
答案 0 :(得分:138)
您正在测量OSR (on-stack replacement)存根。
OSR存根是已编译方法的特殊版本,专门用于在方法运行时将执行从解释模式转换为已编译代码。
OSR存根不像常规方法那样优化,因为它们需要与解释的帧兼容的帧布局。我已经在以下答案中证明了这一点:1,2,3。
类似的事情也在这里发生。当“低效率代码”运行较长的循环时,该方法专门为循环内的堆栈替换而编译。状态从解释的帧转移到OSR编译的方法,并且此状态包括progressCheck
局部变量。此时,JIT无法用常量替换变量,因此无法应用某些strength reduction之类的优化。
特别是,这意味着JIT不会将整数除替换为乘法。 (如果提前启用编译器,则值是内联/常数传播后的编译时常数(如果启用了这些优化),请参见Why does GCC use multiplication by a strange number in implementing integer division?,了解提前编译器提供的asm技巧。{{ 1}}表达式也可以通过%
进行优化,类似于此处甚至在OSR存根中也可以通过JITer对其进行优化。)
但是,如果您多次运行同一方法,则第二次及以后的运行将执行常规(非OSR)代码,该代码已完全优化。这是证明该理论(benchmarked using JMH)的基准:
gcc -O0
结果:
@State(Scope.Benchmark)
public class Div {
@Benchmark
public void divConst(Blackhole blackhole) {
long startNum = 0;
long stopNum = 100000000L;
for (long i = startNum; i <= stopNum; i++) {
if (i % 50000 == 0) {
blackhole.consume(i);
}
}
}
@Benchmark
public void divVar(Blackhole blackhole) {
long startNum = 0;
long stopNum = 100000000L;
long progressCheck = 50000;
for (long i = startNum; i <= stopNum; i++) {
if (i % progressCheck == 0) {
blackhole.consume(i);
}
}
}
}
# Benchmark: bench.Div.divConst
# Run progress: 0,00% complete, ETA 00:00:16
# Fork: 1 of 1
# Warmup Iteration 1: 126,967 ms/op
# Warmup Iteration 2: 105,660 ms/op
# Warmup Iteration 3: 106,205 ms/op
Iteration 1: 105,620 ms/op
Iteration 2: 105,789 ms/op
Iteration 3: 105,915 ms/op
Iteration 4: 105,629 ms/op
Iteration 5: 105,632 ms/op
# Benchmark: bench.Div.divVar
# Run progress: 50,00% complete, ETA 00:00:09
# Fork: 1 of 1
# Warmup Iteration 1: 844,708 ms/op <-- much slower!
# Warmup Iteration 2: 105,893 ms/op <-- as fast as divConst
# Warmup Iteration 3: 105,601 ms/op
Iteration 1: 105,570 ms/op
Iteration 2: 105,475 ms/op
Iteration 3: 105,702 ms/op
Iteration 4: 105,535 ms/op
Iteration 5: 105,766 ms/op
的第一次迭代确实要慢得多,因为OSR存根的编译效率很低。但是,只要方法从头开始重新运行,就会执行新的不受限制的版本,该版本会利用所有可用的编译器优化。
答案 1 :(得分:42)
在跟踪 @phuclv comment时,我检查了JIT 1 生成的代码,结果如下:
variable % 5000
(除以常数):
mov rax,29f16b11c6d1e109h
imul rbx
mov r10,rbx
sar r10,3fh
sar rdx,0dh
sub rdx,r10
imul r10,rdx,0c350h ; <-- imul
mov r11,rbx
sub r11,r10
test r11,r11
jne 1d707ad14a0h
variable % variable
:
mov rax,r14
mov rdx,8000000000000000h
cmp rax,rdx
jne 22ccce218edh
xor edx,edx
cmp rbx,0ffffffffffffffffh
je 22ccce218f2h
cqo
idiv rax,rbx ; <-- idiv
test rdx,rdx
jne 22ccce218c0h
由于除法总是比乘法花费更多的时间,因此最后一个代码段的性能较差。
Java版本:
java version "11" 2018-09-25
Java(TM) SE Runtime Environment 18.9 (build 11+28)
Java HotSpot(TM) 64-Bit Server VM 18.9 (build 11+28, mixed mode)
1-使用的VM选项:-XX:+UnlockDiagnosticVMOptions -XX:CompileCommand=print,src/java/Main.main
答案 2 :(得分:26)
long progressCheck = 50000;
long counter = progressCheck;
for (long i = startNum; i <= stopNum; i++){
if (--counter == 0) {
System.out.println(i);
counter = progressCheck;
}
}
(作为次要的优化尝试,我们在此处使用减量递减计数器,因为在许多架构上,与算术运算之后紧跟0
进行比较之后,恰好花费了0个指令/ CPU周期,因为ALU的标志已经正确设置了不过,即使您编写了if (counter++ == 50000) { ... counter = 0; }
,一个体面的优化编译器也会自动执行该优化。)
请注意,通常您并不需要/不需要模数,因为您知道循环计数器(i
)或仅增加1的东西,而您实际上并不关心实际的余数模数会给你,只要看看递增1计数器是否达到某个值即可。
另一个技巧是使用2的幂/限值,例如progressCheck = 1024;
。可以通过按位and
即if ( (i & (1024-1)) == 0 ) {...}
快速计算出2的幂。这也应该非常快,并且在某些架构上可能会胜过上面明确的counter
。
答案 3 :(得分:3)
看到上述代码的执行效果,我也感到惊讶。这完全取决于编译器根据声明的变量执行程序所花费的时间。在第二个示例中,
for (long i = startNum; i <= stopNum; i++) {
if (i % progressCheck == 0) {
System.out.println(i)
}
}
您正在执行两个变量之间的模运算。在这里,编译器必须在每次迭代后每次检查stopNum
和progressCheck
的值,以转到这些变量所在的特定存储块,因为它是一个变量并且其值可能会更改。>
这就是为什么在每次迭代之后,编译器都会转到内存位置以检查变量的最新值。因此,在编译时,编译器无法创建有效的字节码。
在第一个代码示例中,您将在变量和常量数值之间执行模运算符,该常量在执行期间不会发生变化,并且编译器无需从内存位置检查该数值的值。这就是为什么编译器能够创建有效的字节码的原因。如果将progressCheck
声明为final
或final static
变量,则在运行时/编译时编译器会知道这是一个最终变量,其值不会改变然后编译器在代码中将progressCheck
替换为50000
:
for (long i = startNum; i <= stopNum; i++) {
if (i % 50000== 0) {
System.out.println(i)
}
}
现在您可以看到该代码也看起来像第一个(有效的)代码示例。第一个代码的性能以及我们上面提到的两个代码都将有效地工作。两种代码示例的执行时间都不会有太大差异。