【问题标题】:Why are interface method invocations slower than concrete invocations?为什么接口方法调用比具体调用慢?
【发布时间】:2011-10-13 23:21:57
【问题描述】:

当我发现抽象类和接口之间的区别时,就会想到这个问题。 在this post 中,我知道接口很慢,因为它们需要额外的间接。 但是我没有得到接口而不是抽象类或具体类所需的间接类型。请澄清一下。 提前致谢

【问题讨论】:

  • 告诉你“界面很慢”的来源是什么?
  • @Mat Log4J 文档指出,“在 log4j 中,对 Logger 类的实例发出日志请求。Logger 是一个类而不是一个接口。这显着地降低了方法调用的成本 以牺牲一些灵活性为代价。”对我来说,这就是“在需要极其激进的优化的时候,接口可能是一个障碍。”我不知道这有多准确(而且我从来没有遇到过这种情况,我无法证明它的准确性),但它是一个有信誉的来源。
  • 看到在 JIT 之后显示出可测量差异的测试真的很感兴趣。 Log4J 已经存在了很长时间,可以相信 12 年前是真的,甚至在 1.4.2 热点出现之前。很难相信在现代 JVM 上的 JIT 之后会有任何可测量的差异。
  • @Sanjay:你的第一篇文章是关于 gcj,这是一个 糟糕 Java 实现(他们做了必要的工作,但就纯粹的生产质量而言,它从来都不是很好) .您的第二个链接只是 states 它就好像它是一个事实,并且没有给出关于他是如何得出这个结论的 任何 指示。很可能他自己只是在一篇 10 年前的文章中读过它。

标签: java


【解决方案1】:

存在许多性能神话,有些可能在几年前是正确的,有些在没有 JIT 的 VM 上可能仍然正确。

Android 文档(请记住,Android 没有 JVM,他们有 Dalvik VM)曾经说过在接口上调用方法比在类上调用方法要慢,因此它们有助于传播神话(在他们打开 JIT 之前,Dalvik VM 上的速度也可能较慢)。文档现在确实说:

性能神话

本文档的先前版本提出了各种误导性声明。我们 在这里解决其中的一些问题。

在没有 JIT 的设备上,确实可以通过 具有确切类型而不是接口的变量稍微多一点 高效的。 (因此,例如,在一个 HashMap 映射比 Map 映射,即使在这两种情况下,映射都是 HashMap。)这不是慢 2 倍的情况;实际上 差异更像是慢了 6%。此外,JIT 使这两个 实际上无法区分。

来源:Designing for performance on Android

JVM 中的 JIT 可能也是如此,否则会很奇怪。

【讨论】:

    【解决方案2】:

    如果有疑问,请测量它。我的结果显示没有显着差异。运行时,产生以下程序:

    7421714 (abstract)
    5840702 (interface)
    
    7621523 (abstract)
    5929049 (interface)
    

    但是当我切换两个循环的位置时:

    7887080 (interface)
    5573605 (abstract)
    
    7986213 (interface)
    5609046 (abstract)
    

    抽象类似乎稍微快一些(~6%),但这不应该引起注意;这些是纳秒。 7887080 纳秒约为 7 毫秒。这使得每 40k 调用相差 0.1 毫秒(Java 版本:1.6.20)

    代码如下:

    public class ClassTest {
    
        public static void main(String[] args) {
            Random random = new Random();
            List<Foo> foos = new ArrayList<Foo>(40000);
            List<Bar> bars = new ArrayList<Bar>(40000);
            for (int i = 0; i < 40000; i++) {
                foos.add(random.nextBoolean() ? new Foo1Impl() : new Foo2Impl());
                bars.add(random.nextBoolean() ? new Bar1Impl() : new Bar2Impl());
            }
    
            long start = System.nanoTime();    
    
            for (Foo foo : foos) {
                foo.foo();
            }
    
            System.out.println(System.nanoTime() - start);
    
    
            start = System.nanoTime();
    
            for (Bar bar : bars) {
                bar.bar();
            }
    
            System.out.println(System.nanoTime() - start);    
        }
    
        abstract static class Foo {
            public abstract int foo();
        }
    
        static interface Bar {
            int bar();
        }
    
        static class Foo1Impl extends Foo {
            @Override
            public int foo() {
                int i = 10;
                i++;
                return i;
            }
        }
        static class Foo2Impl extends Foo {
            @Override
            public int foo() {
                int i = 10;
                i++;
                return i;
            }
        }
    
        static class Bar1Impl implements Bar {
            @Override
            public int bar() {
                int i = 10;
                i++;
                return i;
            }
        }
        static class Bar2Impl implements Bar {
            @Override
            public int bar() {
                int i = 10;
                i++;
                return i;
            }
        }
    }
    

    【讨论】:

      【解决方案3】:

      一个对象有一个“vtable 指针”,它指向它的类的一个“vtable”(方法指针表)(“vtable”可能是错误的术语,但这并不重要)。 vtable 有指向所有方法实现的指针;每个方法都有一个对应于表条目的索引。因此,要调用类方法,您只需在 vtable 中查找相应的方法(使用其索引)。如果一个类扩展了另一个类,它只是有一个更长的 vtable 和更多的条目;从基类调用方法仍然使用相同的过程:即通过索引查找方法。

      但是,在通过接口引用从接口调用方法时,必须有一些替代机制来查找方法实现指针。因为一个类可以实现多个接口,所以方法不可能在 vtable 中始终具有相同的索引(例如)。有多种可能的方法来解决这个问题,但没有一种方法比简单的 vtable 调度更有效。

      但是,正如 cmets 中所述,它可能与现代 Java VM 实现没有太大区别。

      【讨论】:

      • 我想知道 JVM 是否使用了 C++ 常用的技术来支持多重继承。 (多个 vtables,指针 thunking 等)
      • @seand,我不这么认为,因为垃圾收集。当您可以确定指针始终指向对象的 start 时,实现 GC 会容易得多。不过,这也不是不可能。当然,thunking 也有性能损失。
      • 这可能都发生在 JVM impl 代码的深处,而不是 GC'd。 JIT 等可能正在动态构建 vtable。我没有尝试阅读代码,但在我看来这是一种合理的方式。
      • 你错过了我的意思。接口引用是指向某物的指针。在 Java 中,它很可能指向对象本身。在 C++ 中,基类指针可以指向派生类对象的中间。
      • @devouredelysium 如果标头与字段的偏移量是固定的,则等同于同一件事。
      【解决方案4】:

      这是 Bozho 示例的变体。它运行时间更长并重复使用相同的对象,因此缓存大小无关紧要。我还使用了一个数组,所以迭代器没有开销。

      public static void main(String[] args) {
          Random random = new Random();
          int testLength = 200 * 1000 * 1000;
          Foo[] foos = new Foo[testLength];
          Bar[] bars = new Bar[testLength];
          Foo1Impl foo1 = new Foo1Impl();
          Foo2Impl foo2 = new Foo2Impl();
          Bar1Impl bar1 = new Bar1Impl();
          Bar2Impl bar2 = new Bar2Impl();
          for (int i = 0; i < testLength; i++) {
              boolean flip = random.nextBoolean();
              foos[i] = flip ? foo1 : foo2;
              bars[i] = flip ? bar1 : bar2;
          }
          long start;
          start = System.nanoTime();
          for (Foo foo : foos) {
              foo.foo();
          }
          System.out.printf("The average abstract method call was %.1f ns%n", (double) (System.nanoTime() - start) / testLength);
          start = System.nanoTime();
          for (Bar bar : bars) {
              bar.bar();
          }
          System.out.printf("The average interface method call was %.1f ns%n", (double) (System.nanoTime() - start) / testLength);
      }
      

      打印

      The average abstract method call was 4.2 ns
      The average interface method call was 4.1 ns
      

      如果你交换测试运行的顺序,你会得到

      The average interface method call was 4.2 ns
      The average abstract method call was 4.1 ns
      

      你运行测试的方式比你选择的有更多的不同。

      我使用 Java 6 update 26 和 OpenJDK 7 得到了相同的结果。


      顺便说一句:如果你添加一个每次只调用同一个对象的循环,你会得到

      The direct method call was 2.2 ns
      

      【讨论】:

      • 所以接口比直接接口慢两倍。这是JIT编译后的吗?
      • @Kevin Kostlan:也许你不应该忽略点之前的数字。 4.24.1 的比例是not 2...
      【解决方案5】:

      我尝试编写一个测试来量化可能调用方法的所有各种方式。我的发现表明,方法是否是接口方法并不重要,重要的是您调用它的引用类型。通过类引用调用接口方法(相对于调用次数)比通过接口引用在同一个类上调用相同方法要快得多。

      1,000,000 次调用的结果是...

      通过接口引用的接口方法:(nanos, millis) 5172161.0, 5.0

      通过抽象引用的接口方法:(nanos, millis) 1893732.0, 1.8

      通过顶层派生参考的接口方法:(nanos, millis) 1841659.0, 1.8

      通过具体类引用的具体方法:(nanos, millis) 1822885.0, 1.8

      请注意,结果的前两行是对完全相同的方法的调用,但通过不同的引用。

      这是代码...

      package interfacetest;
      
      /**
       *
       * @author rpbarbat
       */
      public class InterfaceTest
      {
          static public interface ITest
          {
              public int getFirstValue();
              public int getSecondValue();
          }
      
          static abstract public class ATest implements ITest
          {
              int first = 0;
      
              @Override
              public int getFirstValue()
              {
                  return first++;
              }
          }
      
          static public class TestImpl extends ATest
          {
              int second = 0;
      
              @Override
              public int getSecondValue()
              {
                  return second++;
              }
          }
      
          static public class Test
          {
              int value = 0;
      
              public int getConcreteValue()
              {
                  return value++;
              }
          }
      
          static int loops = 1000000;
      
          /**
           * @param args the command line arguments
           */
          public static void main(String[] args)
          {
              // Get some various pointers to the test classes
              // To Interface
              ITest iTest = new TestImpl();
      
              // To abstract base
              ATest aTest = new TestImpl();
      
              // To impl
              TestImpl testImpl = new TestImpl();
      
              // To concrete
              Test test = new Test();
      
              System.out.println("Method call timings - " + loops + " loops");
      
      
              StopWatch stopWatch = new StopWatch();
      
              // Call interface method via interface reference
              stopWatch.start();
      
              for (int i = 0; i < loops; i++)
              {
                  iTest.getFirstValue();
              }
      
              stopWatch.stop();
      
              System.out.println("interface method via interface reference: (nanos, millis)" + stopWatch.getElapsedNanos() + ", " + stopWatch.getElapsedMillis());
      
      
              // Call interface method via abstract reference
              stopWatch.start();
      
              for (int i = 0; i < loops; i++)
              {
                  aTest.getFirstValue();
              }
      
              stopWatch.stop();
      
              System.out.println("interface method via abstract reference: (nanos, millis)" + stopWatch.getElapsedNanos() + ", " + stopWatch.getElapsedMillis());
      
      
              // Call derived interface via derived reference
              stopWatch.start();
      
              for (int i = 0; i < loops; i++)
              {
                  testImpl.getSecondValue();
              }
      
              stopWatch.stop();
      
              System.out.println("interface via toplevel derived reference: (nanos, millis)" + stopWatch.getElapsedNanos() + ", " + stopWatch.getElapsedMillis());
      
      
              // Call concrete method in concrete class
              stopWatch.start();
      
              for (int i = 0; i < loops; i++)
              {
                  test.getConcreteValue();
              }
      
              stopWatch.stop();
      
              System.out.println("Concrete method via concrete class reference: (nanos, millis)" + stopWatch.getElapsedNanos() + ", " + stopWatch.getElapsedMillis());
          }
      }
      
      
      package interfacetest;
      
      /**
       *
       * @author rpbarbat
       */
      public class StopWatch
      {
          private long start;
          private long stop;
      
          public StopWatch()
          {
              start = 0;
              stop = 0;
          }
      
          public void start()
          {
              stop = 0;
              start = System.nanoTime();
          }
      
          public void stop()
          {
              stop = System.nanoTime();
          }
      
          public float getElapsedNanos()
          {
              return (stop - start);
          }
      
          public float getElapsedMillis()
          {
              return (stop - start) / 1000;
          }
      
          public float getElapsedSeconds()
          {
              return (stop - start) / 1000000000;
          }
      }
      

      这是使用 Oracles JDK 1.6_24。希望这有助于解决这个问题......

      问候,

      罗德尼·巴巴蒂

      【讨论】:

        【解决方案6】:

        接口比抽象类慢,因为方法调用的运行时间决定会增加一点时间损失,

        但是,由于 JIT 会处理重复调用相同方法的问题,因此您可能只会在第一次调用时看到性能滞后,这也是非常小的,

        现在对于 Java 8,他们通过添加默认和静态函数几乎使抽象类变得无用,

        【讨论】:

          猜你喜欢
          • 2020-12-09
          • 1970-01-01
          • 2014-05-21
          • 2020-06-07
          • 1970-01-01
          • 2011-09-18
          • 2019-08-24
          • 1970-01-01
          相关资源
          最近更新 更多