Skip to content

Overloaded Operator performance diff when using Fields or Auto Properties in Structs #12172

Description

@sonnemaf

After reading this great Is C# a low-level language blog from @mattwar I found an performance issue with the * operator.

There is a difference in performance when using the overloaded operators on a struct with Fields compared to Auto Properties.

Have a look at the following Benchmark.

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System;

namespace ConsoleApp1 {
    class Program {
        static void Main(string[] args) {
            BenchmarkRunner.Run<BM>();
        }
    }

    public class BM {

        [Benchmark(Baseline = true)]
        public VecWithFields TestVecWithFields() {
            var p = new VecWithFields(12f, 5f, 1.5f);
            var r = new VecWithFields();
            for (int i = 0; i < 100000; i++) {
                r = p + r;
                //r = p * r;
            }
            return r;
        }

        [Benchmark]
        public VecWithProperties TestVecWithProperties() {
            var p = new VecWithProperties(12f, 5f, 1.5f);
            var r = new VecWithProperties(); ;
            for (int i = 0; i < 100000; i++) {
                r = p + r;
                //r = p * r;
            }
            return r;
        }

        [Benchmark]
        public ReadOnlyVecWithProperties TestReadOnlyVecWithProperties() {
            var p = new ReadOnlyVecWithProperties(12f, 5f, 1.5f);
            var r = new ReadOnlyVecWithProperties(); ;
            for (int i = 0; i < 100000; i++) {
                r = p + r;
                //r = p * r;
            }
            return r;
        }

        [Benchmark]
        public VecWithProperties TestVecWithPropertiesNoOperator() {
            var p = new VecWithProperties(12f, 5f, 1.5f);
            var r = new VecWithProperties(); ;
            for (int i = 0; i < 100000; i++) {
                r = new VecWithProperties(p.X + r.X, p.Y + r.Y, p.Z + r.Z);

                //r = p * r;
                //r = new VecWithProperties(p.X * r.X, p.Y * r.Y, p.Z * r.Z);
            }
            return r;
        }
    }

    public struct VecWithFields {
        public float X;
        public float Y;
        public float Z;

        public VecWithFields(float x, float y, float z) {
            this.X = x;
            this.Y = y;
            this.Z = z;
        }

        public static VecWithFields operator *(VecWithFields q, VecWithFields r) {
            return new VecWithFields(q.X * r.X, q.Y * r.Y, q.Z * r.Z);
        }

        public static VecWithFields operator +(VecWithFields q, VecWithFields r) {
            return new VecWithFields(q.X + r.X, q.Y + r.Y, q.Z + r.Z);
        }
    }


    public struct VecWithProperties {
        public float X { get; set; }
        public float Y { get; set; }
        public float Z { get; set; }

        public VecWithProperties(float x, float y, float z) {
            this.X = x;
            this.Y = y;
            this.Z = z;
        }

        public static VecWithProperties operator *(VecWithProperties q, VecWithProperties r) {
            return new VecWithProperties(q.X * r.X, q.Y * r.Y, q.Z * r.Z);
        }

        public static VecWithProperties operator +(VecWithProperties q, VecWithProperties r) {
            return new VecWithProperties(q.X + r.X, q.Y + r.Y, q.Z + r.Z);
        }

    }

    public readonly struct ReadOnlyVecWithProperties {
        public float X { get; }
        public float Y { get; }
        public float Z { get; }

        public ReadOnlyVecWithProperties(float x, float y, float z) {
            this.X = x;
            this.Y = y;
            this.Z = z;
        }

        public static ReadOnlyVecWithProperties operator *(ReadOnlyVecWithProperties q, ReadOnlyVecWithProperties r) {
            return new ReadOnlyVecWithProperties(q.X * r.X, q.Y * r.Y, q.Z * r.Z);
        }

        public static ReadOnlyVecWithProperties operator +(ReadOnlyVecWithProperties q, ReadOnlyVecWithProperties r) {
            return new ReadOnlyVecWithProperties(q.X + r.X, q.Y + r.Y, q.Z + r.Z);
        }

    }
}

The TestVecWithProperties() method is almost 5 times slower when using the + opeator. I also tested the * operator and it is more than 3 times slower. The C# 7.2 readonly struct is also slower.

BenchmarkDotNet=v0.11.4, OS=Windows 10.0.18348
Intel Core i7-2640M CPU 2.80GHz (Sandy Bridge), 1 CPU, 4 logical and 2 physical cores
.NET Core SDK=2.2.200
  [Host]     : .NET Core 2.2.2 (CoreCLR 4.6.27317.07, CoreFX 4.6.27318.02), 64bit RyuJIT
  DefaultJob : .NET Core 2.2.2 (CoreCLR 4.6.27317.07, CoreFX 4.6.27318.02), 64bit RyuJIT


|                          Method |      Mean |     Error |    StdDev | Ratio | RatioSD |
|-------------------------------- |----------:|----------:|----------:|------:|--------:|
|               TestVecWithFields |  95.46 us | 1.0436 us | 0.9251 us |  1.00 |    0.00 |
|           TestVecWithProperties | 465.63 us | 7.7676 us | 6.8857 us |  4.88 |    0.09 |
|   TestReadOnlyVecWithProperties | 463.03 us | 4.7100 us | 3.9331 us |  4.85 |    0.07 |
| TestVecWithPropertiesNoOperator |  93.72 us | 0.8890 us | 0.8316 us |  0.98 |    0.02 |

Is this by design or a bug?

category:cq
theme:inlining
skill-level:expert
cost:large

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions