-
Notifications
You must be signed in to change notification settings - Fork 832
Description
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 -> mAnd 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 } -> mpublic 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.