Java 随着接口实现的增多,方法调用成本是否保持不变

Java 随着接口实现的增多,方法调用成本是否保持不变,java,c#,performance,Java,C#,Performance,假设我们有一个DoSomething方法的接口。 我们创建了多个类来实现它。示例5/10/100/1000新类定义 不管列表中添加了哪些唯一的类,迭代列表和调用DoSomething都需要相同的时间。假设列表包含相同数量的对象。不同类型的比较列表与相同类型的列表 本质上,我是在问接口方法调用是否总是一个恒定的运行时成本,或者它取决于它背后的唯一类型 我为什么要问这个?我需要了解界面在场景后面是如何工作的,以避免引发意外的开销 编辑:我能把问题再难一点吗? 让我们考虑一下相反的场景,我们现在只有1

假设我们有一个DoSomething方法的接口。 我们创建了多个类来实现它。示例5/10/100/1000新类定义

不管列表中添加了哪些唯一的类,迭代列表和调用DoSomething都需要相同的时间。假设列表包含相同数量的对象。不同类型的比较列表与相同类型的列表

本质上,我是在问接口方法调用是否总是一个恒定的运行时成本,或者它取决于它背后的唯一类型

我为什么要问这个?我需要了解界面在场景后面是如何工作的,以避免引发意外的开销

编辑:我能把问题再难一点吗? 让我们考虑一下相反的场景,我们现在只有1个类但是N个接口它实现了一个…无论它实现的接口是什么,性能都是相同的。假设大多数接口最终指向同一个方法

Edit2:adjan的回答提出了另一个潜在问题: 他说c为每个接口生成一个v表=>其所有方法的成本为O1

让我们考虑下面的场景。 垂直继承链:A0扩展A1扩展A2。。。扩展一个。 所有这些类都是我们构造的,里面没有变量。 我们刚刚创建了一个性能bug

A0.方法成本。 方法成本

如果接口可以以恒定的开销完成这项工作,那么如果有人滥用继承机制并创建深度为N的垂直树状分支,为什么还要支付可变成本呢


A0虚拟方法的成本是多少?那么它的构造函数或析构函数呢?我们100%安全地假设它需要恒定的运行时成本。关于构造函数,调用A0的构造函数会触发A1的无参数构造函数,因此调用链将继续。。。因此,在成本上,首先;这种过早的优化是疯狂的。通过接口调用所涉及的开销并不重要

也就是说,除非处理分支预测,否则混合类型不会对运行时产生影响。看


这样做的原因是,无论虚拟函数指针表是否反复选择同一个指针表,都会导致通过虚拟函数指针表进行调用的开销。真正地这不是你需要担心的事情。

首先;这种过早的优化是疯狂的。通过接口调用所涉及的开销并不重要

也就是说,除非处理分支预测,否则混合类型不会对运行时产生影响。看


这样做的原因是,无论虚拟函数指针表是否反复选择同一个指针表,都会导致通过虚拟函数指针表进行调用的开销。真正地这不是你需要担心的事情。

没有区别

java和C++等语言以类似C++的方式实现虚方法调用。粗略地说,每个类都有一个虚拟方法表,它是指向方法的指针数组。对于类或接口定义的每个虚拟方法,此表中有一个条目。虚拟方法调用只是从该表中的特定索引中选取一个指针并调用它所指向的函数。无论您有一个实现还是一百个实现,这都没有区别。运行时成本保持不变


在Java或.Net虚拟机中,没有任何操作的运行时复杂性是现有类实现数量的函数。

没有区别

java和C++等语言以类似C++的方式实现虚方法调用。粗略地说,每个类都有一个虚拟方法表,它是指向方法的指针数组。对于类或接口定义的每个虚拟方法,此表中有一个条目。虚拟方法调用只是从该表中的特定索引中选取一个指针并调用它所指向的函数。无论您有一个实现还是一百个实现,这都没有区别。运行时成本保持不变


在Java或.Net虚拟机中,没有任何操作的运行时复杂性是现有类实现数量的函数。

调用接口方法时,使用所谓的v表来确定被调用方法的内存地址。每个类型都有自己的v-table,其中包含指向每个实现方法的指针,并且该类型的每个对象都有指向其类型的v-table的指针。因此,在方法调用期间,没有分支或搜索正确的内存地址,因为指针总是 唯一地指向正确的v表和方法地址

另请参见此图:

编辑:关于你的第二个问题。没有区别。对虚拟方法的调用成本始终相同,因为您只访问相同数量的指针:

interface I1
{
   void DoIt();
}


interface I2
{
   void DoIt();
}


class A : I1, I2
{
   public void DoIt()
   {
      // do sth
   }
}

// Note: All v-tables point to the same implementation in A

I1 test1 = new A();
test1.DoIt(); // => lookup in I1's v-table

I2 test2 = (I2)test1;
test2.DoIt(); // => Lookup in I2's v-table

A a = (A)test2;
a.DoIt(); // => Lookup in A's v-table

图像来源:

调用接口方法时,使用所谓的v表来确定被调用方法的内存地址。每个类型都有自己的v-table,其中包含指向每个实现方法的指针,并且该类型的每个对象都有指向其类型的v-table的指针。因此,在方法调用期间,没有分支或搜索正确的内存地址,因为指针总是唯一地指向正确的v表和方法地址

另请参见此图:

编辑:关于你的第二个问题。没有区别。对虚拟方法的调用成本始终相同,因为您只访问相同数量的指针:

interface I1
{
   void DoIt();
}


interface I2
{
   void DoIt();
}


class A : I1, I2
{
   public void DoIt()
   {
      // do sth
   }
}

// Note: All v-tables point to the same implementation in A

I1 test1 = new A();
test1.DoIt(); // => lookup in I1's v-table

I2 test2 = (I2)test1;
test2.DoIt(); // => Lookup in I2's v-table

A a = (A)test2;
a.DoIt(); // => Lookup in A's v-table

图像来源:

关于您的编辑:这无关紧要。见我的答案。关于你的第二次编辑:没有调用链。无论继承树有多深,调用总是O1。没有搜索指向v表的右指针。当您声明变量时,指针已经确定。请参见:关于您的编辑:这无关紧要。见我的答案。关于你的第二次编辑:没有调用链。无论继承树有多深,调用总是O1。没有搜索指向v表的右指针。声明变量时,指针已确定。请参见: