为什么Mono不支持使用AOT进行通用接口实例化?

use*_*159 6 c# generics mono jit unity-game-engine

Mono文档有一个关于完整AOT不支持通用接口实例化的代码示例:

interface IFoo<T> {
... 
    void SomeMethod();
}
Run Code Online (Sandbox Code Playgroud)

它说:

由于Mono无法从静态分析中确定哪种方法将实现接口,IFoo<int>.SomeMethod因此不支持此特定模式."

所以我认为编译器无法在类型推断下使用此方法.但我仍然无法理解完全AOT限制的根本原因.

Unity AOT脚本限制仍存在类似问题.在以下代码中:

using UnityEngine;
using System;

public class AOTProblemExample : MonoBehaviour, IReceiver 
{
    public enum AnyEnum {
        Zero,
        One,
    }

    void Start() {
        // Subtle trigger: The type of manager *must* be
        // IManager, not Manager, to trigger the AOT problem.
        IManager manager = new Manager();
        manager.SendMessage(this, AnyEnum.Zero);
    }

    public void OnMessage<T>(T value) {
        Debug.LogFormat("Message value: {0}", value);
    }
}

public class Manager : IManager {
    public void SendMessage<T>(IReceiver target, T value) {
        target.OnMessage(value);
    }
}

public interface IReceiver {
    void OnMessage<T>(T value);
}

public interface IManager {
    void SendMessage<T>(IReceiver target, T value);
}
Run Code Online (Sandbox Code Playgroud)

我很困惑:

该AOT编译器并没有意识到它应该为泛型方法生成的代码OnMessageTAnyEnum,所以幸福下去,跳过此方法.当调用该方法,并且运行时无法找到要执行的正确代码时,它会放弃此错误消息.

当JIT可以推断出类型时,为什么AOT不知道类型?谁能提供详细的答案?

The*_*kis 11

在描述问题之前,请考虑我的另一个答案的摘录,该答案描述了支持动态代码生成的平台上的泛型情况:

在C#泛型中,泛型类型定义在运行时在内存中维护.每当需要新的具体类型时,运行时环境就会组合泛型类型定义和类型参数,并创建新类型(具体化).因此,我们在运行时为每个类型参数组合获取一个新类型.

运行时的短语是关键,因为它将我们带到了另一个点:

这种实现技术在很大程度上依赖于运行时支持和JIT编译(这就是为什么你经常听到C#泛型在iOS等平台上有一些限制,因为动态代码生成受到限制).

那么完整的AOT编译器也可以这样做吗?它当然是可能的.但这很容易吗?

来自微软研究院的预编译.NET泛型的论文描述与AOT编译仿制药之间的相互作用,重点介绍了一些潜在的问题,并提出解决方案.在这个答案中,我将使用该论文来试图说明为什么.NET泛型未被广泛预编译(还).

一切都必须实例化

考虑你的例子:

IManager manager = new Manager();
manager.SendMessage(this, AnyEnum.Zero);
Run Code Online (Sandbox Code Playgroud)

显然我们在IManager.SendMessage<AnyEnum>这里调用方法,因此完全AOT编译器需要编译该方法.

但这是一个接口调用,它实际上是一个虚拟调用,这意味着我们无法提前知道将调用哪个接口方法的实现.

JIT编译器不关心这个问题.当有人试图运行尚未编译的方法时,将通知JIT并且它将懒惰地编译该方法.

相反,完全AOT编译器无法访问所有此运行时类型信息.因此,它必须在接口的所有实现上悲观地编译泛型方法的所有可能的实例化.(或者只是放弃而不是提供该功能.)

泛型可以是无限递归的

object M<T>(long n)
{
    if (n == 1)
    {
        return new T[]();
    }
    else 
    {
        return M<T[]>(n - 1);
    }
}
Run Code Online (Sandbox Code Playgroud)

要实例化M<int>(),编译器需要实例化int[]M<int[]>().要实例化M<int[]>(),编译器需要实例化int[][]M<int[][]>().要实例化M<int[][]>(),编译器需要实例化int[][][]M<int[][][]>().

这可以通过使用代表性实例来解决(就像JIT编译器使用的那样).这意味着作为引用类型的所有泛型参数都可以共享其代码.所以:

  • int[][],int[][][],int[][][][](等)都可以共享相同的代码,因为他们是引用数组.
  • M<int[]>,M<int[][]>,M<int[][][]>(等)都可以共享相同的代码,因为他们对引用进行操作.

大会需要拥有他们的仿制品......

由于C#程序是在程序集中编译的,因此很难准确地告诉谁应该"拥有"每种类型的实例化.

  • Assembly1声明类型G<T>.
  • Assembly2(引用Assembly1)实例化类型G<int>.
  • Assembly3(引用Assembly1)也实例化类型G<int>.
  • AssemblyX(参考以上所有)想要使用G<int>.

哪个程序集可以编译实际G<int>?如果他们碰巧是独立的图书馆,没有Assembly2Assembly3可以不拥有各自的副本进行编译G<int>.所以我们已经在查看重复的本机代码了.

......那些仿制药必须仍然相互兼容

但是,当AssemblyX编译时,它G<int>应该使用哪个副本?显然,它必须能够处理两者,因为它可能需要接收G<int>来自或发送G<int>到任一组件.

但更重要的是,在C#中,您不能拥有两个具有相同的完全限定名称的类型,这些类型会变得不兼容.换一种说法:

G<int> obj = new G<int>();
Run Code Online (Sandbox Code Playgroud)

上述可从来没有理由不能在该G<int>(变量的类型)是G<int>Assembly2,而G<int>(构造函数的类型)是G<int>Assembly3.如果因为这样的原因失败了,我们就再也不在C#了!

因此,两种类型都需要存在,并且需要透明地兼容,即使它们是单独编译的.为此,需要在链接时操作类型句柄,以保持语言的语义,包括它们应该可以相互分配的事实,它们的类型句柄应该相等(例如当使用typeof),等等.


Gil*_*man 4

Unity使用旧版本的Mono的Full-AOT,不支持通用接口方法。

这是由于泛型在 JIT 中与本机代码中的表示方式不同。(我想详细说明,但坦白说,我不相信自己是准确的)

新版本的 Mono 的 AOT 编译器解决了这个问题(当然,还有其他限制),但 Unity 保留了旧版本的 Mono。(我想我记得听说他们将方法从 AOT 更改为其他方法,但我不确定它是如何工作的)。


我不完全理解该主题警告

例如,C++ 中处理“泛型”的方式(编译为汇编、二进制)是使用称为模板的语言机制。这些模板更像是美化的宏,并且实际上为所使用的每种类型生成了不同的代码。(编辑:实际上,C# 泛型和 C++ 模板之间存在更多差异,但出于本答案的目的,我将它们视为等效)。

例如; 对于以下代码:

template<typename T>
class Foo
{
public:
    T GetValue() { return value; }
    void SetValue(T a) {value = a;}
private:
    T value;
};

int main()
{
    Foo<int> a;
    Foo<char *> b;

    a.SetValue(0);
    b.SetValue((char*)0);

    a.GetValue();
    b.GetValue();

    return 0;
}
Run Code Online (Sandbox Code Playgroud)

将生成以下函数(通过运行获得nm --demangle

00000000004005e4 W Foo<int>::GetValue()
00000000004005b2 W Foo<int>::SetValue(int)
00000000004005f4 W Foo<char*>::GetValue()
00000000004005ca W Foo<char*>::SetValue(char*)
Run Code Online (Sandbox Code Playgroud)

这意味着对于您使用此类的每种类型,都会生成几乎相同代码的另一个实例(尽管我确信 GCC 足够聪明,可以优化一些明显的情况,例如 getter 和 setter,也许还有更多) )。

C# 的泛型稍微复杂一些。这是Eric Lippert撰写的一篇非常有趣的文章。总而言之,编译后的 C# 泛型代码只有一个实例,即generic,并且依赖于类型的内容是在运行时计算的。

将 C# 代码转换为本机/机器代码时(这本质上就是 AOT 所做的),转换泛型时会出现问题。

这就是这个主题对我来说有点模糊的地方。我只能假设 AOT 代码不保留运行时类型信息,因此对于一般情况,它需要每个类型的代码。

当接收 IFooable 类型的对象时,本机虚拟表格式可能不够详细,无法找到正确的实现;尽管我承认我不知道为什么会这样,也不知道 AOT 代码的虚拟表的确切细节(它与 C++ 的虚拟表相同吗?)