4

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

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

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

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

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

4

7 回答 7

1

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

数组

int A[512];

这个声明给你一个 512int的数组。没有其他的。没有指针,什么都没有。只是一个s数组int。数组的大小将为512 * sizeof(int).

名字

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

转换

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

指针

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

情况

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

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

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

  • 每当引用数组的 glvalue 出现为需要纯右值的表达式的操作数时。

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

于 2013-03-05T16:52:27.843 回答
1

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

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

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

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

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

于 2013-03-05T17:12:32.780 回答
0

不,CLR 中的对象也没有映射到C++您所指的(我想象的)“简单”内存映射。请记住,您可以CLR使用反射对对象进行操作,这意味着每个对象都必须在其中包含附加信息(清单)。这已经添加了更多的内存,而不仅仅是对象的普通内容,还添加了一个用于在多线程环境中进行管理的指针,并且您在对象的预期内存分配locking方面走得很远。CLR

还要记住,指针大小在位机32之间有所不同。64

于 2013-03-05T16:33:05.357 回答
0

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

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

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

以上适用于 C 和 C++。

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

于 2013-03-05T16:33:38.207 回答
0

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

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

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

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

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

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

于 2013-03-05T16:33:44.467 回答
0

关于本机 C++

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

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

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

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

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

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

于 2013-03-05T16:38:33.940 回答
0

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

让我们从一个简单的例子开始,一个简单的 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++ 一样,堆栈内存可以非常快速地分配/释放,并且不会产生内存碎片,因此在考虑程序的内存占用时,通常将其分开是有意义的。

于 2013-03-05T17:30:47.350 回答