F# making unnecessary copies of DateTimeOffset

Viewed 147

Definitely have a beginners question here, why is the F# compiler making copies of DateTimeOffset which are unnecessary and how do I stop this? I don't remember this being a problem but maybe it has just been too long since I've worked with DateTimeOffset in F#:

let now = DateTimeOffset.Now
now.AddDays(30.0).ToString("yyyy-MM-dd")

On line 2 the compiler is throwing an error saying, "The value has been copied to ensure the original is not mutated by this operation or because the copy is implicit when returning a struct from a member and another member is addressed." How do I just grab Now and add a few days?

2 Answers

You've already found out that the warning was shown because it is part of the set of warnings for level 5. But you may still be wondering what this warning actually means.

A hint of that is already in the warning itself. When you call an instance method on a struct type, which includes virtual methods like ToString(), the compiler cannot ascertain that the underlying struct remains immutable. This is a key point for F# and it tries very hard to make sure your original let binding remains unchanged.

There are several optimizations in the F# compiler that try to minimize the amount of defensive copying being done. But there are still numerous cases where it cannot be certain a value doesn't change. This is true for any virtual call (you might argue that a virtual call cannot be overridden from a struct, but the override for the current struct can be overridden, and has access to the fields, hence, it can mutate its data), and more generally, for any instance member.

If I take your code, and pass it into FSI (after setting warn:5), it correctly reports two warnings:

> let now = DateTimeOffset.Now
now.AddDays(30.0).ToString("yyyy-MM-dd");;

  now.AddDays(30.0).ToString("yyyy-MM-dd");;
  ^^^^^^^^^^^^^^^^^

stdin(3,1): warning FS0052: The value has been copied to ensure the original is not mutated by this operation or because the copy is implicit when returning a struct from a member and another member is then accessed


  now.AddDays(30.0).ToString("yyyy-MM-dd");;
  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

stdin(3,1): warning FS0052: The value has been copied to ensure the original is not mutated by this operation or because the copy is implicit when returning a struct from a member and another member is then accessed

val now : DateTimeOffset = 16-7-2020 0:21:21 +02:00
val it : string = "2020-08-15"

Oftentimes, the JIT can optimize these away, but just like F#, the JIT also cannot always ascertain the defensive copy is necessary or not. In such cases, the copying will still take place. I've seen this behavior being different for different JITs (can even change for the same JIT between x86 and x64).

So how would you prevent this copying from happening? This isn't always easy, and certainly not if you cannot change the type's implementation. A little counter-intuitive, if you tell F# you don't care whether it is mutated or not, it will stop copying the struct for you:

let mutable now = DateTimeOffset.Now
now <- now.AddDays(30.)
now.ToString("yyyy-MM-dd");;

Note that this warning does not raise for certain build-in types like float or int, because the compiler knows these types and their implementation and knows they won't mutate (all BCL methods are safe). No defensive copies are made for those, generally.

Also note that it is not specific to DateTimeOffset, for instance, DateTime and Guid behave exactly the same, as will do almost any other struct that isn't part of the primitive types.

Edit: Tomas' answer is also valuable, he explains why the copy for AddDays is actually necessary in this case. But this is an intermediate copy for the result and is not the defensive copy, which in this case end up being the same thing (confusing, I know). The warning is raised even if the result doesn't require an intermediate copy, like for ToString.

The full wording of the warning you are getting is:

warning FS0052: The value has been copied to ensure the original is not mutated by this operation or because the copy is implicit when returning a struct from a member and another member is then accessed

In this case, I think the reason for the warning is explained in the second half of the message, i.e. "because the copy is implicit when returning a struct from a member and another member is then accessed".

If you look at the generated IL code, then you'll see that the compiler, indeed, generates a local variable, assigns the result of AddDays to this local variable, then takes the address of this variable and invokes ToString using this address (for comparison, the code generated by the C# compiler for the same snippet is exactly the same):

call valuetype [mscorlib]System.DateTimeOffset 
  [mscorlib]System.DateTimeOffset::get_Now()
stloc.0       // Store the result of 'Now' in local variable #0
ldloca.s 0    // Load the address of local variable #0 to call 'AddDays'
ldc.r8 30
call instance valuetype [mscorlib]System.DateTimeOffset 
  [mscorlib]System.DateTimeOffset::AddDays(float64)
stloc.2       // Store the result of 'AddDays' in local variable #2
ldloca.s 2    // Load the address of local variable #2 to call 'ToString'
ldstr "yyyy-MM-dd"
call instance string [mscorlib]System.DateTimeOffset::ToString(string)
stloc.1

I'm not an IL expert, but I think the compiler has to do what it is doing here - value types can be mutable, so the results need to be stored in a local variable (so that it can then invoke operation on it using its address). If it was not via an address, the method would not be able to mutate the (possibly mutable) value type.

So, the compiler is warning you about the fact that it creates a local variable you don't see in the code. This would be useful if you wrote:

someValueType.MutateOne().MutateTwo()

If you were thinking that the two mutating methods mutate the someValueType variable, the warning would tell you that this is not what's going on! (Because the second method would be mutating the hidden implicit variable.) In your case, you can safely ignore the warning.

Related