12

我在 C++ 中创建了一个简单的类 Storer,用于处理内存分配。它包含六个字段变量,所有这些变量都在构造函数中赋值:

int x;
int y;
int z;
char c;
long l;
double d;

我对这些变量的存储方式很感兴趣,所以我编写了以下代码:

Storer *s=new Storer(5,4,3,'a',5280,1.5465);
cout<<(long)s<<endl<<endl;
cout<<(long)&(s->x)<<endl;
cout<<(long)&(s->y)<<endl;
cout<<(long)&(s->z)<<endl;
cout<<(long)&(s->c)<<endl;
cout<<(long)&(s->l)<<endl;
cout<<(long)&(s->d)<<endl;

我对输出很感兴趣:

33386512

33386512
33386516
33386520
33386524
33386528
33386536

为什么 char c 占用四个字节?sizeof(char) 当然返回 1,那么为什么程序分配的内存比它需要的多呢?确认使用以下代码分配了太多内存:

cout<<sizeof(s->c)<<endl;
cout<<sizeof(Storer)<<endl;
cout<<sizeof(int)+sizeof(int)+sizeof(int)+sizeof(char)+sizeof(long)+sizeof(double)<<endl;

打印:

1
32
29

确认确实是不必要地分配了 3 个字节。谁能向我解释为什么会这样?谢谢。

4

5 回答 5

24

数据对齐和编译器填充打个招呼!

CPU 没有类型的概念,它在 32 位(或 64 位、或 128 位 (SSE) 或 256 位 (AVX) - 让我们保持简单的 32 位)寄存器中获得的内容需要正确对齐以便正确有效地处理。想象一个简单的场景,你有一个 char,后跟一个 int。在 32 位架构中,char 为 1 个字节,整数为 4 个字节。

一个 32 位寄存器必须在其边界上中断,只接收整数的 3 个字节,并将第 4 个字节留作“第二次运行”。它无法以这种方式正确处理数据,因此编译器将添加填充以确保有效处理所有内容。这意味着根据所讨论的类型添加一定数量的填充。

为什么错位是个问题?

计算机不是人类,它不能仅仅用一双眼睛和一个大脑来挑选它们。它必须非常确定并谨慎对待如何做事。首先,它加载一个包含给定信息的 n 个字节的块,将其四处移动,以便删除不相关的信息,然后另一个,再次,移出一堆与手头的操作没有任何关系的不必要的字节,然后只有这样它才能进行必要的操作。通常你有两个操作数,这只是一个完整的。当你完成所有这些工作时,你才能真正处理它。当您可以简单地正确对齐数据时(而且大多数时候,如果您没有做任何花哨的事情,编译器会为您做这件事),性能开销太大了。

你能想象一下吗?

视觉上 - 第一个绿色字节是提到的字符,三个绿色字节加上第二个块的第一个红色是 4 字节 int,在 4 字节访问边界上进行颜色编码(我们谈论的是 32 位登记)。底部的“替代部分”显示了一个理想的设置,其中 int 正确命中寄存器(字符被填充到图像之外的某个地方):

在此处输入图像描述

阅读有关数据对齐的更多信息,当您处理 SSE(128 位 regs)或 AVX(256 位 regs)等指令集的花哨扩展时,这非常方便,因此必须特别小心,以便优化向量化不会失败(在 SSE 的 16 字节边界上对齐,16*8 -> 128 位)。

关于用户定义对齐的附加说明

phonetagger在注释中提出了一个有效的观点,即可以通过预处理器分配编译指示指令以强制编译器以用户、程序员指定的方式对齐数据。但是这样的指令,比如#pragma pack(...),是对编译器的一个声明,你知道你在做什么,什么最适合你。确保你这样做,因为如果你不能适应你的环境,你可能会遇到各种惩罚——最明显的是使用你没有自己编写的外部库,它们在打包数据的方式上有所不同。

当它们发生冲突时,事情就会爆炸。最好的建议是在这种情况下谨慎行事,并真正了解手头的问题。如果您不确定,请将其保留为默认值。如果您不确定但必须使用对齐为王的 SSE 之类的东西(而且不是默认的,也不是很简单),请在线查阅各种资源或在此处提出另一个问题。

于 2012-06-29T20:10:01.973 回答
5

我打个比方来帮助你理解。

假设有一条长面包,你有一台切割机,可以将它切成等厚的薄片。然后你把这些面包分发给孩子们。每个孩子都拿着他们的面包,公平地做他们想做的事(把 Nutella 放在他们身上然后吃,等等)。他们甚至可以用它制作更薄的切片并像那样使用它。

如果一个孩子来找你,说他不想要每个人都吃的那片,而是更薄的那片,那你就会有困难,因为你的切割机经过优化,至少可以切割最少的量,这让每个人都开心. 但是当一个孩子要求更薄的切片时,你必须重新发明机器或增加机器的复杂性,比如引入两种切割模式。你不想要那个。最终你放弃了,还是给了他一大块。

这与它发生的原因相同。希望你能理解这个类比。

于 2012-06-29T20:08:56.807 回答
3

数据对齐是 char 分配 4 个字节的原因:数据对齐

于 2012-06-29T20:03:09.577 回答
2

char不占用四个字节:它像往常一样占用一个字节。您可以通过打印来检查它sizeof(char)。其他三个字节是编译器插入的填充,以优化对类的其他成员的访问。根据硬件的不同,访问多字节类型(例如 4 字节整数)通常要快得多,因为它们位于可被 4 整除的地址。编译器可以在 int 成员之前插入最多三个字节的填充,以将其与良好的内存地址对齐,以便更快地访问。

如果您想尝试使用类布局,可以使用名为offsetof. 它有两个参数 - 成员的名称和类的名称,它返回从结构的基地址到内存中成员位置的字节数。

cout << offsetof(Storer, x) << endl;
cout << offsetof(Storer, y) << endl;
cout << offsetof(Storer, z) << endl;
于 2012-06-29T20:02:45.077 回答
1

结构成员以特定方式对齐。通常,如果您想要最紧凑的表示,请按大小递减顺序列出成员。

http://en.wikipedia.org/wiki/Data_structure_alignment#Typical_alignment_of_C_structs_on_x86

于 2012-06-29T20:03:21.160 回答