Skip to content

Struct Unions + Struct Records don't directly grab backing field for a custom defined property when using pattern matching, causes performance overhead. #2688

@TIHan

Description

@TIHan

Struct Unions and Struct Records don't directly grab backing field for a custom defined property when using pattern matching; this causes performance overhead. This only occurs if the Unions/Records are value types. Reference types are fine.

Repro steps

We will use these types to compare performance overhead when accessing the M property.

    [<Struct>]
    type TestStruct2 =
        {
            m: Matrix4x4
        }

        member this.M = this.m


    [<Struct>]
    type TestStruct3 = TestStruct3 of m : Matrix4x4 with

        member this.M =
            match this with
            | TestStruct3 m -> m

And here are the decompiled to C# versions (notice TestStruct3 doesn't directly grab the backing field):
TestStruct2

public TestStructs.Matrix4x4 M {
	get {
		return this.m@;
	}
}

TestStruct3

public TestStructs.Matrix4x4 M {
	get {
		TestStructs.TestStruct3 testStruct = this;
		return testStruct._m;
	}
}

Now here is the script to run and test (note, this was run in Release):

module TestStructs =

    open System

    [<Struct>]
    type Vector4 =

        val X : float32

        val Y : float32

        val Z : float32

        val W : float32

    [<Struct>]
    type Matrix4x4 =
        {
            v0: Vector4
            v1: Vector4
            v2: Vector4
            v3: Vector4
        }

        static member Identity = 
            { 
                v0 = Vector4 ()
                v1 = Vector4 ()
                v2 = Vector4 ()
                v3 = Vector4 ()
            }

    [<Struct>]
    type TestStruct2 =
        {
            m: Matrix4x4
        }

        member this.M = this.m


    [<Struct>]
    type TestStruct3 = TestStruct3 of m : Matrix4x4 with

        member this.M =
            match this with
            | TestStruct3 m -> m

    let run () =
        let mutable result = Matrix4x4.Identity
        let arr1 = Array.zeroCreate<TestStruct2> 10000000

        let test1 () =
            let stopwatch = System.Diagnostics.Stopwatch.StartNew ()

            for i = 0 to arr1.Length - 1 do
                result <- arr1.[i].M

            stopwatch.Stop ()

            printfn "Test1: %A" stopwatch.Elapsed.TotalMilliseconds
            GC.Collect ()

        let arr2 = Array.zeroCreate<TestStruct3> 10000000

        let test2 () =
            let stopwatch = System.Diagnostics.Stopwatch.StartNew ()

            for i = 0 to arr2.Length - 1 do
                result <- arr2.[i].M

            stopwatch.Stop ()

            printfn "Test2: %A" stopwatch.Elapsed.TotalMilliseconds
            GC.Collect ()

        test1 ()
        test1 ()
        test2 ()
        test2 ()
        System.Threading.Thread.Sleep (500)
        test1 ()
        test1 ()
        test2 ()
        test2 ()
        System.Threading.Thread.Sleep (500)
        test1 ()
        test1 ()
        test2 ()
        test2 ()

open TestStructs

run ()

The C# decompiled versions of the tests:

		internal static void test1@75 (TestStructs.TestStruct2[] arr1, FSharpRef<TestStructs.Matrix4x4> result, Unit unitVar0)
		{
			Stopwatch stopwatch = Stopwatch.StartNew ();
			for (int i = 0; i < arr1.Length; i++) {
				result.contents = arr1 [i].m@;
			}
			stopwatch.Stop ();
			PrintfFormat<FSharpFunc<double, Unit>, TextWriter, Unit, Unit> format = new PrintfFormat<FSharpFunc<double, Unit>, TextWriter, Unit, Unit, double> ("Test1: %A");
			PrintfModule.PrintFormatLineToTextWriter<FSharpFunc<double, Unit>> (Console.Out, format).Invoke (stopwatch.Elapsed.TotalMilliseconds);
			GC.Collect ();
		}

		internal static void test2@88 (TestStructs.TestStruct3[] arr2, FSharpRef<TestStructs.Matrix4x4> result, Unit unitVar0)
		{
			Stopwatch stopwatch = Stopwatch.StartNew ();
			for (int i = 0; i < arr2.Length; i++) {
				TestStructs.TestStruct3 testStruct = arr2 [i];
				result.contents = testStruct._m;
			}
			stopwatch.Stop ();
			PrintfFormat<FSharpFunc<double, Unit>, TextWriter, Unit, Unit> format = new PrintfFormat<FSharpFunc<double, Unit>, TextWriter, Unit, Unit, double> ("Test2: %A");
			PrintfModule.PrintFormatLineToTextWriter<FSharpFunc<double, Unit>> (Console.Out, format).Invoke (stopwatch.Elapsed.TotalMilliseconds);
			GC.Collect ();
		}

It inlines it, which is normal, but you can see for TestStruct3 it's doing more work. I also tried by making the internals private and moving run out of the module to prevent inlining, I get the same exact performance as before, which is completely expected due to JIT optimizations on properties.

Also, this is modifying TestStruct2's M property to use pattern matching:

    [<Struct>]
    type TestStruct2 =
        {
            m: Matrix4x4
        }

        member this.M =
            match this with
            | { m = m } -> m
public TestStructs.Matrix4x4 M {
	get {
		TestStructs.TestStruct2 testStruct = this;
		return testStruct.m@;
	}
}

This has the exact same performance overhead.

Expected behavior

Performance on accessing the M property on either type should be the same with respect to TestStruct2.

Actual behavior

Here are some test results. Notice that Test2 is significantly slower.

Test1: 
663.8413



Test1: 
413.3082



Test2: 
866.1804



Test2: 
621.8993



Test1: 
383.6361



Test1: 
397.3988



Test2: 
628.037



Test2: 
620.8794



Test1: 
382.0673



Test1: 
430.2265



Test2: 
608.1133



Test2: 
595.7762

Known workarounds

Don't use Struct Unions with custom defined properties that grab its data.
Don't use Struct Records to grab its data using pattern matching.

Related information

  • F# 4.1
  • Only tested on OSX using Mono 4.8.0

There might be other performance issues for struct unions and struct records using pattern matching that's not just within the context of a member property.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions