Java中取模运算速度慢吗?

25

出于好奇,我一直在研究JDK中ThreadLocal的实现,然后我发现了这个:

/**
 * Increment i modulo len.
 */
 private static int nextIndex(int i, int len) {
     return ((i + 1 < len) ? i + 1 : 0);
 }

看起来很明显可以使用简单的return (i + 1) % len实现,但我认为这些人懂他们的东西。你有什么想法为什么他们要这样做?

此代码高度面向性能,使用自定义映射来保存线程本地映射,使用弱引用帮助GC变得聪明等等,所以我想这是一个性能问题。在Java中取模速度慢吗?


1
@Dici 抱歉,我已经删除了我的评论,因为另一个评论解释了Java不会生成与汇编相同的代码。 - Leo
1
我的感觉是,在%运算中可能存在一些“最坏情况”,解释器无法选择合理的计算方式(请参见http://blog.teamleadnet.com/2012/07/faster-division-and-modulo-operation.html)。 - Leo
1
@Leo看起来很可信,有一条评论说长度必须是2的幂次方,而实际上确实存在一个动态调整大小的数组。 - Dici
3
我不确定你是如何认为这两个代码是相等的。当你超出长度时,一个会返回0,而另一个无论提供什么数字都会返回0到len-1之间的数字。 - ata
1
@Leo,我刚刚看到了,还没有来得及阅读。也谢谢你。 - Dici
显示剩余18条评论
2个回答

31

为了提高性能,这个示例中避免使用%操作符。

div/rem 操作即使在CPU的架构层面上也较慢;不仅在Java中如此。例如,在Haswell架构下,idiv指令的最小延迟约为10个周期,而add指令仅需1个周期。

让我们使用JMH进行基准测试。

import org.openjdk.jmh.annotations.*;

@State(Scope.Benchmark)
public class Modulo {
    @Param("16")
    int len;

    int i;

    @Benchmark
    public int baseline() {
        return i;
    }

    @Benchmark
    public int conditional() {
        return i = (i + 1 < len) ? i + 1 : 0;
    }

    @Benchmark
    public int mask() {
        return i = (i + 1) & (len - 1);
    }

    @Benchmark
    public int mod() {
        return i = (i + 1) % len;
    }
}

结果:

Benchmark           (len)  Mode  Cnt  Score   Error  Units
Modulo.baseline        16  avgt   10  2,951 ± 0,038  ns/op
Modulo.conditional     16  avgt   10  3,517 ± 0,051  ns/op
Modulo.mask            16  avgt   10  3,765 ± 0,016  ns/op
Modulo.mod             16  avgt   10  9,125 ± 0,023  ns/op

正如您所见,使用%比条件表达式慢约2.6倍。JIT在讨论的ThreadLocal代码中无法自动优化这一点,因为除数(table.length)是可变的。


明天我会仔细看你的答案,现在我需要睡觉了:D 谢谢! - Dici

3
mod在Java中并不慢。它是针对整数和浮点数分别实现的字节码指令iremfrem。JIT在优化方面做得很好。
在我的基准测试中(请参见文章),JDK 1.8中的irem调用大约需要1纳秒。非常快。frem调用要慢3倍,因此尽可能使用整数。
如果您正在使用自然整数(例如数组索引)和2的幂除数(例如8个线程本地变量),那么可以使用位操作技巧来获得20%的性能提升。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接