F# 制作不必要的 DateTimeOffset 副本
F# making unnecessary copies of DateTimeOffset
这里肯定有一个初学者问题,为什么 F# 编译器会制作不必要的 DateTimeOffset 副本,我该如何阻止它?我不记得这是个问题,但也许自从我在 F# 中使用 DateTimeOffset 以来已经太久了:
let now = DateTimeOffset.Now
now.AddDays(30.0).ToString("yyyy-MM-dd")
在第 2 行,编译器抛出一条错误消息,“已复制该值以确保原始值不会被此操作改变,或者因为从成员返回结构时复制是隐式的,并且另一个成员被寻址”我如何才能抓住 Now
并添加几天?
您已经发现显示警告是因为它是 5 级警告集的一部分。但您可能仍然想知道此警告的实际含义。
警告本身已经暗示了这一点。当您在 struct
类型上调用实例方法时,其中包括像 ToString()
这样的虚拟方法,编译器无法确定底层 struct
是否保持不变。这是 F# 的关键点,它非常努力地确保您的原始 let
绑定保持不变。
F# 编译器中有多项优化,可尽量减少所进行的防御性复制的数量。但仍有许多情况无法确定某个值不会改变。对于任何虚拟调用都是如此(您可能会争辩说虚拟调用不能从结构中覆盖,但是当前结构的覆盖 可以被覆盖 ,并且可以访问字段,因此,它可以改变它的数据),更一般地说,对于任何实例成员。
如果我获取你的代码,并将其传递给 FSI(在设置 warn:5
之后),它会正确报告两个警告:
> 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"
通常,JIT 可以优化这些,但就像 F# 一样,JIT 也不能总是确定防御副本是否必要。在这种情况下,复制仍然会发生。我已经看到这种行为对于不同的 JIT 是不同的(甚至可以在 x86 和 x64 之间改变相同的 JIT)。
那么您将如何防止这种复制的发生?这并不总是那么容易,如果您不能更改类型的实现,那当然更不容易。有点违反直觉,如果你告诉 F# 你不关心它是否发生变异,它会停止为你复制 struct
:
let mutable now = DateTimeOffset.Now
now <- now.AddDays(30.)
now.ToString("yyyy-MM-dd");;
请注意,对于 float
或 int
等某些内置类型,此警告 不会引发 ,因为编译器知道这些类型及其实现并且知道它们不会变异(所有 BCL 方法都是安全的)。通常,不会为这些制作防御副本。
另请注意,它并不特定于 DateTimeOffset
,例如,DateTime
和 Guid
的行为完全相同,几乎所有其他 struct
不是基本类型的一部分。
编辑:Tomas 的回答也很有价值,他解释了为什么 AddDays
的副本在这种情况下实际上是必要的。但这是结果的中间副本而不是防御副本,在这种情况下最终是同一件事(令人困惑,我知道)。即使结果不需要中间副本也会引发警告,例如 ToString
.
您收到的警告的完整措辞是:
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
在这种情况下,我认为在消息的后半部分解释了警告的原因,即“因为从成员返回结构时复制是隐式的,然后访问另一个成员”。
如果你查看生成的 IL 代码,你会发现编译器确实生成了一个局部变量,将 AddDays
的结果赋值给这个局部变量,然后取这个变量的地址变量并使用此地址调用 ToString
(为了比较,C# 编译器为同一片段生成的代码完全相同):
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
我不是 IL 专家,但我认为编译器必须做它在这里做的事情——值类型可以是可变的,所以结果需要存储在局部变量中(这样它就可以使用它的地址调用它的操作)。如果不是通过地址,该方法将无法改变(可能可变的)值类型。
因此,编译器警告您它创建了一个您在代码中看不到的局部变量。如果您写道:
,这将很有用
someValueType.MutateOne().MutateTwo()
如果您认为这两个变异方法会变异 someValueType
变量,警告会告诉您这不是发生了什么! (因为第二种方法会改变隐藏的隐式变量。)在你的情况下,你可以安全地忽略警告。
这里肯定有一个初学者问题,为什么 F# 编译器会制作不必要的 DateTimeOffset 副本,我该如何阻止它?我不记得这是个问题,但也许自从我在 F# 中使用 DateTimeOffset 以来已经太久了:
let now = DateTimeOffset.Now
now.AddDays(30.0).ToString("yyyy-MM-dd")
在第 2 行,编译器抛出一条错误消息,“已复制该值以确保原始值不会被此操作改变,或者因为从成员返回结构时复制是隐式的,并且另一个成员被寻址”我如何才能抓住 Now
并添加几天?
您已经发现显示警告是因为它是 5 级警告集的一部分。但您可能仍然想知道此警告的实际含义。
警告本身已经暗示了这一点。当您在 struct
类型上调用实例方法时,其中包括像 ToString()
这样的虚拟方法,编译器无法确定底层 struct
是否保持不变。这是 F# 的关键点,它非常努力地确保您的原始 let
绑定保持不变。
F# 编译器中有多项优化,可尽量减少所进行的防御性复制的数量。但仍有许多情况无法确定某个值不会改变。对于任何虚拟调用都是如此(您可能会争辩说虚拟调用不能从结构中覆盖,但是当前结构的覆盖 可以被覆盖 ,并且可以访问字段,因此,它可以改变它的数据),更一般地说,对于任何实例成员。
如果我获取你的代码,并将其传递给 FSI(在设置 warn:5
之后),它会正确报告两个警告:
> 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"
通常,JIT 可以优化这些,但就像 F# 一样,JIT 也不能总是确定防御副本是否必要。在这种情况下,复制仍然会发生。我已经看到这种行为对于不同的 JIT 是不同的(甚至可以在 x86 和 x64 之间改变相同的 JIT)。
那么您将如何防止这种复制的发生?这并不总是那么容易,如果您不能更改类型的实现,那当然更不容易。有点违反直觉,如果你告诉 F# 你不关心它是否发生变异,它会停止为你复制 struct
:
let mutable now = DateTimeOffset.Now
now <- now.AddDays(30.)
now.ToString("yyyy-MM-dd");;
请注意,对于 float
或 int
等某些内置类型,此警告 不会引发 ,因为编译器知道这些类型及其实现并且知道它们不会变异(所有 BCL 方法都是安全的)。通常,不会为这些制作防御副本。
另请注意,它并不特定于 DateTimeOffset
,例如,DateTime
和 Guid
的行为完全相同,几乎所有其他 struct
不是基本类型的一部分。
编辑:Tomas 的回答也很有价值,他解释了为什么 AddDays
的副本在这种情况下实际上是必要的。但这是结果的中间副本而不是防御副本,在这种情况下最终是同一件事(令人困惑,我知道)。即使结果不需要中间副本也会引发警告,例如 ToString
.
您收到的警告的完整措辞是:
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
在这种情况下,我认为在消息的后半部分解释了警告的原因,即“因为从成员返回结构时复制是隐式的,然后访问另一个成员”。
如果你查看生成的 IL 代码,你会发现编译器确实生成了一个局部变量,将 AddDays
的结果赋值给这个局部变量,然后取这个变量的地址变量并使用此地址调用 ToString
(为了比较,C# 编译器为同一片段生成的代码完全相同):
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
我不是 IL 专家,但我认为编译器必须做它在这里做的事情——值类型可以是可变的,所以结果需要存储在局部变量中(这样它就可以使用它的地址调用它的操作)。如果不是通过地址,该方法将无法改变(可能可变的)值类型。
因此,编译器警告您它创建了一个您在代码中看不到的局部变量。如果您写道:
,这将很有用someValueType.MutateOne().MutateTwo()
如果您认为这两个变异方法会变异 someValueType
变量,警告会告诉您这不是发生了什么! (因为第二种方法会改变隐藏的隐式变量。)在你的情况下,你可以安全地忽略警告。