Understanding Class Performance in C#




Date Added (UTC):

20 May 2024 @ 13:08

Date Updated (UTC):

20 May 2024 @ 13:08


.NET Version(s):

.NET 8

Tag(s):


Added By:
Profile Image

Blog   
Wilmington, DE 19808, USA    
A dedicated executive technical architect who is focused on expanding organizations technology capabilities.

Benchmark Results:





Benchmark Code:



using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

namespace ClassBenchmark
{
    public class StandardClass
    {
        public int StandardProperty { get; set; }

        public int GetStandardProperty() => StandardProperty;
    }

    public sealed class SealedClass
    {
        public int SealedProperty { get; set; }

        public int GetSealedProperty() => SealedProperty;
    }

    public abstract class AbstractBaseClass
    {
        public virtual int GetBaseProperty() => 0;
    }

    public class DerivedClass : AbstractBaseClass
    {
        public int DerivedProperty { get; set; }

        public override int GetBaseProperty() => DerivedProperty;
    }

    public class ExtendedStandardClass : StandardClass
    {
        public int AdditionalProperty { get; set; }

        public int GetAdditionalProperty() => AdditionalProperty;
    }

    public class ClassBenchmark
    {
        private readonly StandardClass standardClassInstance = new StandardClass { StandardProperty = 42 };
        private readonly SealedClass sealedClassInstance = new SealedClass { SealedProperty = 42 };
        private readonly DerivedClass derivedClassInstance = new DerivedClass { DerivedProperty = 42 };
        private readonly ExtendedStandardClass extendedClassInstance = new ExtendedStandardClass { StandardProperty = 42, AdditionalProperty = 84 };

        [Benchmark(Baseline = true)]
        public int BenchmarkStandardClassProperty() => standardClassInstance.GetStandardProperty();

        [Benchmark]
        public int BenchmarkSealedClassProperty() => sealedClassInstance.GetSealedProperty();

        [Benchmark]
        public int BenchmarkDerivedClassProperty() => derivedClassInstance.GetBaseProperty();

        [Benchmark]
        public int BenchmarkExtendedClassStandardProperty() => extendedClassInstance.GetStandardProperty();

        [Benchmark]
        public int BenchmarkExtendedClassAdditionalProperty() => extendedClassInstance.GetAdditionalProperty();
    }

    class Program
    {
        static void Main(string[] args)
        {
            var benchmarkSummary = BenchmarkRunner.Run<ClassBenchmark>();
        }
    }
}

// .NET 8
public int BenchmarkStandardClassProperty()
{
    return standardClassInstance.GetStandardProperty();
}
// .NET 8
public int BenchmarkSealedClassProperty()
{
    return sealedClassInstance.GetSealedProperty();
}
// .NET 8
public int BenchmarkDerivedClassProperty()
{
    return derivedClassInstance.GetBaseProperty();
}
// .NET 8
public int BenchmarkExtendedClassStandardProperty()
{
    return extendedClassInstance.GetStandardProperty();
}
// .NET 8
public int BenchmarkExtendedClassAdditionalProperty()
{
    return extendedClassInstance.GetAdditionalProperty();
}

// .NET 8
.method public hidebysig 
    instance int32 BenchmarkStandardClassProperty () cil managed 
{
    .custom instance void [BenchmarkDotNet.Annotations]BenchmarkDotNet.Attributes.BenchmarkAttribute::.ctor(int32, string) = (
        01 00 39 00 00 00 01 5f 01 00 54 02 08 42 61 73
        65 6c 69 6e 65 01
    )
    // Method begins at RVA 0x20cf
    // Code size 12 (0xc)
    .maxstack 8

    // sequence point: (line 58, col 56) to (line 58, col 99) in _
    IL_0000: ldarg.0
    IL_0001: ldfld class ClassBenchmark.StandardClass ClassBenchmark.ClassBenchmark::standardClassInstance
    IL_0006: callvirt instance int32 ClassBenchmark.StandardClass::GetStandardProperty()
    IL_000b: ret
}
// .NET 8
.method public hidebysig 
    instance int32 BenchmarkSealedClassProperty () cil managed 
{
    .custom instance void [BenchmarkDotNet.Annotations]BenchmarkDotNet.Attributes.BenchmarkAttribute::.ctor(int32, string) = (
        01 00 3c 00 00 00 01 5f 00 00
    )
    // Method begins at RVA 0x20dc
    // Code size 12 (0xc)
    .maxstack 8

    // sequence point: (line 61, col 54) to (line 61, col 93) in _
    IL_0000: ldarg.0
    IL_0001: ldfld class ClassBenchmark.SealedClass ClassBenchmark.ClassBenchmark::sealedClassInstance
    IL_0006: callvirt instance int32 ClassBenchmark.SealedClass::GetSealedProperty()
    IL_000b: ret
}
// .NET 8
.method public hidebysig 
    instance int32 BenchmarkDerivedClassProperty () cil managed 
{
    .custom instance void [BenchmarkDotNet.Annotations]BenchmarkDotNet.Attributes.BenchmarkAttribute::.ctor(int32, string) = (
        01 00 3f 00 00 00 01 5f 00 00
    )
    // Method begins at RVA 0x20e9
    // Code size 12 (0xc)
    .maxstack 8

    // sequence point: (line 64, col 55) to (line 64, col 93) in _
    IL_0000: ldarg.0
    IL_0001: ldfld class ClassBenchmark.DerivedClass ClassBenchmark.ClassBenchmark::derivedClassInstance
    IL_0006: callvirt instance int32 ClassBenchmark.AbstractBaseClass::GetBaseProperty()
    IL_000b: ret
}
// .NET 8
.method public hidebysig 
    instance int32 BenchmarkExtendedClassStandardProperty () cil managed 
{
    .custom instance void [BenchmarkDotNet.Annotations]BenchmarkDotNet.Attributes.BenchmarkAttribute::.ctor(int32, string) = (
        01 00 42 00 00 00 01 5f 00 00
    )
    // Method begins at RVA 0x20f6
    // Code size 12 (0xc)
    .maxstack 8

    // sequence point: (line 67, col 64) to (line 67, col 107) in _
    IL_0000: ldarg.0
    IL_0001: ldfld class ClassBenchmark.ExtendedStandardClass ClassBenchmark.ClassBenchmark::extendedClassInstance
    IL_0006: callvirt instance int32 ClassBenchmark.StandardClass::GetStandardProperty()
    IL_000b: ret
}
// .NET 8
.method public hidebysig 
    instance int32 BenchmarkExtendedClassAdditionalProperty () cil managed 
{
    .custom instance void [BenchmarkDotNet.Annotations]BenchmarkDotNet.Attributes.BenchmarkAttribute::.ctor(int32, string) = (
        01 00 45 00 00 00 01 5f 00 00
    )
    // Method begins at RVA 0x2103
    // Code size 12 (0xc)
    .maxstack 8

    // sequence point: (line 70, col 66) to (line 70, col 111) in _
    IL_0000: ldarg.0
    IL_0001: ldfld class ClassBenchmark.ExtendedStandardClass ClassBenchmark.ClassBenchmark::extendedClassInstance
    IL_0006: callvirt instance int32 ClassBenchmark.ExtendedStandardClass::GetAdditionalProperty()
    IL_000b: ret
}

// .NET 8 (X64)
BenchmarkStandardClassProperty()
    L0000: mov rax, [rcx+8]
    L0004: mov eax, [rax+8]
    L0007: ret
// .NET 8 (X64)
BenchmarkSealedClassProperty()
    L0000: mov rax, [rcx+0x10]
    L0004: mov eax, [rax+8]
    L0007: ret
// .NET 8 (X64)
BenchmarkDerivedClassProperty()
    L0000: mov rcx, [rcx+0x18]
    L0004: mov rax, [rcx]
    L0007: mov rax, [rax+0x40]
    L000b: jmp qword ptr [rax+0x20]
// .NET 8 (X64)
BenchmarkExtendedClassStandardProperty()
    L0000: mov rax, [rcx+0x20]
    L0004: mov eax, [rax+8]
    L0007: ret
// .NET 8 (X64)
BenchmarkExtendedClassAdditionalProperty()
    L0000: mov rax, [rcx+0x20]
    L0004: mov eax, [rax+0xc]
    L0007: ret


Benchmark Description:


#### Standard Class (StandardClass) - **Average Time:** 0.0035 ns - This is a basic class without any additional modifiers or inheritance. It is fast because it lacks additional complexity. #### Sealed Class (SealedClass) - **Average Time:** 0.0294 ns - Sealed classes are slightly slower than standard classes. Although they cannot be inherited, compiler optimizations can impact performance. #### Derived Class (DerivedClass) - **Average Time:** 0.2313 ns - This class inherits from an abstract base class. Due to virtual methods and polymorphism, derived classes are slower due to additional checks and indirection. #### Extended Standard Class (ExtendedStandardClass) - **Standard Properties:** 0.0048 ns - **Additional Properties:** 0.0104 ns - This class inherits from StandardClass and adds new properties. Access to standard properties is slightly slower than the base class, and access to additional properties is slower due to extra complexity. --- #### Usage Guidelines #### StandardClass - **Use for:** Simple objects without the need for inheritance. - **Performance:** Best performance. #### SealedClass - **Use for:** Preventing class inheritance. - **Advantages:** Good for security and design clarity but might be slightly slower. #### DerivedClass - **Use for:** When abstraction and polymorphism are needed. - **Performance:** Always slower due to added complexity. #### ExtendedStandardClass - **Use for:** Extending functionality of the standard class. - **Consideration:** Be aware of potential performance costs. --- ### Factors Affecting Performance 1. **Virtual Methods:** Slowdown due to additional checks. 2. **Inheritance:** Introduces additional complexity and overhead. 3. **Compiler Optimizations:** Can either improve or degrade performance, depending on class structure.

The provided benchmark code is designed to measure and compare the performance of property access and method invocation across different types of classes in C#. These include a standard class, a sealed class, a derived class (from an abstract base class), and an extended class (inheriting from a standard class). The benchmarks are implemented using BenchmarkDotNet, a powerful .NET library for benchmarking, which provides detailed insights into the performance characteristics of the tested code. The .NET version isn't explicitly mentioned, but BenchmarkDotNet supports a wide range of .NET versions, including .NET Core and .NET Framework. ### Benchmark Setup - **StandardClass**: A basic class with a property and a method to get the property's value. - **SealedClass**: Similar to `StandardClass` but sealed, preventing other classes from inheriting from it. - **AbstractBaseClass and DerivedClass**: An abstract class with a virtual method, and a derived class that overrides this method. - **ExtendedStandardClass**: Inherits from `StandardClass` and adds an additional property and method. Each class instance is initialized with specific values for their properties in the `ClassBenchmark` constructor. The benchmarks compare how accessing properties and calling methods differ across these class types under identical conditions. ### Benchmark Methods Rationale 1. **BenchmarkStandardClassProperty**: Serves as the baseline for comparison. It measures the time it takes to retrieve a property value through a method in a standard class. This benchmark is crucial for understanding the overhead of method calls and property access in the simplest scenario. 2. **BenchmarkSealedClassProperty**: Measures the performance of accessing a property in a sealed class. Sealing a class can sometimes lead to performance benefits due to optimizations like devirtualization, as the JIT compiler knows that the method cannot be overridden. 3. **BenchmarkDerivedClassProperty**: Tests the performance impact of method overriding in inheritance hierarchies. Accessing a property through an overridden method in a derived class can have different performance characteristics due to polymorphism and the potential for virtual method calls. 4. **BenchmarkExtendedClassStandardProperty**: Measures the performance of accessing a property inherited from a base class. This benchmark helps understand if there's any performance difference when properties are accessed from a derived class rather than directly from an instance of the base class. 5. **BenchmarkExtendedClassAdditionalProperty**: Focuses on the performance of accessing a property that is declared in the derived class. This scenario tests whether adding new properties and methods to a derived class impacts performance differently compared to accessing inherited members. ### Expected Insights - **Inheritance and Sealing Impact**: One key insight would be understanding how inheritance (especially deep inheritance) and sealing affect method call and property access performance. Sealed classes might show slightly better performance due to JIT optimizations. - **Overhead of Virtual Calls**: Comparing the performance of `BenchmarkStandardClassProperty` with `BenchmarkDerivedClassProperty` can highlight the overhead associated with virtual calls in the .NET runtime. - **Extended vs. Standard Class Performance**: By comparing `BenchmarkExtendedClassStandardProperty` and `BenchmarkExtendedClassAdditionalProperty` with `BenchmarkStandardClassProperty`, it's possible to gauge any performance penalty introduced by class extension and inheritance. Running these benchmarks provides concrete data on how different class designs and inheritance patterns can impact performance in C#. This information is valuable for making informed decisions when designing systems that require both high performance and maintainability.


Benchmark Comments: