内联一个常用方法
Inline a common method
我想在不知道谁实现 TraitA
的情况下内联 TraitA::x()
方法。
例如:
(注:StructB
实现了 TraitA
。)
let a: &TraitA = &StructB { x: 0f32 };
a.x(); // This should access `a.x` field directly
// as `TraitA::x()` always return the same field,
// the same `b.x` field slot.
该方法将在不同的结构中以相同的方式实现。
trait TraitA {
fn x(&self) -> f32;
}
struct StructB {
x: f32
}
impl TraitA for StructB {
#[inline]
fn x(&self) -> f32 {
self.x
}
}
a.x()
会内联到 (a as &StructB).x
吗?
我可以在 C++ 上做到这一点,例如:
class A {
float _x;
public:
float x() {
return _x;
}
};
struct B : A {};
int main() {
A* a = new B;
a->x();
}
根据Can #[inline] be used in both trait method declarations and implementations? and this thread on the language forum,不可能告诉编译器应该内联特定方法的所有实现。然而,由于 Rust 是静态类型的,特征可以在编译时消除歧义,所以我认为 Rust 编译器没有理由不能内联特定的方法实现。我不能肯定地告诉你它是否真的会这样做,因为 #[inline]
属性只是编译器可以自由忽略的建议,但即使我们谈论的是独立函数而不是方法。换句话说,内联方法应该是可能的,但在任何情况下你都不应该依赖你的函数被内联。
我认为您在这里混淆了一些事情。首先:当你在纯虚拟上下文中使用它时,不可能从特征中内联一个方法(也就是说:你没有关于实际类型的信息)。这看起来像这样:
fn unknown_type(foo: &MyTrait) -> f32 {
foo.x()
}
在这里,编译器不可能知道特征对象背后的实际类型foo
。因此,它被迫使用 vtable 并进行动态调度来调用该方法。有一个名为 devirtualization 的优化试图猜测正确的类型来执行静态分派(甚至内联方法),但这种优化有其局限性。
Will a.x()
get inlined to (a as &StructB).x
?
在大多数情况下是的,但这与您的内联属性或特征对象无关。在您的小示例中,编译器可以看到整个函数并且知道 a
具有基础类型 StructB
。但同样,这不是一个纯粹的虚拟上下文:编译器有它可以使用的类型信息。
另一件事:这与 C# 中 Java/f
中的 final
无关——如 the first version of your question 中所述。
这些关键字仅对 class 层次结构有用。原则上,由于您可以在派生的 class 中覆盖 Java/C# 中每个 class 的所有方法,编译器在理论上永远不会内联或静态分派方法。它总是必须检查 vtable 以检查是否有此方法的更专业版本。当编译器有一个 class 的变量声明该方法为 final
时,它可以静态调用它,因为它保证它不会被覆盖。
但在 Rust 中拥有一个特征对象(关于这个问题)等同于拥有一个具有接口类型的变量。而且您(显然)不能将接口方法声明为 final
。所以在这里,编译器必须进行虚拟分派,而不管某些实现 classes 是否将它们的实现声明为 final
.
此外,您的 C++ 与您的要求没有任何关系。 base class A
声明了一个带有方法体的非虚函数。这意味着该函数永远不会在虚拟上下文中调用。但是在你的 Rust 代码中 x()
没有主体并且可以在虚拟上下文中使用。
在 Rust 中没有类似的东西,但您可以通过 impl Trait { ... }
向特征对象添加方法。该特征的实现者无法覆盖这些方法,因此编译器可以轻松地执行静态分派或内联方法。
您可以看到该示例及其程序集 here。
回答我认为你实际要问的问题:
You want to inline that trait method call so that it's as cheap/fast as a simple field access of a concrete type, right?
再次强调,这在纯虚拟环境中是不可能的。当编译器不知道实现类型时,它无法生成简单的字段访问,因为它不知道该字段相对于基指针的偏移量!我的意思是,并不是所有的实现类型都保证具有相同的内存布局,并且 x
始终保持在相同的位置。
但是,可以比方法做得更好:不调用 getter 方法,可以将字段偏移量存储在 vtable 中并使用索引该字段。这仍然比标准字段访问更昂贵,但比方法调用更快。
这正是 this RFC: "Allow fields in traits that map to lvalues in impl'ing type" 中提出的建议。 RFC 线程已关闭,但 RFC 仍在开发中。
所以你现在不能那样做。使用特征方法是您现在可以做的最好的事情。
我想在不知道谁实现 TraitA
的情况下内联 TraitA::x()
方法。
例如:
(注:StructB
实现了 TraitA
。)
let a: &TraitA = &StructB { x: 0f32 };
a.x(); // This should access `a.x` field directly
// as `TraitA::x()` always return the same field,
// the same `b.x` field slot.
该方法将在不同的结构中以相同的方式实现。
trait TraitA {
fn x(&self) -> f32;
}
struct StructB {
x: f32
}
impl TraitA for StructB {
#[inline]
fn x(&self) -> f32 {
self.x
}
}
a.x()
会内联到 (a as &StructB).x
吗?
我可以在 C++ 上做到这一点,例如:
class A {
float _x;
public:
float x() {
return _x;
}
};
struct B : A {};
int main() {
A* a = new B;
a->x();
}
根据Can #[inline] be used in both trait method declarations and implementations? and this thread on the language forum,不可能告诉编译器应该内联特定方法的所有实现。然而,由于 Rust 是静态类型的,特征可以在编译时消除歧义,所以我认为 Rust 编译器没有理由不能内联特定的方法实现。我不能肯定地告诉你它是否真的会这样做,因为 #[inline]
属性只是编译器可以自由忽略的建议,但即使我们谈论的是独立函数而不是方法。换句话说,内联方法应该是可能的,但在任何情况下你都不应该依赖你的函数被内联。
我认为您在这里混淆了一些事情。首先:当你在纯虚拟上下文中使用它时,不可能从特征中内联一个方法(也就是说:你没有关于实际类型的信息)。这看起来像这样:
fn unknown_type(foo: &MyTrait) -> f32 {
foo.x()
}
在这里,编译器不可能知道特征对象背后的实际类型foo
。因此,它被迫使用 vtable 并进行动态调度来调用该方法。有一个名为 devirtualization 的优化试图猜测正确的类型来执行静态分派(甚至内联方法),但这种优化有其局限性。
Will
a.x()
get inlined to(a as &StructB).x
?
在大多数情况下是的,但这与您的内联属性或特征对象无关。在您的小示例中,编译器可以看到整个函数并且知道 a
具有基础类型 StructB
。但同样,这不是一个纯粹的虚拟上下文:编译器有它可以使用的类型信息。
另一件事:这与 C# 中 Java/f
中的 final
无关——如 the first version of your question 中所述。
这些关键字仅对 class 层次结构有用。原则上,由于您可以在派生的 class 中覆盖 Java/C# 中每个 class 的所有方法,编译器在理论上永远不会内联或静态分派方法。它总是必须检查 vtable 以检查是否有此方法的更专业版本。当编译器有一个 class 的变量声明该方法为 final
时,它可以静态调用它,因为它保证它不会被覆盖。
但在 Rust 中拥有一个特征对象(关于这个问题)等同于拥有一个具有接口类型的变量。而且您(显然)不能将接口方法声明为 final
。所以在这里,编译器必须进行虚拟分派,而不管某些实现 classes 是否将它们的实现声明为 final
.
此外,您的 C++ 与您的要求没有任何关系。 base class A
声明了一个带有方法体的非虚函数。这意味着该函数永远不会在虚拟上下文中调用。但是在你的 Rust 代码中 x()
没有主体并且可以在虚拟上下文中使用。
在 Rust 中没有类似的东西,但您可以通过 impl Trait { ... }
向特征对象添加方法。该特征的实现者无法覆盖这些方法,因此编译器可以轻松地执行静态分派或内联方法。
您可以看到该示例及其程序集 here。
回答我认为你实际要问的问题:
You want to inline that trait method call so that it's as cheap/fast as a simple field access of a concrete type, right?
再次强调,这在纯虚拟环境中是不可能的。当编译器不知道实现类型时,它无法生成简单的字段访问,因为它不知道该字段相对于基指针的偏移量!我的意思是,并不是所有的实现类型都保证具有相同的内存布局,并且 x
始终保持在相同的位置。
但是,可以比方法做得更好:不调用 getter 方法,可以将字段偏移量存储在 vtable 中并使用索引该字段。这仍然比标准字段访问更昂贵,但比方法调用更快。
这正是 this RFC: "Allow fields in traits that map to lvalues in impl'ing type" 中提出的建议。 RFC 线程已关闭,但 RFC 仍在开发中。
所以你现在不能那样做。使用特征方法是您现在可以做的最好的事情。