【问题标题】:Does an array or object's pointer/reference handle affect its size?数组或对象的指针/引用句柄会影响其大小吗?
【发布时间】:2013-03-05 16:27:52
【问题描述】:

我知道如果我有一个数组int A[512],那么引用 A 可以指向第一个元素。在指针算法中,内存被引用为A + index

但如果我没记错的话,指针/引用也会占用机器字的空间。假设一个int占用了一个机器字,那是不是意味着上面数组的512个整数占用了513个字的空间呢?

C++ 或 C# 中的对象及其数据成员的真/假是否相同?

更新:哇你们真快。澄清一下,我对 C++ 和 C# 在处理此问题的方式不同以及如何调整对象大小以适应缓存行(如果可能)感兴趣。

更新:我已经意识到指针和数组之间的区别。我知道数组不是指针,并且我上面引用的指针算法仅在数组转换为指针后才有效。但是,我认为这种区别与整个问题无关。我对如何在 C++ 和 C# 中将数组和其他对象存储在内存中感兴趣。

【问题讨论】:

  • 对于 C# 来说不止如此,但对于 C++ 则不然。在 C++ 中,数组实际上只是一个指向内存块的指针。但是,在 C++ 中,可能会有其他开销,具体取决于内存的来源。堆有保护块和其他需要考虑的家务。在 C# 中,它更像是一个 C++ 向量——它是一个类,它有一些其他字节的开销。有关所有血腥细节,请参见此处:msdn.microsoft.com/en-us/magazine/cc163791.aspx
  • 有没有办法确定 C# 中的开销?我想调整一些对象的大小以完全适合缓存行。
  • 那将是编译器/IDE 特定的。除了实验(检查不同大小数组的代码大小)之外,我不确定您将如何检查。
  • 任何开销都将特定于 CLR 和垃圾收集器的实现(不同平台上的实现可能不同,并且版本之间会发生变化)。恕我直言,如果您对这些事情有正当顾虑(即它们会对您的解决方案产生明显影响),那么您不应该使用 .net。
  • 我会考虑的。不过,跨平台的灵活性和反射能力很有吸引力,所以我很想知道如何处理或管理这种开销,以免严重损害性能。在运行时收集关于我们正在运行的机器的数据似乎可能会有所帮助。我不需要踏板到金属的速度,但我确实想要快速处理很多小物体。

标签: c# c++ arrays memory-management size


【解决方案1】:

请注意,当您谈论将数据装入缓存行时,包含引用的变量和它所引用的实际数据不会位于附近。该引用最终将在寄存器中结束,但它可能最初作为另一个对象的一部分存储在内存中的其他位置,或者作为堆栈上的局部变量。数组内容本身在被操作时仍然可以放入缓存行中,而不管与“对象”相关的任何其他开销。如果您对它在 C# 中的工作方式感到好奇,Visual Studio 有一个反汇编器视图,它显示为您的代码生成的实际 x86 或 x64 程序集。

数组引用在 IL(中间语言)级别具有特殊的内置支持,因此您会发现加载/使用内存的方式与在 C++ 中使用数组基本相同。在引擎盖下,对数组的索引是完全相同的操作。您将开始注意到差异的地方是,如果您使用“foreach”对数组进行索引,或者当数组是对象类型数组时开始必须“取消装箱”引用。

请注意,当您在方法中本地实例化对象时,可能会出现 C++ 和 C# 之间的内存局部性差异。 C++ 允许您在堆栈上实例化数组,这创建了一种特殊情况,其中数组内存实际上存储在靠近“引用”和其他局部变量的位置。在 C# 中,(托管)数组的内容将始终在堆上分配。

另一方面,当引用堆分配的对象时,C# 有时可以比 C++ 具有更好的内存局部性,尤其是对于短期对象。这是由于 GC 按其“世代”(它们存在多长时间)存储对象的方式以及它所做的堆压缩。短期对象在不断增长的堆上快速分配;收集时,堆也被压缩,防止“碎片”导致非压缩堆中的后续分配分散在内存中。

您可以在 C++ 中使用“对象池”技术(或通过避免频繁出现的小型短期对象)获得类似的内存局部性优势,但这需要一些额外的工作和设计。当然,这样做的代价是 GC 必须运行,线程劫持、提升生成、压缩和重新分配引用会在一些不可预测的时间导致可测量的开销。在实践中,开销很少成为问题,尤其是 Gen0 收集,它针对频繁分配的短期对象的使用模式进行了高度优化。

【讨论】:

  • 感谢您提供的信息丰富的回答。我相当确定在 C# 中可以为数组分配堆栈,但通常不是。这取决于 JIT 编译器和 CLR 来决定,通常对象是堆分配的,但这不能保证。也就是说,我知道引用的存储位置和它指向的对象的存储位置可能大不相同。我不知道作为特殊情况对数组进行不同的管理。谢谢!
【解决方案2】:

您似乎对 C++ 中的数组和指针有误解。

数组

int A[512];

此声明为您提供一个包含 512 个ints 的数组。没有其他的。没有指针,什么都没有。只是ints 的数组。数组的大小将为512 * sizeof(int)

名字

名称A 指的是该数组。它不是指针类型。它是数组类型。它是一个名称,它指的是数组。名称只是编译时构造,用于告诉编译器您在谈论什么对象。名称在运行时不存在。

转换

在某些情况下可能会发生一种称为数组到指针转换的转换。转换采用数组类型的表达式(例如简单表达式A)并将其转换为指向其第一个元素的指针。也就是说,在某些情况下,表达式A(表示数组)可能会转换为int*(指向数组中的第一个元素)。

指针

由数组到指针的转换创建的指针在它所属的表达式的持续时间内存在。它只是在那些特定情况下出现的一个临时对象。

情况

数组到指针的转换是一种标准转换,可能发生的情况包括:

  • 从数组转换为指针时。例如,(int*)A

  • 初始化指针类型的对象时,例如int* = A;

  • 每当引用数组的泛左值作为期望纯右值的表达式的操作数出现时。

    当您为数组下标时会发生这种情况,例如使用A[20]。下标运算符需要指针类型的纯右值,因此A 进行数组到指针的转换。

【讨论】:

  • 感谢您提供的信息。我不知道这种区别,因为我的 C++ 教科书(诚然很老)在实现方面总是将数组视为指向索引的指针(但不是打字)。
【解决方案3】:

不,CLR 中的对象确实映射到您所指的C++(我想象)的“简单”内存映射。请记住,您可以使用反射对 CLR 中的对象进行操作,这意味着 每个 对象必须在其中包含附加信息(manifest)。这已经增加了比对象的普通内容更多的内存,还添加了一个 pointer 用于在多线程环境中进行locking 管理,就预期而言你会走得很远CLR 对象的内存分配。

还请记住,指针大小在 3264 位机之间有所不同。

【讨论】:

    【解决方案4】:

    我认为您混淆了 C++ 中的数组和指针。

    int 的数组就是这样,它是内存中的一组位置,每个位置占用 sizeof(int),您可以在其中存储 N-1 个ints。

    指针是一种可以指向内存位置的类型,并且占用内存中的 CPU 寄存器大小,因此在 32 位机器上,sizeof(int*) 将是 32 位。

    如果你想要一个指向你的数组的指针,你可以这样做:int * ptr = &A[0]; 这指向数组中的第一个元素。现在您有了占用内存的指针(CPU 字大小),并且您有了ints 的数组。

    当您将数组传递给 C 或 C++ 中的函数时,它衰减为指向数组中第一个元素的指针。这并不是说指针是数组,而是说从数组到指针存在衰减

    在 C# 中,您的数组是一种引用类型,并且您没有指针,因此您不必担心。它只占用数组的大小。

    【讨论】:

    • 数组的声明创建了字面意义上的指针(即数组的名字实际上是一个指针)。
    • 我没有说数组是指针。我说过数组的名称在技术上是一个指针,即使使用不同,它的行为也是相同的。
    • @tmwoods 数组名就是数组名。它在某些上下文中衰减为指针,但它不是指针,技术上或其他方面。
    • 不,数组的名称在技术上不是一个指针,它是一个数组。其余的都是废话。
    • @tmwoods 因为它们是由不知道自己在说什么的人写的?众所周知,cplusplus.com 作为参考和学习材料都很糟糕。
    【解决方案5】:

    一个数组,int A[512] 占用 512 * sizeof(int)(+ 编译器决定添加的任何填充 - 在这种特殊情况下,很可能没有填充)。

    数组A 可以转换为指向int A 的指针并与A + index 一起使用这一事实使用了在实现中A[index] 几乎总是与A + index 完全相同的指令这一事实。在这两种情况下都会发生到指针的转换,因为要到达 A[index],我们必须获取数组 A 的第一个地址,并添加 index 乘以 sizeof(int) - 无论您将其写为 A[index] 还是 @987654331 @ 没有任何区别。在这两种情况下,A 指的是数组中的第一个地址,index 指的是其中元素的数量。

    这里没有使用额外的空间。

    以上内容适用于 C 和 C++。

    在 C# 和其他使用“托管内存”的语言中,跟踪每个变量需要额外的开销。这不会影响变量A 本身的大小,但它当然必须存储在某个地方,因此每个变量,无论是单个整数还是非常大的数组,都会有一些开销,存储在某个地方,包括变量的大小和某种“引用计数”(变量被使用的位置,以及是否可以删除)。

    【讨论】:

    • 请注意,OP 说的是 C# AND C++,所以你最好说一下你说的是哪个(好像是 C++)
    • 没错,我对两种语言如何处理这个问题很感兴趣。我已更新问题以反映这一点。
    【解决方案6】:

    关于原生C++

    但如果我没记错的话,指针/引用也占用了一个机器字空间

    引用不一定要占用内存空间。根据 C++11 标准的第 8.3.2/4 段:

    未指定引用是否需要存储 (3.7)。

    在这种情况下,您可以像指针一样使用 A,并且确实会在必要时衰减指针(例如,将其作为参数传递给函数),但A 的类型是int[512],而不是int*:因此,A 不是指针。例如,您不能这样做:

    int A[512];
    int B;
    A = &B;
    

    不需要任何内存位置来存储A(即用于存储数组开始的内存地址),因此您的编译器很可能不会分配任何额外的内存字节来保存地址A.

    【讨论】:

      【解决方案7】:

      我们在这里有多个不同的例子,因为我们甚至有几种语言要讨论。

      让我们从简单的例子开始,一个简单的 C++ 数组:

      int array[512];
      

      这里的内存分配会发生什么?在堆栈上为数组分配了 512 个字的内存。没有分配堆内存。没有任何类型的开销;没有指向数组的指针,什么都没有,只有 512 个内存字。

      这是在 C++ 中创建数组的另一种方法:

      int * array = new int[512];
      

      这里我们在堆上创建一个数组。它将分配 512 个字的内存,而不会在堆上分配额外的内存。然后,一旦完成,该数组开始的地址将被放置在堆栈上的一个变量中,占用一个额外的内存字。如果您查看整个应用程序的总内存占用量,是的,它将是 513,但值得注意的是,一个在堆栈上,其余在堆上(堆栈内存分配成本要低得多,并且不会导致碎片化,但如果你过度使用它或误用它,你会更容易用完。

      现在进入 C#。在 C# 中,我们没有两种不同的语法,您所拥有的只是:

      int[] array = new int[512];
      

      这将在堆上创建一个新的数组对象。它将包含用于数组中数据的 512 个字的内存,以及用于数组对象开销的一些额外内存。它需要 4 个字节来保存数组的计数、一个同步对象以及一些我们不需要考虑的其他开销。该开销很小,并且不依赖于数组的大小。

      还有一个指针(或“引用”,更适合在 C# 中使用)指向放置在堆栈上的该数组,这将占用一个字的内存。与 C++ 一样,堆栈内存可以非常快速地分配/释放,并且不会产生内存碎片,因此在考虑程序的内存占用时,通常将其分开是有意义的。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2021-07-08
        • 1970-01-01
        • 1970-01-01
        • 2014-02-20
        • 1970-01-01
        • 1970-01-01
        • 2017-08-06
        相关资源
        最近更新 更多