通用方法和非通用方法的性能差异

3
假设我们举例需要处理不同类型的矩阵线性代数问题,而我们有一个自定义的矩阵类(Matrix class)实现如下:
interface IMatrix
{
    double this[int i, int j] { get; set; }
    int Size { get; }
}

我希望实现矩阵乘法。我之前认为两种方法都可以:

static void Multiply<TMatrix>(TMatrix a, TMatrix b, TMatrix result) where TMatrix : IMatrix

并且

static void Multiply(Matrix a, Matrix b, Matrix result)

(当然,实现类似的情况下)这将在内部生成完全相同的IL代码,因此具有相同的性能表现。但事实并非如此:第一个比第二个慢四倍。从IL代码来看,通用型的代码似乎类似于通过接口进行调用:
static void Multiply(IMatrix a, IMatrix b, IMatrix result)

我错过了什么吗?是否有一种方法可以通过泛型实现与直接调用相同的性能?

已安装Framework 4.8,目标Framework:4.7.2(也测试过.Net Core 3)

方法实现:

static void Multiply(Matrix a, Matrix b, Matrix result)
{
    for (int i = 0; i < a.Size; i++)
    {
        for (int j = 0; j < a.Size; j++)
        {
            double temp = 0;
            for (int k = 0; k < a.Size; k++)
            {
                temp += a[i, k] * b[k, j];
            }
            result[i, j] = temp;
        }
    }
}

最小可复现示例


你能否提供一个 [mcve],其中包含一个完全可执行的示例,两者都包含在内?这样更容易让其他人测试。 - Lasse V. Karlsen
@LasseV.Karlsen 好主意,我已经编辑了问题,并提供了一个最小可重现的示例链接。 - Frédéric Delanchy
不应该,因为可能在内部使用了反射。 - SILENT
在泛型变量中使用索引器访问矩阵元素时,会出现装箱操作,IL 代码会显示 box !!0/*TMatrix*/ 指令。这可能会导致性能下降,因为装箱是一项非常昂贵的操作。但现在无法确定为什么代码会出现装箱情况。 - Pavel Anikhouski
@FrédéricDelanchy,已经有一个线程解释了为什么会对泛型类型参数执行装箱操作。这个原因以及我之前的评论都可能是性能差异的原因。在非泛型变体中没有装箱操作。 - Pavel Anikhouski
@PavelAnikhouski 我忘记说明了,即使我的项目针对4.7.2 .Net Framework,但我计算机上安装的框架是4.8。我在IL中没有看到任何装箱。 - Frédéric Delanchy
1个回答

4

.NET只会为所有引用类型生成一次通用方法的代码,而且该代码必须通过IMatrix接口进行调用,因为各种实现类型可能使用不同的方法实现接口。因此,这只是一个接口调用。

但是,如果您将Matrix更改为struct而不是class,JITter将生成特定于类型的通用方法实现,并且在其中接口调用可以被优化掉。


刚测试了一下:它可以工作。你知道为什么它不能生成引用类型的特定类型实现吗? - Frédéric Delanchy
这是JIT中的性能权衡。想象一下,如果List<T>为每个类型参数生成不同的代码,会发生什么。 - David Browne - Microsoft

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接