Boxing/Unboxing 在本地范围上下文之外

Boxing/Unboxing outside of local scope context

我花了很长时间才明白 boxing/unboxing 不是将变量 ['s value] 从堆栈复制到堆的过程,而只是值 <-> 引用之间的转换过程。所有这一切都是因为我看到的所有例子都是这样的:

int i = 12;
object o = i;
int j = (int)o;

伴随着一个可怕的图表(在许多不同的例子中我看到它们是相同的)看起来像这样:

这让我得出了错误的结论,即装箱是从堆栈移动到堆并发生值-> 引用转换(反之亦然)的过程。

现在我只了解转换过程本身,但有一些细微差别我需要深入的帮助:

1.当 boxing/unboxing 发生在实例 variables/class 字段时,它在内存示意图方面看起来如何?

默认情况下,所有这些变量都已分配到堆中。此范围内的任何装箱示例及其行为方式?不想画就不用画了,书面说明即可。

2。这里发生了什么,例如:

int i = 12;
object o = 12; // boxing? if so - why?
int i = (int)o; // unboxing?
int k = (int)o; // Same?

3。如果 boxing/unboxing 根据 memory/performance 考虑 "bad" - 如果您不能这样做,您将如何处理?例如:

int i = 10;
ArrayList arrlst = new ArrayList();
arrlst.Add(i);
int j = (int)arrlst[0];

除了"use generics"(例如不适用的情况)之外,这里还有什么合适的解决方案。


原答案

Boxing/Unboxing 不是进出堆,而是间接的。当变量被装箱时,你得到的是一个新对象(好的,在堆中,这是一个实现细节),它有一个值的副本。

现在,您获取一个对象并读取其字段之一……会发生什么?你得到一个值。 实现细节是它被加载到堆栈中[*]你得到的值可以被装箱(你可以创建一个新对象来保存对它的引用)。

[*]:然后,例如,您将调用一个方法(或运算符),该方法将从堆栈中读取其参数(MSIL 中的语义是堆栈操作)。

顺便说一句,当你拿到字段并装箱时,箱子里的是副本。 想一想,你装箱的东西是从栈中来的(你先从堆中复制到栈中,然后装箱。至少MSIL中是这样的语义)。例子:

void Main()
{
    var t = new test();
    t.boxme = 1;
    object box = t.boxme;
    t.boxme = 2;
    Console.WriteLine(box); // outputs 1
}

class test
{
    public int boxme;
}

在 LINQPad 上测试。


扩展答案

在这里我将复习编辑问题中的要点...

1. How does it looks in terms of memory schematics when boxing/unboxing happens with instance variables/class field?

By default, all these variables are already allocated in heap. Any examples of boxing in this scope and how does it behave? No need to draw it if you dont want, written explanation will do.

我知道你想要解释装箱如何在实例字段上工作。由于上面的代码演示了在实例字段上使用 box,因此我将重温该代码。

在深入研究代码之前,我想提一下我使用了“堆栈”这个词,因为——正如我在原始答案中所说——这是语言的语义。然而,实际上它不一定是文字堆栈。抖动很可能会优化代码以利用 CPU 寄存器。因此,当您看到我说我们将东西放入堆栈中以立即将它们取出时……是的,抖动可能会在那里使用寄存器。事实上,我们会反复将一些东西放在堆栈上;抖动可能决定为这些事情重用寄存器是值得的。

首先,我们使用的是一个非常简单但不实用的 class test,只有一个字段 boxme:

class test
{
    public int boxme;
}

关于此 class 我唯一要说的是提醒您编译器将生成一个不带参数的构造函数。考虑到这一点,让我们逐行检查 Main 中的代码...


var t = new test();

这一行做了两个操作:

  • 调用classtest的构造函数。它将在堆上创建一个新对象并将对它的引用推送到堆栈上。
  • 将局部变量 t 设置为我们从堆栈中弹出的内容。

t.boxme = 1;

这一行做了三个操作:

  • 将局部变量t的值压入栈顶。
  • 将值 1 压入栈顶。
  • 将字段 boxme 设置为从我们从堆栈中弹出引用的对象的堆栈 (1) 中弹出的值。

object box = t.boxme;

你可能猜到了,这条线就是我们在这里的目的。它总共执行四项操作:

  • 将局部变量t的值压入栈顶。
  • 将字段 boxme 的值(从堆栈中弹出引用的对象)压入堆栈顶部。
  • BOX:从栈中弹出,将值(以及它是一个int的事实)复制到一个新对象(在堆中创建),将对它的引用压入堆栈。
  • 将局部变量 box 设置为我们从堆栈中弹出的内容。

t.boxme = 2;

t.boxme = 1;基本相同,但我们推2而不是1

Console.WriteLine(box);
  • 将局部变量box的值压入栈顶。
  • 调用方法 System.Console.WriteLine 并将我们从堆栈中弹出的内容作为参数。

用户看到“1”


2. What happens here, for example:

int i = 12;
object o = 12; // boxing? if so - why?
int i = (int)o; // unboxing?
int k = (int)o; // Same?

耶,更多代码...


int i = 12;
  • 将值 12 压入栈顶。
  • 将局部变量 i 设置为我们从堆栈中弹出的内容。

到目前为止没有惊喜。


object o = 12; // boxing? if so - why?

是的,拳击。

  • 将值 12 压入栈顶。
  • BOX:从栈中弹出,将值(以及它是一个int的事实)复制到一个新对象(在堆中创建),将对它的引用压入堆栈。
  • 将局部变量 o 设置为我们从堆栈中弹出的内容。

为什么?因为使 int 看起来不像引用类型的 32 位。如果你想要一个值为 int 的引用类型,你需要将 int 的值放在它可以被引用的地方(放在堆上)然后你可以让你的 object.


int i = (int)o; // unboxing?

A local variable named 'i' is already defined in this scope

我想你的意思是:

i = (int)o; // unboxing?

是,开箱。

  • 将局部变量o的值压入栈顶。
  • Unbox: 读取我们从栈中弹出的对象的值,并将该值压入栈中。
  • 将局部变量 i 设置为我们从堆栈中弹出的内容。

int k = (int)o; // Same?

是的。只是一个不同的局部变量。


3. If boxing/unboxing considered "bad" in terms of memory/performance - how do you handle it in cases where you cant do that? For example:

int i = 10;
ArrayList arrlst = new ArrayList();
arrlst.Add(i);
int j = (int)arrlst[0];

1.使用泛型

int i = 10;
var arrlst = new List<T>();
arrlst.Add(i);
int j = arrlst[0];

不得不承认。有时 使用泛型 不是答案。

2。使用 ref

C# 7.0 有 ref return 并且本地人应该涵盖我们过去需要 boxing/unboxing 的一些情况。

通过使用 ref,您传递的是对存储在堆栈中的值的引用。由于 ref 的想法是您可以修改原始值,因此使用 box(将值复制到堆中)将违背其目的。

3。关注盒子寿命

您可以尝试重复使用您的引用,而不是多次不必要地装箱相同的值。这可能有助于保持较低的盒子数量,并且垃圾收集器会发现这些盒子是长寿命的盒子并减少检查它们的频率。

另一方面,垃圾收集器将非常有效地处理短命的盒子。因此,如果您无法避免制作大量 boxing/unboxing,请尝试制作短命的盒子。

4.尝试使用引用类型

如果您遇到性能问题,因为您有许多长寿命的盒子...您可能需要制作一些 classes。如果您一开始就使用引用类型,则无需将它们装箱。

虽然如果您需要用于互操作的结构可能会出现问题...嗯...可能不是您要找的东西,但请查看 ref structSpan<T>等。阿尔。可以通过其他方式节省您的分配。

5.让它成为

不拳不行,不拳不行。

例如,如果您需要一个泛型容器来对泛型类型的成员进行原子操作...但您还需要允许泛型类型成为值类型...那么您会怎么做?好吧,当您需要存储一些非原子值类型时,您必须使用 object 类型初始化容器。

不,那样的话ref救不了你,因为ref不保证原子性。

与其更加努力地通过优化 boxing/unboxing 的使用来获得性能提升,不如寻找其他方法来提高性能。例如,我正在谈论的那个通用容器可能很昂贵,但如果它允许您并行化某些算法并且给您带来的性能提升大于该成本,那么它是合理的。