7

Is there realy significant performance advantage of using typeof(String) vs System.Type.GetType("System.String")?

If there is, I'd like to know why. Go as deep into the CLR as you have to to prove it.

My tests show yes, by a good margin.

Version 2

Results

Configuration=Release

baseline: 5572 ticks 2 ms
typeof(Test): 8757 ticks 3 ms
Type.GetType(String): 3899966 ticks 1482 ms

Code

[MethodImpl(MethodImplOptions.NoInlining)]
static int Call(Type t)
{
    return 1;
}
static void Main(string[] args)
{
    const int Iterations = 1000000;
    int count;

    Stopwatch sw = Stopwatch.StartNew(); count = 0;
    for (int i = 0; i < Iterations; i++)
    {
        count += Call(null);
    }
    sw.Stop();
    Console.WriteLine("baseline: {0} ticks {1} ms", sw.ElapsedTicks, sw.ElapsedMilliseconds);

    sw = Stopwatch.StartNew(); count = 0;
    for (int i = 0; i < Iterations; i++)
    {
        count += Call(typeof(String));
    }
    sw.Stop();
    Console.WriteLine("typeof(Test): {0} ticks {1} ms", sw.ElapsedTicks, sw.ElapsedMilliseconds);

    sw = Stopwatch.StartNew(); count = 0;
    for (int i = 0; i < Iterations; i++)
    {
        count += Call(Type.GetType("System.String"));
    }
    sw.Stop();
    Console.WriteLine("Type.GetType(String): {0} ticks {1} ms", sw.ElapsedTicks, sw.ElapsedMilliseconds);
}

Version 1

Results

Configuration=Debug

typeof(Test): 24782 ticks 9 ms
Type.GetType(String): 4783195 ticks 1818 ms

Code

static void Main() 
{
  const int Iterations = 1000000;
  Stopwatch sw = Stopwatch.StartNew();
  for (int i = 0; i < Iterations; i++)
  {
    Type t = typeof(String);
  }
  sw.Stop();
  Console.WriteLine("typeof(Test): {0} ticks {1} ms", sw.ElapsedTicks, sw.ElapsedMilliseconds);

  sw = Stopwatch.StartNew();
  for (int i = 0; i < Iterations; i++)
  {
    Type t = System.Type.GetType("System.String");
  }
  sw.Stop();
  Console.WriteLine("Type.GetType(String): {0} ticks {1} ms", sw.ElapsedTicks, sw.ElapsedMilliseconds);
}
George Chondrompilas
  • 3,167
  • 27
  • 35
JJS
  • 6,431
  • 1
  • 54
  • 70
  • 1
    Both benchmarks are invalid because the result is thrown away. The JIT might optimize that. – usr Feb 24 '16 at 19:32
  • 4
    `typeof(string)` would be handled at compile time. `Type.GetType("System.String")` requires that it be resolved at run time. Of course it will be faster. – willaien Feb 24 '16 at 19:32
  • wish @jonskeet would have included this in his answer years ago. http://stackoverflow.com/a/353435/26877 – JJS Feb 24 '16 at 19:36
  • 3
    This is a rather pointless question. You aren't going to need to call the function more than once because the answer isn't going to change. – David Heffernan Feb 24 '16 at 22:58
  • The first loop in version 1 is empty because the JIT throws away the line `Type t = typeof(String);`. – Enigmativity Feb 24 '16 at 23:09
  • @Enigmativity does it get optimized when Configuration=Debug? Can you cite your reference? – JJS Feb 24 '16 at 23:13
  • @DavidHeffernan pointless enough to get 65 views in a few hours. The example code is demonstrative of adding Columns to a DataTable, which is done a **lot** in our codebase. – JJS Feb 24 '16 at 23:14
  • @JJS - No, when in debug it doesn't get optimized out. I used LINQPad to via the IL. – Enigmativity Feb 24 '16 at 23:48
  • @Enigmativity I don't think many people compile about the C# compiler optimizing things away. I think most people have issues with the JIT process optimizing statements away. – JJS Feb 25 '16 at 16:10

2 Answers2

8

You kind of answered your own question. typeof(string) is faster. But it's interesting to see why.

typeof is compiled to ldtoken and GetTypeFromHandle (see Efficiency of C#'s typeof operator (or whatever its representation is in MSIL)). This is more efficient than GetType("System.String").

Also note that the benchmarks in Version 1 are invalid because the result variable Type t is not used. Not using the local variable will cause the JIT to optimize the statement away. The first loop body would effectively be a no-operation, but the second loop will execute. This is my guess based on the performance numbers you reported.

Here's a benchmark done right. The NoInline function serves as a sink to the value you want to benchmark. Disadvantage is that you're now benchmarking function call costs but they are small.

Community
  • 1
  • 1
usr
  • 168,620
  • 35
  • 240
  • 369
  • I explicitly did the test in Configuration=Debug to avoid the optimizer. I've made an edit to the post to include JonSkeet's technique, and a baseline for comparison. – JJS Feb 24 '16 at 22:51
  • That is even worse. Performance in Debug mode is very unrelated to performance under optimizations. The new benchmark looks valid to me. Run it under Release mode without the debugger attached. Since this is such a common mistake, please confirm explicitly that you ran it without debugger attached (Ctrl-F5). – usr Feb 24 '16 at 22:56
  • I do not know why Jons benchmark measures typeof to be so slow compared to yours. Maybe optimizations in the runtime that have been added in the last 7 years. – usr Feb 24 '16 at 22:57
  • I ran the release code from a console w/ msbuild in the %path%. no visual studio open to press ctrl+F5 in... – JJS Feb 24 '16 at 22:59
  • OK. 100 million typeof evaluations per second seem appropriate to me. That number is believable. That's 30-120 instructions per typeof. This is more than one loop iteration and one call which implies that neither of them were optimized out. Is your question resolved? I think the benchmark and conclusion are valid (9ms is too short to obtain an accurate number but it's clear that one version is better than the other). – usr Feb 24 '16 at 23:00
  • I don't feel like you really answered the `why` to my satisfaction. I've added a new answer that contains the level of detail I was after. Thanks for helping me understand the fault in my microperf testing. – JJS Feb 26 '16 at 16:18
3

Because it's doing more work @JJS. Remember your training and use the source, Luke.

The documentation gives us some clues. Type.GetType Method (String)

  • If a type is in an assembly known to your program at compile time, it is more efficient to use in C#, GetType in Visual Basic, or in C++.
  • If typeName includes the namespace but not the assembly name, this method searches only the calling object's assembly and Mscorlib.dll, in that order.

we know that typeof(T) is a call to is compiled to ldtoken and GetTypeFromHandle, but what is GetTypeFromHandle doing compared to GetTypeByName?

Lets solve the easy one first. GetTypeFromHandle is defined as

[Pure]
[System.Security.SecuritySafeCritical]  // auto-generated
[ResourceExposure(ResourceScope.None)]
[MethodImpl(MethodImplOptions.InternalCall)]
public static extern Type GetTypeFromHandle(RuntimeTypeHandle handle);

Lets get a version of the CLR we can reference.

Shared Source Common Language Infrastructure 2.0 Release

runtimehandles.cpp

FCIMPL1(Object*, RuntimeTypeHandle::GetRuntimeType, void* th) { 
    CONTRACTL {
        THROWS;
        DISABLED(GC_TRIGGERS);
        MODE_COOPERATIVE;
        SO_TOLERANT;
    }
    CONTRACTL_END;

    OBJECTREF refType = NULL;

    TypeHandle typeHandle = TypeHandle::FromPtr(th);
    TypeHandle* pTypeHandle = &typeHandle;

    _ASSERTE(CheckPointer(pTypeHandle));
    _ASSERTE(CheckPointer(pTypeHandle->AsPtr(), NULL_OK));

    if (pTypeHandle->AsPtr() == NULL)
        return NULL;

    refType = pTypeHandle->GetManagedClassObjectIfExists();

    if (refType != NULL)
        return OBJECTREFToObject(refType);

    HELPER_METHOD_FRAME_BEGIN_RET_ATTRIB_1(Frame::FRAME_ATTR_RETURNOBJ, refType);

    refType = pTypeHandle->GetManagedClassObject();
    HELPER_METHOD_FRAME_END();

    return OBJECTREFToObject(refType);
}
FCIMPLEND

Alright. This is legit. We are doing a wicked simple call here to get an OBJECTREFToObject.

No searching, just looking up a type effectively by it's method table. Need a refresher on .Net internals?

OK, what about the slow method? Type.GetType Method (String)

Chase the call stack and find out that it calls RuntimeTypeHandle.GetTypeByName

[System.Security.SecurityCritical]  // auto-generated
[ResourceExposure(ResourceScope.None)]
[DllImport(JitHelpers.QCall, CharSet = CharSet.Unicode)]
[SuppressUnmanagedCodeSecurity]
private extern static void GetTypeByName(string name, bool throwOnError, bool ignoreCase, bool reflectionOnly, StackCrawlMarkHandle stackMark, 
#if FEATURE_HOSTED_BINDER
IntPtr pPrivHostBinder,
#endif
bool loadTypeFromPartialName, ObjectHandleOnStack type);

runtimehandles.cpp

FCIMPL6(EnregisteredTypeHandle, RuntimeTypeHandle::GetTypeByName, 
    StringObject* classNameUNSAFE, CLR_BOOL bThrowOnError, CLR_BOOL bIgnoreCase, CLR_BOOL bReflectionOnly, StackCrawlMark* pStackMark, CLR_BOOL bLoadTypeFromPartialNameHack) 
{
    CONTRACTL 
    {
        THROWS;
        DISABLED(GC_TRIGGERS);
        MODE_COOPERATIVE;
        SO_TOLERANT;
    }
    CONTRACTL_END;

    STRINGREF sRef = (STRINGREF) classNameUNSAFE;
    TypeHandle typeHandle;

    HELPER_METHOD_FRAME_BEGIN_RET_1(sRef);
    {
        if (!sRef)
            COMPlusThrowArgumentNull(L"className",L"ArgumentNull_String");

        typeHandle = TypeName::GetTypeManaged(sRef->GetBuffer(), NULL, bThrowOnError, bIgnoreCase, bReflectionOnly, /*bProhibitAsmQualifiedName =*/ FALSE, pStackMark, bLoadTypeFromPartialNameHack);        
    }
    HELPER_METHOD_FRAME_END();

    return typeHandle.AsPtr();
}
FCIMPLEND

Fine, but what is TypeName::GetTypeManaged doing?!

typeparse.cpp

//--------------------------------------------------------------------------------------------------------------
// This everything-but-the-kitchen-sink version is what used to be called "GetType()". It exposes all the
// funky knobs needed for implementing the specific requirements of the managed Type.GetType() apis and friends.
//--------------------------------------------------------------------------------------------------------------
/*public static */ TypeHandle TypeName::GetTypeManaged

But it doesn't stop there

typeparse.cpp

// -------------------------------------------------------------------------------------------------------------
// This is the "uber" GetType() that all public GetType() funnels through. It's main job is to figure out which
// Assembly to load the type from and then invoke GetTypeHaveAssembly.
//
// It's got a highly baroque interface partly for historical reasons and partly because it's the uber-function
// for all of the possible GetTypes.
// -------------------------------------------------------------------------------------------------------------
/* private instance */ TypeHandle TypeName::GetTypeWorker

Doesn't stop here either.

typeparse.cpp

//----------------------------------------------------------------------------------------------------------------
// This is the one that actually loads the type once we've pinned down the Assembly it's in.
//----------------------------------------------------------------------------------------------------------------
/* private instance */ TypeHandle TypeName::GetTypeHaveAssembly(Assembly* pAssembly, BOOL bThrowIfNotFound, BOOL bIgnoreCase, BOOL bRecurse)

for (COUNT_T i = 0; i < names.GetCount(); i ++)
{
    LPCWSTR wname = names[i]->GetUnicode();
    MAKE_UTF8PTR_FROMWIDE(name, wname);
    typeName.SetName(name);       
    th = pAssembly->GetLoader()->LoadTypeHandleThrowing(&typeName);
}

clsload.cpp

TypeHandle ClassLoader::LoadTypeHandleThrowing(NameHandle* pName, ClassLoadLevel level, Module* pLookInThisModuleOnly/*=NULL*/)

    BOOL foundSomething = FindClassModuleThrowing(pName,

// FindClassModuleThrowing discovers which module the type you're looking for is in and loads the Module if necessary. // Basically, it iterates through all of the assembly's modules until a name match is found in a module's // AvailableClassHashTable.

    if (!typeHnd.IsNull()) {
        typeHnd = LoadTypeDefThrowing(typeHnd.GetModule(), typeHnd.GetCl(),

// Given a token specifying a typeDef, and a module in which to // interpret that token, find or load the corresponding type handle.

typeHnd = pModule->LookupTypeDef(typeDef, level);

ceeload.h

TypeHandle LookupTypeDef(mdTypeDef token, ClassLoadLevel level = CLASS_LOAD_UNRESTOREDTYPEKEY)

which is giving us the TypeHandle. The same thing we got in a single stack frame of GetTypeFromHandle

    PTR_MethodTable pMT = PTR_MethodTable(GetFromRidMap(&m_TypeDefToMethodTableMap, RidFromToken(token)));
    if (pMT == NULL || pMT->GetLoadLevel() < level)
        return TypeHandle();
    else
        return (TypeHandle)pMT;

So... What's slow? The iteration in FindClassModuleThrowing. It has to iterate through names to lookup the Method Table... Iterating over an array, is always slower than looking up something by a known key, which was available in GetTypeFromHandle

Case closed.

JJS
  • 6,431
  • 1
  • 54
  • 70