为什么转换数组(向量)如此缓慢?

8
我曾经认为在.NET中转换(而非将类型转换)是很便宜和快速的。然而,对于数组,事实似乎并非如此。我试图做一个非常简单的转换,将T1[]作为T2[]进行转换,其中T1:T2。
有三种方法可以做到这一点,我称之为以下三种方式:
DropCasting: T2[] array2 = array;
CastClass: (T2[])array;
IsInst: array as T2[]; 

我创建了一些方法来实现这个功能,但不幸的是,C# 似乎会根据此是否为泛型来创建一些相当奇怪的代码。(如果它是泛型,则 DropCasting 使用 castclass 操作符。在两种情况下,都拒绝发出 'as' 运算符,当 T1:T2 时)

无论如何,我编写了一些动态方法,并进行了测试,得到了一些惊人的结果(string[] => object[]):

DropCast :    223ms
IsInst   :   3648ms
CastClass:   3732ms

滴定法(Dropcasting)比任何一个类型转换运算符快约18倍。为什么数组类型转换这么慢?对于像字符串=>对象这样的普通对象,差异要小得多。
DropCast :    386ms
IsInst   :    611ms
CastClass:    519ms

以下是基准代码:
class Program
{
    static readonly String[] strings = Enumerable.Range(0, 10).Select(x => x.ToString()).ToArray();

    static Func<string[], object[]> Dropcast = new Func<Func<string[], object[]>>(
        () =>
        {
            var method = new DynamicMethod("DropCast", typeof(object[]), new[] { typeof(object), typeof(string[]) },true);
            var ilgen = method.GetILGenerator();
            ilgen.Emit(OpCodes.Ldarg_1);
            ilgen.Emit(OpCodes.Ret);
            return method.CreateDelegate(typeof(Func<string[], object[]>)) as Func<string[], object[]>;
        })();
    static Func<string[], object[]> CastClass = new Func<Func<string[], object[]>>(
        () =>
        {
            var method = new DynamicMethod("CastClass", typeof(object[]), new[] { typeof(object), typeof(string[]) },true);
            var ilgen = method.GetILGenerator();
            ilgen.Emit(OpCodes.Ldarg_1);
            ilgen.Emit(OpCodes.Castclass, typeof(object[]));
            ilgen.Emit(OpCodes.Ret);
            return method.CreateDelegate(typeof(Func<string[], object[]>)) as Func<string[], object[]>;
        })();

    static Func<string[], object[]> IsInst = new Func<Func<string[], object[]>>(
        () =>
        {
            var method = new DynamicMethod("IsInst", typeof(object[]), new[] { typeof(object), typeof(string[]) },true);
            var ilgen = method.GetILGenerator();
            ilgen.Emit(OpCodes.Ldarg_1);
            ilgen.Emit(OpCodes.Isinst, typeof(object[]));
            ilgen.Emit(OpCodes.Ret);
            return method.CreateDelegate(typeof(Func<string[], object[]>)) as Func<string[], object[]>;
        })();

    static Func<string[], object[]>[] Tests = new Func<string[], object[]>[]{
        Dropcast,
        IsInst,
        CastClass
    };
    static void Main(string[] args)
    {
        int maxMethodLength = Tests.Select(x => GetMethodName(x.Method).Length).Max();
        RunTests(1, false, maxMethodLength);
        RunTests(100000000, true, maxMethodLength);
    }

    static string GetMethodName(MethodInfo method)
    {
        return method.IsGenericMethod ?
        string.Format(@"{0}<{1}>", method.Name, string.Join<Type>(",", method.GetGenericArguments())) : method.Name;
    }

    static void RunTests(int count, bool displayResults, int maxLength)
    {
        foreach (var action in Tests)
        {
            Stopwatch sw = Stopwatch.StartNew();
            for (int i = 0; i < count; i++)
            {
                action(strings);
            }
            sw.Stop();
            if (displayResults)
            {
                Console.WriteLine("{0}: {1}ms", GetMethodName(action.Method).PadRight(maxLength),
                ((int)sw.ElapsedMilliseconds).ToString().PadLeft(6));
            }
            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.Collect();
        }
    }
}

在有人问之前,请编辑int []-> uint []等事物,这同样适用于clr规范,应该在没有转换的情况下进行转换。

重点是正确处理IL。在极其简单的方法中,例如()=>strings as object[];编译器将删除as方法。动态方法创建仅在程序的.cctor中运行一次。之后,每个方法都只是IL blob。此外,我为每个动态方法添加了一个“实例”(对象参数),只是为了避免在使用静态方法的委托时进行thunk shuffle。 - Michael B
是的,我第一次阅读时错过了第二层函数。所以我删除了我的评论。 ;) - Kirk Woll
1
已经多次涉及,只能找到主页:http://blogs.msdn.com/b/ericlippert/archive/2007/10/17/covariance-and-contravariance-in-c-part-two-array-covariance.aspx - Hans Passant
2个回答

0

对我来说,强制类型转换的成本(几乎)与使用as运算符的成本几乎相同是有道理的。在两种情况下,必须对对象的类型进行运行时检查,并确定它是否与目标类型兼容。检查是必需的,以使转换操作在必要时抛出InvalidCastException

换句话说,as运算符是一种强制类型转换操作 - 它还具有使转换失败而不抛出异常(通过返回null)的优点。这也可以通过is运算符和强制类型转换的组合来完成,但这将使工作量加倍。


我意识到正在进行类型检查,但为什么它要更加昂贵? - Michael B
比什么更昂贵?你自己的结果表明,强制转换和as运算符本质上是相同的。(正如我所主张的那样)而你的第一个“控制”示例是一个赋值语句——几乎是一个无操作。(对于你的“删除转换”,编译器已经完成了检查有效性所需的所有工作,因此没有运行时性能损失) - Kirk Woll
抱歉,我知道as(T[])都是强制转换,我的意思是为什么它们与将字符串转换为对象的控制案例相比如此缓慢。 - Michael B
1
因为就CLR而言,您的控制案例根本不是转换。它是一个分配,由编译器预先验证为合法。另一方面,运行时转换涉及大量工作来检查类型兼容性 - 在您的控制案例中,这个代价已经在编译时支付,因此没有运行时成本。 - Kirk Woll
@Kirk Woll 我认为你的最后一条评论是真正的答案。我试图愚弄CLR并尝试将string[]分配给int[],结果得到了“操作可能破坏运行时”的错误提示。 - Andrey
@Andrey 这并不是整个故事。如果你将 object[] 更改为 objectstring[] 更改为 string,则三种方法的运行时间几乎相同。 - Jeffrey Sax

0

因为你正在进行数组转换。

这3个IL代码片段之间的区别在于后两个添加了IsInst和CastClass操作。很少有关于类型的信息,所以CLR必须检查它是否是有效的操作。这需要时间。

CastClass和IsInst之间的轻微差异可以通过CastClass首先进行空值检查并且如果参数为空则立即成功来解释。

我怀疑减速是因为你正在数组之间进行转换。可能需要更多的工作来确保数组转换是有效的。可能需要查看每个元素以查看它是否可以转换为目标元素类型。因此,我猜测,与其在“内联”机器代码中执行所有这些操作,不如JIT发出对验证函数的调用。

实际上,如果您运行性能分析,您会发现这确实是正在发生的事情。将近90%的时间都花费在名为“JIT_ChkCastArray”的函数中。


这很有道理。只是对我来说似乎有点奇怪,如果T1:class,T2那么强制转换必须始终合法,那为什么还要进行检查呢? - Michael B
我的猜测是,由于转换数组相对较少,JIT(即时编译器)开发人员没有费心去优化它。编译器可以通过不首先发出CastClass或IsInst指令来轻松地进行优化。JIT资源有限,因此任何优化都是相对昂贵且必须得到证明的。 - Jeffrey Sax

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