【问题标题】:Efficiency of temporary variables in JavaJava中临时变量的效率
【发布时间】:2013-06-26 04:26:48
【问题描述】:

我有课

class A {
    private int x;
    public void setX(...){...}
    public int getX(){return x;}
}

class B {
    int y;
    public void setY() {
        //Accessing x of A, assume I already have object of A
        if(a.getX() < 0) {
             y = a.getX();
        }
    }
}

class C {
    int y;
    public void setY() {
        //Accessing x of A, assume I already have object of A
        int tmpX = a.getX();
        if(tmpX < 0) {
             y = tmpX;
        }
    }
}

哪种编码方式更好?我在 B 类或 C 类中访问 x of A 的方式?

【问题讨论】:

  • 编译器很聪明,相信他。它会将它们转换为相同的字节码。
  • 两者都和我差不多。
  • 在考虑微优化之前,您应该先看看“JIT(即时)编译器做了什么”stackoverflow.com/questions/95635/…。换句话说:只要您没有真正的性能问题,您应该更喜欢清晰的设计而不是代码优化。
  • 这个问题已经被问到并回答了here
  • @gaganbm - 编译器生成与第二种情况相同的第一种情况是非法的。如果方法调用出现在源代码中,则必须对其进行评估。毕竟,不能保证 getX 每次都返回相同的值,也不能保证 getX 不会修改 a 中的某些内部值。

标签: java performance


【解决方案1】:

让我们看看它编译成什么。我编译

class A {
    private int x;
    public void setX(int x_){x=x_;}
    public int getX(){return x;}
}

class B {
    int y;
    A a;
    public void setY() {
        //Accessing x of A, assume I already have object of A
        if(a.getX() < 0) {
             y = a.getX();
        }
    }
}

class C {
    int y;
    A a;
    public void setY() {
        //Accessing x of A, assume I already have object of A
        int tmpX = a.getX();
        if(tmpX < 0) {
             y = tmpX;
        }
    }
}

然后得到 B

  public void setY();
    Code:
       0: aload_0       
       1: getfield      #2                  // Field a:LA;
       4: invokevirtual #3                  // Method A.getX:()I
       7: ifge          21
      10: aload_0       
      11: aload_0       
      12: getfield      #2                  // Field a:LA;
      15: invokevirtual #3                  // Method A.getX:()I
      18: putfield      #4                  // Field y:I
      21: return        
}

对于 C

  public void setY();
    Code:
       0: aload_0       
       1: getfield      #2                  // Field a:LA;
       4: invokevirtual #3                  // Method A.getX:()I
       7: istore_1      
       8: iload_1       
       9: ifge          17
      12: aload_0       
      13: iload_1       
      14: putfield      #4                  // Field y:I
      17: return        
}

因为C 只调用getX 一次它会更“高效”,因为这是那里最昂贵的东西。但是你真的不会注意到这一点。尤其是 HotSpot JVM 会很快“内联”这个方法调用。

除非这是正在运行的主要代码 没有必要对此进行优化,因为您几乎不会注意到它。

然而,正如其他地方所提到的,除了性能之外还有其他原因,为什么C 方法更可取。一个明显的问题是如果getX() 的结果在两个调用之间发生变化(在存在并发的情况下)。

【讨论】:

  • 嵌入式系统不使用热点
  • 对不起,我认为它使用的是标准的 JVM(没有其他问题的迹象)。无论如何都不值得优化。但我想在嵌入式系统中也许 - C 还是更好。
  • 对不起 - 我的意思是类 C 这样做的方式。一个虚拟通话与两个虚拟通话。
  • 在字节码分析方面做得很好,但我不同意这个答案的结论,原因有两个 a) 效率论点假设最好的情况(完美的 JIT 优化)并且在很多情况下它 将产生性能差异 b)还有其他原因更喜欢使用临时变量(并发影响、可维护性)
【解决方案2】:

哪种编码方式更好?

在可读性方面,值得商榷,但差别不大。

在健壮性方面,C 更好;见下文(最后),尽管您通常可以排除这些情况。

就性能(这是您真正要问的)而言,答案是它取决于平台。这取决于:

  • 无论您是编译还是解释代码,
  • 如果您正在 JIT 编译该代码是否实际被编译,并且
  • 编译器/优化器的质量,以及有效优化的能力。

确定的唯一方法是创建一个有效的微基准测试并使用您关注的特定平台来实际测试性能

(这也取决于getX()是否需要虚拟调用;即是否是覆盖getX()方法的X的子类。)

但是,我会预测:

  • 在启用 JIT 编译的 Java 热点系统上,JIT 将内联 getX() 调用(以虚拟调用问题为模),
  • 在早期的 Davlik VM 上,JIT 编译器不会内联调用,并且
  • 在最近的 Davlik VM 上,JIT 编译器将内联调用。

(最后的预测是基于来自 Davlik 编译器之一的this Answer...)


先发制人对代码进行微优化通常是个坏主意:

  • 大多数时候,微优化会浪费时间。除非此代码被大量执行,否则任何性能差异都可能不明显。
  • 在其余时间中,微优化将无效...或者实际上使事情变得更糟1
  • 即使您的微优化在您的一代平台上运行,后续版本中的 JIT 编译器更改也可能导致微优化无效......甚至更糟。

1 - 我从 Sun 编译器人员那里看到了“聪明的微优化”实际上可以阻止优化器检测到可能的有用优化的建议。这可能不适用于此示例,但是 ...


最后,我要指出,在某些情况下BC 不是等效代码。想到的一种情况是,如果有人创建了A 的子类,其中getX 方法具有隐藏的副作用;例如其中调用getX 会导致发布事件,或增加调用计数器。

【讨论】:

    【解决方案3】:

    C 更高效,因为 getter 被调用一次。

    用户 Hot Licks 评论说编译器无法优化第二次调用, 因为它不知道getX() 是否会在第二次调用中提供另一个结果。

    在您的示例中,差别不大,但在循环中差别不大。

    用户 selig 证明了假设,他反编译并表明 C 更有效,因为 B 调用了两次方法。)

    【讨论】:

      【解决方案4】:

      您通常应该使用临时变量,即以下通常更好:

       int tmpX = a.getX();
       if(tmpX < 0) {
             y = tmpX;
       }
      

      这有几个原因:

      • 它将至少一样快或更快。使用临时本地 int 变量非常便宜(很可能存储在 CPU 寄存器中),并且比额外的方法调用加上额外的字段查找的成本要好。如果您幸运,那么 JIT 可能会将两者编译成等效的本机代码,但这取决于实现。
      • 并发性更安全 - 字段 x 可能会在两个 getX() 调用之间被另一个线程更改。通常,您只想读取一个值并使用该值,而不是遇到处理两个可能不同的值和混淆结果的问题....
      • 如果将来有人让getX() 调用变得更加复杂(例如添加日志记录,或计算 x 的值而不是使用字段),它肯定会更有效。考虑长期可维护性。
      • 您可以使用更好的名称,方法是分配给命名良好的临时变量。 tmpX 并不是很有意义,但如果它类似于playerOneScore,那么它会让你的代码更清晰。好的名称使您的代码更具可读性和可维护性。
      • 一般的良好做法是尽量减少多余的方法调用。即使在这种特殊情况下无关紧要,最好养成这样做的习惯,以便在重要的情况下自动执行(例如,当方法调用导致昂贵的数据库查找时)。李>

      【讨论】:

        【解决方案5】:

        在标题中,您问的是哪个更效率。我认为你的意思是性能方面。在这种情况下,对于一个简单地公开一个字段的典型 getter,如果这两种情况有任何不同,我会感到惊讶。

        编码的更好方式,另一方面,往往是指可读性和结构化。在那种情况下,我个人会选择第二个。

        【讨论】:

          【解决方案6】:

          B类的方法会调用两次,C类的方法会调用一次。所以C类的方法更好

          【讨论】:

          • 是的,但如果它是真的,那么它肯定会进行两次调用.. 如此有效的方法是在 C 类中使用。
          【解决方案7】:

          如果您真的很在意,最好的办法是编写一个相当测试的代码并找出哪个执行得最快。问题是结果可能会根据您使用的 VM 版本而改变。

          我最好的猜测是 c 类比 b 稍好,因为它只需要一个方法调用。如果您最终确定临时 int,您甚至可能会获得更好的性能。我曾经测试过这个

          for( int i = 0; i < foo.size(); i++ )
          

          反对

          for( int i = 0, n = foo.size(); i < n; i++ )
          

          并发现后者更可取(这是与另一个程序员的争论,我赢了)。您遇到的情况可能非常相似,因为我猜您不会担心这一点,除非您创建数百万个 b 或 c 类对象。如果您没有创建数百万个 b / c 类对象,那么我会担心其他事情,因为您不会做出任何明显的改变。

          【讨论】:

            【解决方案8】:

            在分配给 y 之前,它们是相同的—— temp var 没有效果(因为在第一种情况下内部生成了一个)。

            但是,第一种情况将导致(根据 Java 规则)再次调用 getX 以分配给 y,而第二种情况将重用先前的值。

            (但 JITC 可能会将其展平并再次使它们相同。)

            注意:不过,重要的是要了解这两个版本在语义上相同。他们做不同的事情,可以产生不同的结果。

            【讨论】:

              【解决方案9】:

              如果你想自己检查,你可以使用System.currentTimeMillis(),然后运行代码几百万次(每次首先将任何创建的变量设置为其他变量以确保它被重置)然后再次使用System.currentTimeMillis()并减去以获得每个人的总时间重复,看看哪个更快。顺便说一句,除非您实际上要运行数百万次,否则我怀疑它会产生很大的不同。

              【讨论】:

                【解决方案10】:

                如果 x 和 y 是您经常需要的坐标,请考虑直接访问: 如果你有一个 getter 和一个 setter ,那么你也可以将它们设为 public 或 protected。

                 if (a.x < 0) {
                    y = a.x;
                 }
                

                这可能看起来有点反面向对象,但在现代语言中你有属性 避免公式中丑陋的吸气剂。 该代码比您重复的 getX() 更具可读性。

                (a.getX() + b.getX() + c.getX()) / 3.0;
                

                如果beeing是正确的,那么证明比:

                (a.x + b.x + c.x) / 3.0;
                

                【讨论】:

                  【解决方案11】:

                  此答案仅用于解决此评论中提出的观点:

                  编译器生成第一种情况与第二种情况相同是非法的。如果方法调用出现在源代码中,则必须对其进行评估。毕竟,不能保证 getX 每次都返回相同的值,也不能保证 getX 不会修改 a 中的某些内部值。 – Hot Licks Jun 28 at 11:55

                  这是有问题的代码:

                      if(a.getX() < 0) {
                           y = a.getX();
                      }
                  

                  getX() 在哪里

                      public int getX(){return x;}
                  

                  (这种方法显然没有副作用。)

                  事实上,编译器可以优化第二次调用,假设它可以推断出当前线程中没有任何东西可以改变结果。 允许忽略另一个线程所做的更改...除非存在导致相关状态更改的操作“发生在”观察状态的动作。 (换句话说,除非以线程安全的方式进行更改。)

                  在这种情况下,代码显然不是线程安全的。因此,允许编译器(或更准确地说,JIT 编译器)优化第二次调用。

                  但是,不允许字节码编译器进行这种优化。这两个类是独立的编译单元,字节码编译器必须允许在重新编译B 之后可以修改和重新编译(比如)A。因此,字节码编译器不能确定A.getX() 在编译B 时总是没有副作用。 (相比之下,JIT 可以进行这种推论......因为类在加载后无法更改。)

                  请注意,这只是编译器允许做的事情。在实践中,它们可能更加保守,尤其是因为这些优化的执行成本往往相对较高。


                  我不知道 JIT 编译器的优化器是如何工作的,一个明显的方法是这样的;

                  1. 推断getX() 是一种不需要虚拟方法分派的方法,因此是内联的候选对象
                  2. 将方法主体内联到调用中的两个点
                  3. 执行本地数据流分析,显示相同变量在几条指令的空间内被加载两次
                  4. 在此基础上,消除二次负载。

                  所以事实上,第二次调用可以完全优化掉,明确推理该方法可能的副作用。

                  【讨论】:

                    猜你喜欢
                    • 2021-03-10
                    • 1970-01-01
                    • 1970-01-01
                    • 2013-02-23
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 2014-05-09
                    • 1970-01-01
                    相关资源
                    最近更新 更多