【问题标题】:Strange performance drop of JDK8 LocalDate.toEpochDayJDK8 LocalDate.toEpochDay 的奇怪性能下降
【发布时间】:2014-03-01 07:02:53
【问题描述】:

我很好奇我们是否最终会使用 JDK8 获得一个快速的日期时间库。几乎所有LocalDate 计算都使用toEpochDay,所以我查看了source,大量的部门和分支让我很好奇我是否可以做得更好。

我消除了一些分支和除一个之外的所有分支,但是加速比预期的要差。所以我的第一个问题是,使用多重除法的算法怎么可能只需要大约 30 个周期(吞吐量)。 Holger 的 cmets 似乎已经回答了这个问题: 除以一个小常数得到 JIT-ed 到乘法。 我是手动完成的,现在我始终以 2 倍的速度击败原始实现。

benchmark 非常简单,只需遍历一个随机数组LocalDates 并转换每个toEpochDay。尽管存在随机性,results 还是相当一致的。数组的大小是一个参数,我的主要问题是 2000 到 30000 之间的大幅减速来自哪里。由于数据不再适合 L1 缓存,因此应该会有所放缓,但两种算法的内存访问完全相同(即,除了从数组中获取 date 之外没有其他算法)。

仍然悬而未决的问题是:当迭代一个数组时,同一函数的两个简单的无内存访问实现的行为是如何发生变化的?原始算法遭受了更大的减速比我的。

我的algorithm 可能不值得在这里复制,它没有文档并且和原版一样神秘,而且只有一个非常基本的test

【问题讨论】:

    标签: java performance datetime benchmarking caliper


    【解决方案1】:

    我没有直接找到原因,但这肯定是基准测试框架的缺陷。与 GC 和每次调用成本有关的东西。我与 JMH 的性能下降相同,除了 100 个日期的工作台显示比 2000 个日期更好的性能。我尝试创建始终具有最大大小的 dates 数组,并仅迭代前 100、2000、30000 个元素。在这种情况下,所有版本的性能都相同(在我的机器上为 15.3 +- 0.3 ns)。

    import org.openjdk.jmh.annotations.*;
    
    import java.time.LocalDate;
    import java.util.*;
    import java.util.concurrent.ThreadLocalRandom;
    import java.util.concurrent.TimeUnit;
    
    
    @State(Scope.Benchmark)
    @BenchmarkMode(Mode.AverageTime)
    @OperationsPerInvocation(LocalDateBenchmark.ITERATIONS)
    @OutputTimeUnit(TimeUnit.NANOSECONDS)
    public class LocalDateBenchmark {
        public static final int MAX_ITERATIONS = 1000000;
        public static final int ITERATIONS = 30000;
    
        private static final LocalDate MIN_DATE = LocalDate.of(1900, 1, 1);
        private static final LocalDate MAX_DATE = LocalDate.of(2100, 1, 1);
        private static final int DAYS_BETWEEN = (int) (MAX_DATE.toEpochDay() - MIN_DATE.toEpochDay());
    
        public LocalDate[] dates = new LocalDate[MAX_ITERATIONS];
        private Random random;
    
        @Setup(Level.Trial)
        public void setUpAll() {
            Random r = ThreadLocalRandom.current();
            for (int i=0; i< dates.length; ++i) {
                dates[i] = MIN_DATE.plusDays(r.nextInt(DAYS_BETWEEN));
            }
        }
    
        @Setup(Level.Iteration)
        public void setUpRandom() {
            random = new Random();
        }
    
        @GenerateMicroBenchmark
        public int timeToEpochDay(LocalDateBenchmark state) {
            int result = 0;
            LocalDate[] dates = state.dates;
            int offset = random.nextInt(MAX_ITERATIONS - ITERATIONS);
            for (int i = offset; i < offset + ITERATIONS; i++) {
                LocalDate date = dates[i];
                result += date.toEpochDay();
            }
            return result;
        }
    }
    

    【讨论】:

    • 我无法确认。对我来说,results 几乎没有改变。你能发布你的基准吗?
    • 我明白了。但这会导致缓存丢失,与预取器作斗争,甚至可能会消除范围检查。这就解释了为什么 100 是失败者,也许也解释了为什么其他尺寸的速度保持不变。但是,我认为它并不能解释原始情况(当 caliper 和 JMH 都发生这种情况时,我不会将其称为框架故障)。
    • @maaartinus 也许是因为缓存?在 L1 中循环 100 个日期。
    【解决方案2】:

    那是因为算法中没有除法。所有 / 4 都被班次取代。所有的 / 100 实际上都是 * 0.01。这些部门是为了可读性而存在的(呵呵)。我不确定这种优化是否发生在字节码发射或 JIT 编译期间,查看类文件并找出答案会很有趣。

    【讨论】:

    • 字节码中肯定没有这样的优化(javac 在这方面没有做任何事情)。一般来说/4 不能被&gt;&gt;2 替换(因为除法的奇怪的零舍入语义,这里可以。有时你可以用浮点乘法和舍入来替换整数除法,但我不确定如果有帮助。对于一般的长操作数,这很可能是不可能的,因为double 仅具有 56 位尾数。
    • @maaartinus:如果您查看代码,您会在除法之前看到一个if (y &gt;= 0)。因此 JVM 可以证明在一种情况下股息不是负数,因此将 /4 替换为 &gt;&gt;2 是适用的,即使在另一种情况下,它也可以生成正确的代码来修复它,例如如果你知道y 是负数,-((-y)&gt;&gt;2) 就可以了。
    • 对于100的除法,不需要使用浮点数。由于源值是intshort,并且除法是在long 范围内完成的,因此可以应用“小数的整数除法”优化。 Wikipedia 对该概念进行了简要说明。简单地说,/100*X/Y 替换,XY 被选中,因此 Y2 的幂,因此 /Y 被移位替换,X/Y足够接近0.01的整数结果。
    • @Holger:关于移位而不是除法,是的(我什至在上面混淆的句子中写了“这里可以”)。然而,这只是四个部门之一。
    • @maaartinus:它是long,但使用来自int 的值进行初始化。其他值甚至来自shorts。那是我已经写的;这是优化的完美情况,因为 JVM 可以证明long 的高位未被使用。也许这就是在这个地方将long 用于ym 的唯一原因。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-12-08
    • 2023-01-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-05-11
    • 1970-01-01
    相关资源
    最近更新 更多