【问题标题】:Performance of Java OptionalJava Optional 的性能
【发布时间】:2019-07-08 18:54:27
【问题描述】:

我刚刚偶然发现 Java 8 中的 Optional 类 - 我真的很喜欢用 isPresent() 方法调用替换我的代码中的一些 null 检查(字面意思是“值是否存在?”)的方法。

我的问题是:这不会导致我的代码性能下降吗?我只是猜测简单的空检查可能会便宜一些,而且我在字节码读取/解释方面还不是很好,所以我对你对这个话题的想法很感兴趣。

【问题讨论】:

  • 为什么不对它进行基准测试?
  • 你不应该使用isPresent,而是使用maporElse
  • @Łukasz:这需要证明。有时确实如此,但是如果你想在值存在时进行副作用操作,如果没有if (isPresent()) doSomething(),你会怎么做? map 和 orElse 在那里都没有意义。
  • 代码的性能从来都与空检查的速度无关,所以它甚至都没有关系。
  • @jod 我的意思是,在现实生活中,一点都不重要(没有双关语)。 Optional 提供的更高的正确性和可读性比字节码的数量重要得多,除非您在非常特定的环境中工作。

标签: java


【解决方案1】:

我使用一种算法进行了一些性能测试,该算法大量使用空检查以及访问可能为空的字段。我实现了一个简单的算法,从单链表中删除中间元素。

首先我实现了两类链表节点:safe - with Optional 和 unsafe - without。

安全节点

class Node<T> {
    private final T data;
    private Optional<Node<T>> next = Optional.empty();

    Node(T data) {

        this.data = data;
    }

    Optional<Node<T>> getNext() {
        return next;
    }

    void setNext(Node<T> next) { setNext(Optional.ofNullable(next)); }

    void setNext(Optional<Node<T>> next ) { this.next = next; }
}

不安全节点

class NodeUnsafe<T> {
    private final T data;
    private NodeUnsafe<T> next;

    NodeUnsafe(T data) {
        this.data = data;
    }

    NodeUnsafe<T> getNext() {
        return next;
    }

    void setNext(NodeUnsafe<T> next) {
        this.next = next;
    }
}

然后我实现了两个类似的方法,唯一的区别是第一个使用Node&lt;T&gt;,第二个使用NodeUsafe&lt;T&gt;

class DeleteMiddle {
    private static <T> T getLinkedList(int size, Function<Integer, T> supplier, BiConsumer<T, T> reducer) {
        T head = supplier.apply(1);
        IntStream.rangeClosed(2, size).mapToObj(supplier::apply).reduce(head,(a,b)->{
            reducer.accept(a,b);
            return b;
        });
        return head;
    }

    private static void deleteMiddle(Node<Integer> head){
        Optional<Node<Integer>> oneStep = Optional.of(head);
        Optional<Node<Integer>> doubleStep = oneStep;
        Optional<Node<Integer>> prevStep = Optional.empty();

        while (doubleStep.isPresent() && doubleStep.get().getNext().isPresent()){
            doubleStep = doubleStep.get().getNext().get().getNext();
            prevStep = oneStep;
            oneStep = oneStep.get().getNext();
        }

        final Optional<Node<Integer>> toDelete = oneStep;
        prevStep.ifPresent(s->s.setNext(toDelete.flatMap(Node::getNext)));
    }

    private static void deleteMiddleUnsafe(NodeUnsafe<Integer> head){
        NodeUnsafe<Integer> oneStep = head;
        NodeUnsafe<Integer> doubleStep = oneStep;
        NodeUnsafe<Integer> prevStep = null;

        while (doubleStep != null && doubleStep.getNext() != null){
            doubleStep = doubleStep.getNext().getNext();
            prevStep = oneStep;
            oneStep = oneStep.getNext();
        }
        if (prevStep != null) {
            prevStep.setNext(oneStep.getNext());
        }
    }

    public static void main(String[] args) {
        int size = 10000000;
        Node<Integer> head = getLinkedList(size, Node::new, Node::setNext);
        Long before = System.currentTimeMillis();
        deleteMiddle(head);
        System.out.println("Safe: " +(System.currentTimeMillis() - before));

        NodeUnsafe<Integer> headUnsafe = getLinkedList(size, NodeUnsafe::new, NodeUnsafe::setNext);
        before = System.currentTimeMillis();
        deleteMiddleUnsafe(headUnsafe);
        System.out.println("Unsafe: " +(System.currentTimeMillis() - before));
    }
}

比较具有不同大小的列表的两次运行表明,使用使用Optional 的代码的方法最多比使用可空值的方法慢两倍。对于小型列表,它会慢 3 倍。

【讨论】:

  • 我期待这个,因为 Optional 是一个必须创建的附加对象......甚至空对象使用大小、对象标题大小等
【解决方案2】:

Optional&lt;T&gt; 只是一个普通的泛型类,它包含类型 T 的引用。因此,它增加了一个间接层。方法调用本身也不会很昂贵,因为类是final,因此可以避免动态调度。

您可能遇到性能问题的唯一地方是在处理大量此类实例时,但即使这样,Stream&lt;Optional&lt;String&gt;&gt; 之类的性能也一点也不差。但是,当处理大量原始值时,您会发现使用Stream&lt;Integer&gt;(或Integer[])与原始专业化IntStream(或int[])相比性能会受到影响,因为这层间接需要非常频繁Integer 对象的实例化。但是,这是一种我们已经知道并且在使用 ArrayList&lt;Integer&gt; 之类的东西时会支付的惩罚。

你显然会遇到与Stream&lt;OptionalInt&gt; / OptionalInt[] 相同的命中,因为 OptionalInt 基本上是一个具有int 字段和boolean 存在标志的类(与Optional&lt;T&gt; 不同,它可以凑合只有T 字段),因此与Integer 非常相似,尽管尺寸更大。当然,Stream&lt;Optional&lt;Integer&gt;&gt; 会增加 两个 级间接性,相应的双倍性能损失。

【讨论】:

    【解决方案3】:

    我们使用 openjdk 对以下代码进行了基准测试。

    sc.map(MYObject::getRequest)
      .map(RequestDO::getMyInst)
      .map(MyInstDO::getCar)
      .map(CarDO::getId); 
    
    if(id.isPresent())
    

    if( null != MYObject.getRequest() && null != 
        MYObject.getRequest().getMyInst() && null != 
        MYObject.getRequest().getMyInst().getCar() && null != 
        MYObject.getRequest().getMyInst().getCar().getId() )
    

    结果显示 Optional 比传统的非空检查要好得多。

    Benchmark                     Mode     Cnt        Score    Error   Units
    
    JMHBMarkModes.measureNotNull  thrpt    5          0.149    ± 0.036  ops/us
    JMHBMarkModes.measureOptional thrpt    5         11.418    ± 1.140  ops/us
    JMHBMarkModes.measureNotNull  avgt     5         12.342    ± 8.334  us/op
    JMHBMarkModes.measureOptional avgt     5          0.088    ± 0.010  us/op
    

    但是,如果您的用例类似于 (null != MYObject.getRequest()) ,那么非空检查会更好。所以可选性能取决于你的用例。

    【讨论】:

    • 正是我正在寻找的基准。但是 thrpt 和 avgt 是什么意思?分数是ms?错误是什么?
    • thrpt - 吞吐量衡量每秒的操作数。 avgt - 平均时间
    • 我想知道如果您对一系列 null 检查进行 OR 运算然后否定它,而不是对一系列非 null 检查进行 AND 运算,结果会怎样?编译器可能足够聪明,可以为您执行此操作,但我想知道是否通过使用 AND 您强制程序执行这些测试中的每一个,而不是简单地在出现空值的第一个符号时退出...
    • @BradleyMcKinley 当然,复合条件的评估在第一次遇到null 时停止。否则,下一个条件将导致NullPointerException。不过,我不同意这种公然的代码重复(调用getRequest() 四次,getMyInst() 三次等)是“传统的非空检查”。然而,这个基准缺乏任何背景;这些方法有多贵,在测试期间他们多久返回一次null/non-null,等等……
    • 你能分享这个基准(完整代码)吗?甚至无法得到接近这样的结果,没有 Optionals 的代码总是快几倍
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-06-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-07-04
    • 1970-01-01
    相关资源
    最近更新 更多