C# 'is'运算符的性能表现

123

我有一个需要快速运行的程序。在其中一个内部循环中,我需要测试对象的类型,以查看它是否继承自某个接口。

一种方法是使用CLR内置的类型检查功能。最优雅的方法可能是使用 'is' 关键字:

if (obj is ISpecialType)

另一种方法是给基类自己定义一个虚拟的GetType()函数,它返回预定义的枚举值(在我的情况下,实际上只需要一个布尔值)。 这种方法会很快,但不够优雅。

我听说有一个针对'is'关键字的IL指令,但这并不意味着在转换成本地汇编时它执行得很快。 有人能分享一些关于'is'与其他方法性能的见解吗?

更新:感谢所有明智答案!答案中有几个有用的要点:Andrew提到的'is'自动执行强制类型转换是必要的,而Binary Worrier和Ian收集的性能数据也非常有用。 如果其中一个答案被编辑以包含所有这些信息将会很好。


2
顺便提一下,CLR 不会为您提供创建自己的 GetType() 函数的可能性,因为它违反了 CLR 的主要规则 - 真正的类型。 - abatishchev
1
嗯,我不完全确定您所说的“真实类型”规则是什么意思,但我了解到CLR具有内置的Type GetType()函数。如果我要使用该方法,它将与返回某些枚举的其他名称函数一起使用,因此不会有任何名称/符号冲突。 - JubJub
3
我认为abatishchev的意思是“类型安全”。GetType()是非虚拟的,以防止类型欺骗自己,从而保持类型安全。 - Andrew Hare
2
你有没有考虑预取和缓存类型的合规性,这样你就不必在循环内部执行它了?似乎每个性能问题总是被大量+1,但对我来说,这似乎只是对c#的理解不够好。它真的太慢了吗?怎么会呢?你尝试过什么?显然,鉴于你对答案的评论,你并没有尝试过太多... - Gusdor
8个回答

118

如果你在使用 is 后转换为已检查过的类型,那么会影响性能。实际上,is 会将对象强制转换为你正在检查的类型,因此任何后续的转换都是多余的。

如果你仍然要进行转换,以下是更好的方法:

ISpecialType t = obj as ISpecialType;

if (t != null)
{
    // use t here
}

2
谢谢。但是如果条件不成立时我不打算转换对象,那么使用虚函数来测试类型是否更好呢? - JubJub
4
дёҖдёӘеӨұиҙҘзҡ„asж“ҚдҪңе’Ңisеҹәжң¬дёҠжү§иЎҢзӣёеҗҢзҡ„ж“ҚдҪңпјҲеҚізұ»еһӢжЈҖжҹҘпјүпјҢе”ҜдёҖзҡ„еҢәеҲ«жҳҜе®ғиҝ”еӣһnullиҖҢдёҚжҳҜfalseгҖӮ - Konrad Rudolph
13
近几年来,我们获得了使用以下模式的能力:if (obj is ISpecialType t) { t.DoThing(); }。请注意,这是一种编程语言中的语法结构。 - Licht
5
Stackoverflow应该删除过时的答案,因为这些答案会误导未来的人。 - huang

84

我同意Ian的观点,你可能不想这样做。

不过,只是让你知道,在10,000,000次迭代中,两者之间的差别很小。

  • 枚举类型检查需要约700毫秒
  • IS关键字检查需要约1000毫秒

个人而言,我不会用这种方式解决这个问题,但如果必须选择一种方法,我会选择内置的IS检查,因为性能差异不值得考虑编码开销。

我的基类和派生类

class MyBaseClass
{
    public enum ClassTypeEnum { A, B }
    public ClassTypeEnum ClassType { get; protected set; }
}

class MyClassA : MyBaseClass
{
    public MyClassA()
    {
        ClassType = MyBaseClass.ClassTypeEnum.A;
    }
}
class MyClassB : MyBaseClass
{
    public MyClassB()
    {
        ClassType = MyBaseClass.ClassTypeEnum.B;
    }
}

JubJub:根据要求提供更多测试信息。

我从控制台应用程序(调试版本)中运行了两个测试,每个测试看起来像以下内容:

static void IsTest()
{
    DateTime start = DateTime.Now;
    for (int i = 0; i < 10000000; i++)
    {
        MyBaseClass a;
        if (i % 2 == 0)
            a = new MyClassA();
        else
            a = new MyClassB();
        bool b = a is MyClassB;
    }
    DateTime end = DateTime.Now;
    Console.WriteLine("Is test {0} miliseconds", (end - start).TotalMilliseconds);
}

在发布模式下运行,我和Ian一样得到了60-70毫秒的差异。

进一步更新-2012年10月25日
离开几年后,我注意到关于这个问题的一些事情,编译器可以选择在发布模式下省略bool b = a is MyClassB,因为b没有在任何地方使用。

此代码...

public static void IsTest()
{
    long total = 0;
    var a = new MyClassA();
    var b = new MyClassB();
    var sw = new Stopwatch();
    sw.Start();
    for (int i = 0; i < 10000000; i++)
    {
        MyBaseClass baseRef;
        if (i % 2 == 0)
            baseRef = a;//new MyClassA();
        else
            baseRef = b;// new MyClassB();
        //bool bo = baseRef is MyClassB;
        bool bo = baseRef.ClassType == MyBaseClass.ClassTypeEnum.B;
        if (bo) total += 1;
    }
    sw.Stop();
    Console.WriteLine("Is test {0} miliseconds {1}", sw.ElapsedMilliseconds, total);
}

这段代码一直显示is检查大约需要57毫秒,而枚举比较则只需要29毫秒。

注意:我仍然更喜欢使用is检查,因为差异太小了,不值得关心。


42
实际测试性能比假设的要好,给你点赞。 - Jon Tackabury
3
使用Stopwatch类进行测试要比使用非常耗费资源的DateTime.Now更好。 - abatishchev
2
我会考虑到这个建议,但在这种情况下,我认为它不会影响结果。谢谢 :) - Binary Worrier
11
@Binary Worrier- 你的类的新操作符分配将完全掩盖“is”操作的任何性能差异。为什么不通过重复使用两个不同的预分配实例来移除这些新操作,并重新运行代码并发布结果呢? - user196088
1
@BinaryWorrier - 我同意,我们就保持不同意见吧 :) - mcmillab
显示剩余9条评论

25

好的,我刚刚与某人聊天并决定更多地测试这个内容。据我所知,与测试自己的成员或函数以存储类型信息相比,asis性能都非常好。

我使用了Stopwatch,但我刚学会它可能不是最可靠的方法,所以我还尝试了UtcNow。后来,我也尝试了处理器时间方法,这似乎类似于UtcNow,包括不可预测的创建时间。我还尝试使基类非抽象且没有虚拟函数,但似乎没有显著影响。

我在一台带有16GB RAM的Quad Q6600上运行了50百万次迭代,即使这样,数字仍然在+/- 50或如此毫秒之间反弹,因此我不会从小差异中读取太多内容。

有趣的是看到x64创建更快,但执行as/is却比x86慢。

x64 Release Mode:
Stopwatch:
As: 561ms
Is: 597ms
Base property: 539ms
Base field: 555ms
Base RO field: 552ms
Virtual GetEnumType() test: 556ms
Virtual IsB() test: 588ms
创建时间 : 10416ms

UtcNow:
As: 499ms
Is: 532ms
Base property: 479ms
Base field: 502ms
Base RO field: 491ms
Virtual GetEnumType(): 502ms
Virtual bool IsB(): 522ms
创建时间 : 285ms (使用UtcNow时这个数字似乎不可靠。我也得到了109ms和806ms。)

x86 Release Mode:
Stopwatch:
As: 391ms
Is: 423ms
Base property: 369ms
Base field: 321ms
Base RO field: 339ms
Virtual GetEnumType() test: 361ms
Virtual IsB() test: 365ms
创建时间 : 14106ms

UtcNow:
As: 348ms
Is: 375ms
Base property: 329ms
Base field: 286ms
Base RO field: 309ms
Virtual GetEnumType(): 321ms
Virtual bool IsB(): 332ms
创建时间 : 544ms (使用UtcNow时这个数字似乎不可靠。)

这是大部分代码:

    static readonly int iterations = 50000000;
    void IsTest()
    {
        Process.GetCurrentProcess().ProcessorAffinity = (IntPtr)1;
        MyBaseClass[] bases = new MyBaseClass[iterations];
        bool[] results1 = new bool[iterations];

        Stopwatch createTime = new Stopwatch();
        createTime.Start();
        DateTime createStart = DateTime.UtcNow;
        for (int i = 0; i < iterations; i++)
        {
            if (i % 2 == 0) bases[i] = new MyClassA();
            else bases[i] = new MyClassB();
        }
        DateTime createStop = DateTime.UtcNow;
        createTime.Stop();


        Stopwatch isTimer = new Stopwatch();
        isTimer.Start();
        DateTime isStart = DateTime.UtcNow;
        for (int i = 0; i < iterations; i++)
        {
            results1[i] =  bases[i] is MyClassB;
        }
        DateTime isStop = DateTime.UtcNow; 
        isTimer.Stop();
        CheckResults(ref  results1);

        Stopwatch asTimer = new Stopwatch();
        asTimer.Start();
        DateTime asStart = DateTime.UtcNow;
        for (int i = 0; i < iterations; i++)
        {
            results1[i] = bases[i] as MyClassB != null;
        }
        DateTime asStop = DateTime.UtcNow; 
        asTimer.Stop();
        CheckResults(ref  results1);

        Stopwatch baseMemberTime = new Stopwatch();
        baseMemberTime.Start();
        DateTime baseStart = DateTime.UtcNow;
        for (int i = 0; i < iterations; i++)
        {
            results1[i] = bases[i].ClassType == MyBaseClass.ClassTypeEnum.B;
        }
        DateTime baseStop = DateTime.UtcNow;
        baseMemberTime.Stop();
        CheckResults(ref  results1);

        Stopwatch baseFieldTime = new Stopwatch();
        baseFieldTime.Start();
        DateTime baseFieldStart = DateTime.UtcNow;
        for (int i = 0; i < iterations; i++)
        {
            results1[i] = bases[i].ClassTypeField == MyBaseClass.ClassTypeEnum.B;
        }
        DateTime baseFieldStop = DateTime.UtcNow;
        baseFieldTime.Stop();
        CheckResults(ref  results1);


        Stopwatch baseROFieldTime = new Stopwatch();
        baseROFieldTime.Start();
        DateTime baseROFieldStart = DateTime.UtcNow;
        for (int i = 0; i < iterations; i++)
        {
            results1[i] = bases[i].ClassTypeField == MyBaseClass.ClassTypeEnum.B;
        }
        DateTime baseROFieldStop = DateTime.UtcNow;
        baseROFieldTime.Stop();
        CheckResults(ref  results1);

        Stopwatch virtMethTime = new Stopwatch();
        virtMethTime.Start();
        DateTime virtStart = DateTime.UtcNow;
        for (int i = 0; i < iterations; i++)
        {
            results1[i] = bases[i].GetClassType() == MyBaseClass.ClassTypeEnum.B;
        }
        DateTime virtStop = DateTime.UtcNow;
        virtMethTime.Stop();
        CheckResults(ref  results1);

        Stopwatch virtMethBoolTime = new Stopwatch();
        virtMethBoolTime.Start();
        DateTime virtBoolStart = DateTime.UtcNow;
        for (int i = 0; i < iterations; i++)
        {
            results1[i] = bases[i].IsB();
        }
        DateTime virtBoolStop = DateTime.UtcNow;
        virtMethBoolTime.Stop();
        CheckResults(ref  results1);


        asdf.Text +=
        "Stopwatch: " + Environment.NewLine 
          +   "As:  " + asTimer.ElapsedMilliseconds + "ms" + Environment.NewLine
           +"Is:  " + isTimer.ElapsedMilliseconds + "ms" + Environment.NewLine
           + "Base property:  " + baseMemberTime.ElapsedMilliseconds + "ms" + Environment.NewLine + "Base field:  " + baseFieldTime.ElapsedMilliseconds + "ms" + Environment.NewLine + "Base RO field:  " + baseROFieldTime.ElapsedMilliseconds + "ms" + Environment.NewLine + "Virtual GetEnumType() test:  " + virtMethTime.ElapsedMilliseconds + "ms" + Environment.NewLine + "Virtual IsB() test:  " + virtMethBoolTime.ElapsedMilliseconds + "ms" + Environment.NewLine + "Create Time :  " + createTime.ElapsedMilliseconds + "ms" + Environment.NewLine + Environment.NewLine+"UtcNow: " + Environment.NewLine + "As:  " + (asStop - asStart).Milliseconds + "ms" + Environment.NewLine + "Is:  " + (isStop - isStart).Milliseconds + "ms" + Environment.NewLine + "Base property:  " + (baseStop - baseStart).Milliseconds + "ms" + Environment.NewLine + "Base field:  " + (baseFieldStop - baseFieldStart).Milliseconds + "ms" + Environment.NewLine + "Base RO field:  " + (baseROFieldStop - baseROFieldStart).Milliseconds + "ms" + Environment.NewLine + "Virtual GetEnumType():  " + (virtStop - virtStart).Milliseconds + "ms" + Environment.NewLine + "Virtual bool IsB():  " + (virtBoolStop - virtBoolStart).Milliseconds + "ms" + Environment.NewLine + "Create Time :  " + (createStop-createStart).Milliseconds + "ms" + Environment.NewLine;
    }
}

abstract class MyBaseClass
{
    public enum ClassTypeEnum { A, B }
    public ClassTypeEnum ClassType { get; protected set; }
    public ClassTypeEnum ClassTypeField;
    public readonly ClassTypeEnum ClassTypeReadonlyField;
    public abstract ClassTypeEnum GetClassType();
    public abstract bool IsB();
    protected MyBaseClass(ClassTypeEnum kind)
    {
        ClassTypeReadonlyField = kind;
    }
}

class MyClassA : MyBaseClass
{
    public override bool IsB() { return false; }
    public override ClassTypeEnum GetClassType() { return ClassTypeEnum.A; }
    public MyClassA() : base(MyBaseClass.ClassTypeEnum.A)
    {
        ClassType = MyBaseClass.ClassTypeEnum.A;
        ClassTypeField = MyBaseClass.ClassTypeEnum.A;            
    }
}
class MyClassB : MyBaseClass
{
    public override bool IsB() { return true; }
    public override ClassTypeEnum GetClassType() { return ClassTypeEnum.B; }
    public MyClassB() : base(MyBaseClass.ClassTypeEnum.B)
    {
        ClassType = MyBaseClass.ClassTypeEnum.B;
        ClassTypeField = MyBaseClass.ClassTypeEnum.B;
    }
}

49
生存抑或毁灭,这是问题: 在代码中坚持抽象基类的枚举和属性, 还是拿起中介语言者提供的帮助, 通过调用其指令来信任它们? 猜测:思考; 不再;然后凭借时间的感知结束头痛和千万个下意识的思考, 这是时限编码者所继承的。这是一个闭包, 虔诚地期望着。没有死亡,只是沉睡; 是的,我将沉睡,或许梦见从最基础的类中可以派生出什么。 - Jared Thirsk
我们能否得出这样的结论,即在x64上访问属性比访问字段更快!因为对我来说,这是一个惊喜,我不知道这是怎么回事? - Didier A.
1
我不会得出结论,因为:“即使进行了5000万次迭代,数字仍然在+/-50毫秒左右反弹,所以我不会过多关注微小差异。” - Jared Thirsk

22

Andrew Hare提到的关于在执行is检查和转换时性能损失的观点是正确的,但自从C# 7.0以后,我们可以使用模式匹配来进行is检查,以避免后续的额外转换:

if (obj is ISpecialType st)
{
   //st is in scope here and can be used
}

此外,如果您需要在多个类型之间进行检查,C# 7.0模式匹配结构现在允许您在类型上执行switch操作:

public static double ComputeAreaModernSwitch(object shape)
{
    switch (shape)
    {
        case Square s:
            return s.Side * s.Side;
        case Circle c:
            return c.Radius * c.Radius * Math.PI;
        case Rectangle r:
            return r.Height * r.Length;
        default:
            throw new ArgumentException(
                message: "shape is not a recognized shape",
                paramName: nameof(shape));
    }
}

您可以在此处的文档中了解有关C#模式匹配的更多信息。


1
一个有效的解决方案,但这种 C# 模式匹配功能让我感到沮丧,因为它鼓励编写“特性嫉妒”代码。我们应该努力实现逻辑的封装,只有派生对象“知道”如何计算它们自己的区域,然后它们只返回值即可。 - Dib
4
SO需要在问题页面上添加筛选按钮,用于过滤适用于更新版本的框架、平台等的答案。这个回答是正确回答C# 7的基础。 - Nick Westgate
5
当你使用无法控制的类型/类别/接口时,面向对象编程(OOP)的理想会被抛弃。当处理一个函数的结果,它可以返回完全不同类型中的多个值之一时,这种方法也很有用(因为C#还不支持联合类型,但是可以使用像OneOf<T...>这样的库,但它们存在重大缺陷)。 - Dai
1
@Dib 重新表述Dai的意思:如果你正在使用的类层次结构来自于一个你无法控制源代码的库,你就无法添加新的“virtual”函数。模式匹配通过允许你创建一个新函数,可以根据类型改变其行为而不必修改现有代码来解决这个问题。它也提供了一种更简单的方法来完成通常需要hacky和大量样板代码visitor模式的任务。 - Pharap

20

我进行了两种类型比较的性能比较:

  1. myobject.GetType() == typeof(MyClass)
  2. myobject is MyClass

结果是:使用 "is" 大约快10倍!!!

输出:

类型比较用时: 00:00:00.456

"is" 比较用时: 00:00:00.042

我的代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;

namespace ConsoleApplication3
{
    class MyClass
    {
        double foo = 1.23;
    }

    class Program
    {
        static void Main(string[] args)
        {
            MyClass myobj = new MyClass();
            int n = 10000000;

            Stopwatch sw = Stopwatch.StartNew();

            for (int i = 0; i < n; i++)
            {
                bool b = myobj.GetType() == typeof(MyClass);
            }

            sw.Stop();
            Console.WriteLine("Time for Type-Comparison: " + GetElapsedString(sw));

            sw = Stopwatch.StartNew();

            for (int i = 0; i < n; i++)
            {
                bool b = myobj is MyClass;
            }

            sw.Stop();
            Console.WriteLine("Time for Is-Comparison: " + GetElapsedString(sw));
        }

        public static string GetElapsedString(Stopwatch sw)
        {
            TimeSpan ts = sw.Elapsed;
            return String.Format("{0:00}:{1:00}:{2:00}.{3:000}", ts.Hours, ts.Minutes, ts.Seconds, ts.Milliseconds);
        }
    }
}

尽量使用类似于 BenchmarkDotNet 的工具,而不是自己编写代码,因为你很可能会被预热效应等问题所困扰。 - Zastai
使用.NET 6.0.1,我发现is XGetType() == typeof(X)慢得多。参考代码请查看https://gist.github.com/Zastai/1fbaa1e5f290ee46999361adbca6424d - Zastai

18

安德鲁说得对。实际上,通过代码分析,Visual Studio会将此转换视为不必要的。

一个想法(在不知道你在做什么的情况下有点靠感觉),但我一直被建议避免像这样进行检查,而是拥有另一个类。所以,不要进行某些检查,并根据类型执行不同的操作,而是使类知道如何处理自己...

例如,Obj可以是ISpecialType或IType;

它们都定义了DoStuff()方法。对于IType,它可以只返回或执行自定义操作,而对于ISpecialType,则可以执行其他操作。

这样就完全消除了任何转换,使代码更加清晰易于维护,且该类知道如何执行其自身任务。


是的,因为如果类型测试为真,我要做的就是调用它的某个接口方法,所以我可以将该接口方法移动到基类中,并默认情况下什么也不做。这可能比创建虚函数来测试类型更优雅。 - JubJub
在abatishchev的评论后,我进行了与Binary Worrier类似的测试,并发现在1000万次迭代中只有60毫秒的差异。 - Ian
1
哇,感谢您的帮助。我想我现在会继续使用类型检查运算符,除非重新组织类结构似乎更合适。我将使用 Andrew 建议的 'as' 运算符,因为我不想进行冗余转换。 - JubJub

5

文章链接已失效。 - James Wilkins
@James的链接已恢复。 - Gru
很好的内容 - 但我没有给你点踩(实际上我还给你点了赞);万一你想知道的话。 :) - James Wilkins

-3

我一直被建议避免像这样进行检查,而是使用另一个类。因此,不要进行某些检查并根据类型执行不同的操作,而是使类知道如何处理自己...

例如,Obj可以是ISpecialType或IType;

它们都定义了DoStuff()方法。对于IType,它可以只返回或执行自定义操作,而对于ISpecialType,则可以执行其他操作。

这样完全消除了任何强制转换,使代码更清晰、更易于维护,并且类知道如何执行自己的任务。


1
这不是答案。无论如何,由于缺乏上下文,类可能并不总是知道如何处理自己。当我们允许异常沿调用链向上抛出直到某个方法/函数具有足够的上下文来处理错误时,我们会应用类似的逻辑。 - Vakhtang

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