C#是操作员的绩效


102

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

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

if (obj is ISpecialType)

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

我听说有一个专门针对'is'关键字的IL指令,但这并不意味着它在转换为本地程序集时执行速度很快。谁能对“是”与其他方法的性能分享一些见解?

更新: 感谢您提供所有明智的答案!答案中似乎有几个有用的观点:安德鲁(Andrew)关于“是否”自动执行演员表的观点是必不可少的,但Binary Worrier和Ian收集的性能数据也非常有用。如果对其中一个答案进行编辑以包括所有这些信息,那就太好了。


2
顺便说一句,CLR将使您无法创建自己的Type GetType()函数,因为它违反了CLR的主要规则之一-真正的类型
abatishchev 2009年

1
嗯,我不确定“真正类型”规则的含义,但是我知道CLR具有内置的Type GetType()函数。如果我要使用该方法,它将使用具有不同名称的函数返回一些枚举,因此不会出现任何名称/符号冲突。
JubJub

3
我认为abatishchev的意思是“类型安全”。GetType()是非虚拟的,可以防止类型位于自身周围,因此可以保持类型的安全性。
Andrew Hare

2
您是否考虑过预取和缓存类型符合性,这样就不必在循环中执行它了?似乎每个性能问题总是被大量+1了,但这对我来说似乎对C#的理解很差。真的太慢了​​吗?怎么样?你尝试了什么?显然,您对答案的评论不多...
Gusdor

Answers:


114

is如果一旦检查类型将其转换为该类型,使用会损害性能。 is实际上将对象转换为您要检查的类型,因此任何后续的转换都是多余的。

如果仍然要进行投射,这是一种更好的方法:

ISpecialType t = obj as ISpecialType;

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

1
谢谢。但是,如果在条件失败的情况下我不打算强制转换对象,那么使用虚拟函数来测试类型会更好吗?
2009年

4
@JubJub:不。失败as基本上执行与is(即类型检查)相同的操作。唯一的区别是它然后返回null而不是false
康拉德·鲁道夫

74

我和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支票,两者之间的差别太小了


35
+1用于实际测试性能,而不是假设。
乔恩·塔卡伯里

3
用Stopwatch类进行测试要好得多,而不是使用DateTime.Now进行测试,这是非常昂贵的
abatishchev 2009年

2
我会考虑这一点,但是在这种情况下,我认为这不会影响结果。谢谢:)
Binary Worrier

11
@Binary Worrier-您对类的运算符分配将完全掩盖'is'操作中的任何性能差异。您为什么不通过重用两个不同的预分配实例来删除这些操作,然后重新运行代码并发布结果。

1
@mcmillab:我将保证,无论您在做什么,都会遇到比is操作员造成的任何性能下降都大几个数量级的瓶颈,而且围绕is操作员进行设计和编码的过分重视将使您付出巨大的代价。代码质量,并且最终也会在性能上击败自我。在这种情况下,我坚持我的发言。“ is”运算符永远不会成为运行时性能问题。
Binary Worrier

23

好的,所以我正在和某人聊天,并决定对其进行更多测试。据我所知,与测试您自己的成员或函数来存储类型信息相比,as和的性能is都非常好。

我曾经使用过Stopwatch,但这并不是最可靠的方法,所以我也尝试过UtcNow。后来,我还尝试了“处理器时间”方法,该方法似乎类似于UtcNow包含不可预测的创建时间。我还尝试过使基类没有虚拟对象而成为非抽象类,但是它似乎并没有起到很大的作用。

我在具有16GB RAM的Quad Q6600上运行。即使进行了5000万次迭代,这些数字仍会在+/- 50左右的毫秒数左右跳动,因此我不会对细微的差异了解太多。

有趣的是,x64的创建速度比x86快,但执行速度却慢于x86

x64发布模式:
秒表:形式
:561ms
是:597ms
基本属性:539ms
基本字段:555ms
基本RO字段:552ms
虚拟GetEnumType()测试:556ms
虚拟IsB()测试:588ms
创建时间:10416ms

UtcNow:值
:499ms
是:532ms
基本属性:479ms
基本字段:502ms
基本RO字段:491ms
虚拟GetEnumType():502ms
虚拟bool IsB():522ms
创建时间:285ms(此数字对于UtcNow似乎不可靠。我也得到109ms和806毫秒。)

x86发布模式:
秒表:
As:391ms
是:423ms
基本属性:369ms
基本字段:321ms
基本RO字段:339ms
虚拟GetEnumType()测试:361ms
虚拟IsB()测试:365ms
创建时间:14106ms

UtcNow::
348ms
是:375ms
基本属性:329ms
基本字段:286ms
基本RO字段:309ms
虚拟GetEnumType():321ms
虚拟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;
    }
}

45
(一些受上午5点启发的莎士比亚戏剧奖...)成为或不成为:这是一个问题:代码中的nobler是否要遭受抽象基的枚举和属性,还是要接受中介的报价?语言学家并通过援引其指示,相信他们吗?猜测:怀疑 不再; 并通过时机辨别,我们消除了有时限的编码人员所继承的头痛和数千种潜意识的疑问。“这是一个关闭,非常希望得到”。死,不是,而是睡觉。是的,我会入睡,渴望梦想,就像从最基础的阶级中得到的一样。
贾里德·瑟斯克

我们可以由此得出结论,在x64上访问属性比访问字段更快!!!因为这对我来说是一个惊喜,怎么可能呢?
Didier A.

1
我不会得出这样的结论,因为:“即使进行了5千万次迭代,数字仍会在+/- 50左右的毫秒数左右跳动,因此我不会对细微的差别有太多了解。”
Jared Thirsk

16

安德鲁是正确的。实际上,通过代码分析,Visual Studio将其报告为不必要的强制转换。

一个主意(不知道自己在做什么)是在黑暗中打针,但始终建议我避免像这样检查,而要另外一个课。因此,让类知道如何处理自身,而不是进行检查并根据类型进行不同的操作...

例如,Obj可以是ISpecialType或IType;

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

然后,这完全消除了所有强制转换,使代码更简洁,更易于维护,并且该类知道如何完成自己的任务。


是的,因为如果类型测试为true,我要做的就是在其上调用某个接口方法,那么我可以将该接口方法移至基类中,并且默认情况下不执行任何操作。这可能比创建一个虚拟函数来测试类型更为优雅。
JubJub

在abatishchev的评论之后,我对Binary Worrier进行了类似的测试,发现在10,000,000次发送中仅相差60毫秒。
伊恩(Ian)

1
哇,谢谢你的帮助。我想那时候我将坚持使用类型检查运算符,除非看起来适当地重组了类结构。我将使用安德鲁建议的'as'运算符,因为我不想重复转换。
2009年

15

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

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

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

输出:

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

比较时间: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);
        }
    }
}

13

在执行is检查然后强制转换有效时,Andrew Hare指出了性能下降,但是在C#7.0中,我们可以做的是检查女巫模式匹配,以避免以后再进行附加强制转换:

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

2
SO需要过滤器按钮(上的问题),以了解适用于框架的较新版本的答案,平台等这个答案形成一个正确的C#7的基础
尼克梅龙镇

1
当您使用不受控制的类型/类/接口时,@ Dib OOP理想被抛弃了。此方法对于处理可返回完全不同类型的多个值的函数的结果也很有用(因为C#仍不支持联合类型-您可以使用像这样的库,OneOf<T...>但它们有主要缺点) 。

4

如果有人想知道,我已经在Unity引擎2017.1中进行了测试,并在具有i5-4200U CPU的笔记本电脑上编写了脚本运行时版本.NET4.6(Experimantal)。结果:

Average Relative To Local Call LocalCall 117.33 1.00 is 241.67 2.06 Enum 139.33 1.19 VCall 294.33 2.51 GetType 276.00 2.35

全文:http : //www.ennoble-studios.com/tuts/unity-c-performance-comparison-is-vs-enum-vs-virtual-call.html


文章链接已死。
詹姆斯·威尔金斯

@James链接恢复。
Gru

好东西-但是我并没有对你投反对票(实际上我还是对它投了赞成票);如果您想知道。:)
詹姆斯·威尔金斯

-3

一直建议我避免像这样检查,而要另外一个课。因此,让类知道如何处理自身,而不是进行检查并根据类型进行不同的操作...

例如,Obj可以是ISpecialType或IType;

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

然后,这完全消除了所有强制转换,使代码更简洁,更易于维护,并且该类知道如何完成自己的任务。


1
这不能回答问题。无论如何,由于缺少上下文,类可能并不总是知道如何处理自己。当我们允许异常沿调用链上升,直到某些方法/函数具有足够的上下文来处理错误时,我们将类似的逻辑应用于异常处理。
Vakhtang
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.