2

I want to minimize copying of structs in a maths library and read about the C# 7.2 in modifier, especially the warnings when using it with mutable structs.

It so happens that I have this mutable struct:

public struct Quaternion
{
    public float W;
    public float X;
    public float Y;
    public float Z;
}

So far, the library has methods like this, where parameters are passed by ref:

public static void Dot(ref Quaternion left, ref Quaternion right, out float result)
    => result = left.W * right.W + left.X * right.X + left.Y * right.Y + left.Z * right.Z;

From the MSDN documentation, I learned that if I change these to in parameters, as long as I only access fields of a mutable struct, no defensive copy will occur since the compiler sees I am not modifying the mutable struct:

public static void Dot(in Quaternion left, in Quaternion right, out float result)
    => result = left.W * right.W + left.X * right.X + left.Y * right.Y + left.Z * right.Z;

First question: Is my understanding of that behavior correct?

Second, silly question: If in one of such methods which accept the struct as an in parameter, will the compiler copy it if I call another method accepting them as in parameters? An example:

public static void Lerp(in Quaternion start, in Quaternion end, float amount,
    out Quaternion result)
{
    float inv = 1.0f - amount;
    if (Dot(start, end) >= 0.0f) // will 2 copies be created here?
    {
        result.W = inv * start.W + amount * end.W;
        result.X = inv * start.X + amount * end.X;
        result.Y = inv * start.Y + amount * end.Y;
        result.Z = inv * start.Z + amount * end.Z;
    }
    else
    {
        result.W = inv * start.W - amount * end.W;
        result.X = inv * start.X - amount * end.X;
        result.Y = inv * start.Y - amount * end.Y;
        result.Z = inv * start.Z - amount * end.Z;
    }
    result.Normalize();
}

I am pretty sure it should not create copies - how else would I prevent copies from the call side then? But as I am not sure, I better ask first before creating a mess.


Addendum

Reasons I want to change ref to in:

  • (static) readonly fields (e.g. specific constant quaternions) cannot be passed as ref arguments.
  • I cannot specify ref on operator parameters, but I can use in.
  • Continually specifying ref on the call site is ugly.
  • I'm aware I have to change the call site everywhere, but that is okay since this library will only be used internally.
Ray
  • 7,940
  • 7
  • 58
  • 90
  • In the general case, `in` *can* cause multiple copies, because the runtime needs to enforce that there are no side-effects from things like calling methods. If the type is marked `readonly struct`, then it trusts methods not to change the type - but: your scenario is a non-`readonly` value-type. In your case, you're *not calling methods* - only accessing fields; so... it gets less clear – Marc Gravell Nov 04 '19 at 14:24
  • Why are you using mutable structs to begin with? All best practice recommendations I've ever seen regarding structs talk about making them immutable... – Zohar Peled Nov 04 '19 at 14:36
  • Is your concern one of performance or semantics? – 500 - Internal Server Error Nov 04 '19 at 14:38
  • 1
    @ZoharPeled The C# 3D math libraries I used in the past typically do not use immutable structs - which may also be because they didn't exist back then in C#. I actually tried this on a Matrix type. There was still too much code just wanting to change one field of a matrix, so I kept the mutable ones. It may just work for quaternions though, but for the sake of the question, let's say I have to stick to mutable ones from user expectations. – Ray Nov 04 '19 at 17:15
  • @500-InternalServerError Performance is more important here. If not copying them cannot be guaranteed, I may just have to live with `ref`. I saw an opportunity to use a new feature with less lengthy semantics here and apparently no performance cost as I only access fields. – Ray Nov 04 '19 at 17:15
  • FYI DateTime has always been an immutable struct – Hans Kesting Nov 04 '19 at 18:23
  • @HansKesting Oops, ignore that small note then. – Ray Nov 04 '19 at 22:02

1 Answers1

0

As mentioned in the comments, using in for parameters of mutable structs can create defensive copies if the runtime cannot guarantee that the passed instance is not modified. It may be hard to guarantee this if you call properties, indexers, or methods on that instance.

Ergo, whenever you do not intend to modify the instance in such, you should state this explicitly by making them readonly. This has the benefit of also causing compilation to fail if you attempt to modify the instance in them.

Note the placement of the readonly keyword in the following examples especially:

public struct Vec2
{
    public float X, Y;

    // Properties
    public readonly float Length
    {
        get { return MathF.Sqrt(LengthSq); }
    }
    public readonly float LengthSq => X * X + Y * Y;

    // Indexers (syntax the same for properties if they also have setter)
    public float this[int index]
    {
        readonly get => index switch
        {
            0 => X,
            1 => Y,
            _ => throw ...
        };
        set
        {
            switch (index)
            {
                case 0: X = value; break;
                case 1: Y = value; break;
                default: throw ...
            }
        }
    }

    // Methods
    public readonly override int GetHashCode() => HashCode.Combine(X, Y);
}

Now, whenever you have a method using Vec2 with the in modifier, you can safely call the above without a copy being made.

(This feature was introduced in C# 8.0 and not available when I asked the question.)

Ray
  • 7,940
  • 7
  • 58
  • 90