如果 T 是整数,拳击会在这里发生吗?
Is boxing going to happen here if T is int?
public static bool Equal<T>(T value, T match) {
return Equals(value, match);
}
所以问题是,如果 T 是 int32,这里会进行装箱还是编译器会选择没有装箱的 int32 等于?
对原始问题和 Rango(基本上正确)的回答的评论有些混乱,所以我想我应该把它们弄清楚。
首先,请注意泛型在 C# 中的工作原理。泛型不是模板!
在 C# 中,泛型被 C# 编译器编译成泛型 IL,然后通过抖动将 IL 重新编译成专用形式。例如,如果我们有一个方法 M<T>(T t)
,那么 C# 编译器会将该方法及其主体 一次 编译到 IL 中。
当出现抖动时,调用M<string>
、M<object>
或M<IEnumerable>
将触发exactly one编译;抖动非常聪明,它可以将主体编译成一种形式,无论类型参数是什么,它都可以工作,前提是类型参数是引用类型。但是M<int>
和M<double>
会各自编译成各自的汇编代码体。
注意抖动不知道C#的规则,C#做重载解析。当 C# 编译器生成 IL 时,每个方法调用的确切方法已经被选择。所以如果你有:
static bool X(object a, object b) => object.Equals(a, b);
static bool X(int a, int b) => a == b;
static bool M<T>(T v, T m) => X(v, m);
然后重载解析选择 X(object, object)
并编译代码,就像您写的那样:
static bool M<T>(T v, T m) => X((object)v, (object)m);
如果 T
结果是 int
,那么两个 int
都被装箱到 object
。
让我再次强调一下。当我们开始抖动时,我们已经知道将调用哪个 X
;该决定是在 C# 编译时做出的。 C# 编译器原因 "I've got two Ts, I do not know that they are convertible to int, so I've got to choose the object version".
这与 C++ 模板代码形成对比,后者为每个模板实例化重新编译代码,并重新进行重载解析。
这样就回答了最初提出的问题。
现在让我们进入奇怪的细节。
When jit compiling M<int>
, is the jitter permitted to notice that M<int>
calls X(object, object)
, which then calls object.Equals(object, object)
, which is known to compare two boxed ints for equality, and generate the code directly that compares the two ints in their unboxed form?
是的,允许抖动执行该优化。
Does it in practice perform that optimization?
据我所知不是。抖动确实执行了一些内联优化,但据我所知,它不执行任何先进的内联。
Are there situations in which the jitter does in practice elide a boxing?
是的!
Can you give some examples?
没问题。考虑以下 糟糕的 代码:
struct S
{
public int x;
public void M()
{
this.x += 1;
}
}
当我们这样做时:
S s = whatever;
s.M();
发生了什么?值类型中的 this
等同于 ref S
类型的参数。所以我们将引用引用到 s
,将其传递给 M
,依此类推。
现在考虑以下问题:
interface I
{
void M();
}
struct S : I { /* body as before */ }
现在假设我们这样做:
S s = whatever;
I i = s;
i.M();
发生了什么?
- 将
s
转换为I
是一个装箱转换,所以我们分配一个盒子,让盒子实现I
,并复制s
在盒子.
- 调用
i.M()
将盒子作为接收者传递给盒子中I
的实现。然后在框中获取对 s
副本的引用,并将该引用作为 this
传递给 M
.
好吧,现在是让你感到困惑的部分。
void Q<T>(T t) where T : I
{
t.M();
}
...
S s = whatever;
Q<S>(s);
现在发生了什么?显然我们把s
复制成t
,没有装箱;两者都是 S
类型。但是:I.M
需要一个 I
类型的接收者,而 t
是 S
类型的接收者。我们必须做我们以前做过的事吗?我们是否将 t
框到一个实现 I
的框,然后该框调用 S.M
并将 this
作为对框的引用?
没有。抖动生成省略装箱的代码,直接用ref t
调用S.M
as this
.
这是什么意思?这意味着:
void Q<T>(T t) where T : I
{
t.M();
}
和
void Q<T>(T t) where T : I
{
I i = t;
i.M();
}
不一样!前者发生变异 t
因为跳过了装箱。后者框然后变异框。
这里的要点应该是可变值类型是纯粹的邪恶,你应该不惜一切代价避免它们。正如我们所见,您很容易遇到这样的情况:您认为您应该改变一个副本,但您正在改变原始文件,或者更糟糕的是,您认为您正在改变一个原始文件,但您正在改变一个副本复制.
What bizarre magic makes this work?
使用sharplab.io并将我给出的方法分解为IL。仔细阅读 IL;有什么不明白的可以查。使此优化工作的所有神奇机制都有详细记录。
Does the jitter always do this?
没有! (如果您按照我刚才的建议阅读了所有文档,您就会知道。)
但是,构造一个无法执行优化的场景有点棘手。我会把它当作一个谜题:
给我写一个程序,其中我们有一个实现接口 I
的结构类型 S
。我们将类型参数 T
约束为 I
,并用 S
构造 T
,并传入一个 T t
。我们调用了一个方法t
作为receiver,抖动总是导致receiver被装箱
提示:我预测被调用的方法名有七个字母。我说得对吗?
挑战 #2:一个问题:是否也可以证明拳击是使用我之前建议的相同技术发生的? (该技术是:表明必须发生装箱,因为副本发生了突变,而不是原始突变。
Are there scenarios where the jitter boxes unnecessarily?
是的!当我在编译器上工作时,抖动并没有优化掉 "box T to O, immediately unbox O back to T" 指令序列,有时 C# 编译器需要生成这样的序列才能让验证者满意。我们要求实施优化;我不知道它是否曾经是。
Can you give an example?
当然可以。假设我们有
class C<T>
{
public virtual void M<U>(T t, U u) where U : T { }
}
class D : C<int>
{
public override void M<U>(int t, U u)
{
好的,现在你知道 U
唯一可能的类型是 int
,因此 t
应该可以分配给 u
,并且 u
应该可以分配给 t
,对吧?但是 CLR 验证器并不这么看,然后您可以 运行 遇到编译器必须生成导致 int
装箱到 object
然后取消装箱到 [= 的代码的情况72=],即int
,所以往返没有意义。
What's the takeaway here?
- 不要改变值类型。
- 泛型不是模板。过载解析只会发生一次。
- 抖动非常努力地消除泛型中的装箱,但是如果
T
被转换为 object
,那么 T
确实被转换为 object
.
public static bool Equal<T>(T value, T match) {
return Equals(value, match);
}
所以问题是,如果 T 是 int32,这里会进行装箱还是编译器会选择没有装箱的 int32 等于?
对原始问题和 Rango(基本上正确)的回答的评论有些混乱,所以我想我应该把它们弄清楚。
首先,请注意泛型在 C# 中的工作原理。泛型不是模板!
在 C# 中,泛型被 C# 编译器编译成泛型 IL,然后通过抖动将 IL 重新编译成专用形式。例如,如果我们有一个方法 M<T>(T t)
,那么 C# 编译器会将该方法及其主体 一次 编译到 IL 中。
当出现抖动时,调用M<string>
、M<object>
或M<IEnumerable>
将触发exactly one编译;抖动非常聪明,它可以将主体编译成一种形式,无论类型参数是什么,它都可以工作,前提是类型参数是引用类型。但是M<int>
和M<double>
会各自编译成各自的汇编代码体。
注意抖动不知道C#的规则,C#做重载解析。当 C# 编译器生成 IL 时,每个方法调用的确切方法已经被选择。所以如果你有:
static bool X(object a, object b) => object.Equals(a, b);
static bool X(int a, int b) => a == b;
static bool M<T>(T v, T m) => X(v, m);
然后重载解析选择 X(object, object)
并编译代码,就像您写的那样:
static bool M<T>(T v, T m) => X((object)v, (object)m);
如果 T
结果是 int
,那么两个 int
都被装箱到 object
。
让我再次强调一下。当我们开始抖动时,我们已经知道将调用哪个 X
;该决定是在 C# 编译时做出的。 C# 编译器原因 "I've got two Ts, I do not know that they are convertible to int, so I've got to choose the object version".
这与 C++ 模板代码形成对比,后者为每个模板实例化重新编译代码,并重新进行重载解析。
这样就回答了最初提出的问题。
现在让我们进入奇怪的细节。
When jit compiling
M<int>
, is the jitter permitted to notice thatM<int>
callsX(object, object)
, which then callsobject.Equals(object, object)
, which is known to compare two boxed ints for equality, and generate the code directly that compares the two ints in their unboxed form?
是的,允许抖动执行该优化。
Does it in practice perform that optimization?
据我所知不是。抖动确实执行了一些内联优化,但据我所知,它不执行任何先进的内联。
Are there situations in which the jitter does in practice elide a boxing?
是的!
Can you give some examples?
没问题。考虑以下 糟糕的 代码:
struct S
{
public int x;
public void M()
{
this.x += 1;
}
}
当我们这样做时:
S s = whatever;
s.M();
发生了什么?值类型中的 this
等同于 ref S
类型的参数。所以我们将引用引用到 s
,将其传递给 M
,依此类推。
现在考虑以下问题:
interface I
{
void M();
}
struct S : I { /* body as before */ }
现在假设我们这样做:
S s = whatever;
I i = s;
i.M();
发生了什么?
- 将
s
转换为I
是一个装箱转换,所以我们分配一个盒子,让盒子实现I
,并复制s
在盒子. - 调用
i.M()
将盒子作为接收者传递给盒子中I
的实现。然后在框中获取对s
副本的引用,并将该引用作为this
传递给M
.
好吧,现在是让你感到困惑的部分。
void Q<T>(T t) where T : I
{
t.M();
}
...
S s = whatever;
Q<S>(s);
现在发生了什么?显然我们把s
复制成t
,没有装箱;两者都是 S
类型。但是:I.M
需要一个 I
类型的接收者,而 t
是 S
类型的接收者。我们必须做我们以前做过的事吗?我们是否将 t
框到一个实现 I
的框,然后该框调用 S.M
并将 this
作为对框的引用?
没有。抖动生成省略装箱的代码,直接用ref t
调用S.M
as this
.
这是什么意思?这意味着:
void Q<T>(T t) where T : I
{
t.M();
}
和
void Q<T>(T t) where T : I
{
I i = t;
i.M();
}
不一样!前者发生变异 t
因为跳过了装箱。后者框然后变异框。
这里的要点应该是可变值类型是纯粹的邪恶,你应该不惜一切代价避免它们。正如我们所见,您很容易遇到这样的情况:您认为您应该改变一个副本,但您正在改变原始文件,或者更糟糕的是,您认为您正在改变一个原始文件,但您正在改变一个副本复制.
What bizarre magic makes this work?
使用sharplab.io并将我给出的方法分解为IL。仔细阅读 IL;有什么不明白的可以查。使此优化工作的所有神奇机制都有详细记录。
Does the jitter always do this?
没有! (如果您按照我刚才的建议阅读了所有文档,您就会知道。)
但是,构造一个无法执行优化的场景有点棘手。我会把它当作一个谜题:
给我写一个程序,其中我们有一个实现接口 I
的结构类型 S
。我们将类型参数 T
约束为 I
,并用 S
构造 T
,并传入一个 T t
。我们调用了一个方法t
作为receiver,抖动总是导致receiver被装箱
提示:我预测被调用的方法名有七个字母。我说得对吗?
挑战 #2:一个问题:是否也可以证明拳击是使用我之前建议的相同技术发生的? (该技术是:表明必须发生装箱,因为副本发生了突变,而不是原始突变。
Are there scenarios where the jitter boxes unnecessarily?
是的!当我在编译器上工作时,抖动并没有优化掉 "box T to O, immediately unbox O back to T" 指令序列,有时 C# 编译器需要生成这样的序列才能让验证者满意。我们要求实施优化;我不知道它是否曾经是。
Can you give an example?
当然可以。假设我们有
class C<T>
{
public virtual void M<U>(T t, U u) where U : T { }
}
class D : C<int>
{
public override void M<U>(int t, U u)
{
好的,现在你知道 U
唯一可能的类型是 int
,因此 t
应该可以分配给 u
,并且 u
应该可以分配给 t
,对吧?但是 CLR 验证器并不这么看,然后您可以 运行 遇到编译器必须生成导致 int
装箱到 object
然后取消装箱到 [= 的代码的情况72=],即int
,所以往返没有意义。
What's the takeaway here?
- 不要改变值类型。
- 泛型不是模板。过载解析只会发生一次。
- 抖动非常努力地消除泛型中的装箱,但是如果
T
被转换为object
,那么T
确实被转换为object
.