为什么不管类型的值是什么,它总是有一定的大小?

实现在类型的实际大小上可能有所不同,但是在大多数情况下,无符号 int 和 float 这样的类型总是4个字节。但是为什么一个类型总是占用 当然的内存量而不管它的值如何呢?例如,如果我创建了以下值为255的整数

int myInt = 255;

那么 myInt在我的编译器中将占用4个字节。但是,实际值 255只能用1字节表示,那么为什么 myInt不能只占用1字节的内存呢?或者更一般化的问法是: 为什么一个类型只有一个与之相关联的大小,而表示该值所需的空间可能小于这个大小?

12589 次浏览

因为类型从根本上表示存储,并且它们是根据它们可以保存的 最大值值定义的,而不是当前值。

很简单的比喻就是房子——房子有固定的大小,不管有多少人住在里面,而且还有一个建筑规范,规定了一定大小的房子里最多可以住多少人。

然而,即使一个人住在可容纳10人的房子里,房子的大小也不会受到目前住户人数的影响。

因为使用具有动态大小的简单类型会非常复杂和计算量大。我不确定这是否可行。
计算机将不得不检查数字在每次改变它的值后有多少位。会有很多额外的手术。 如果在编译过程中不知道变量的大小,那么执行计算就会困难得多。

为了支持变量的动态大小,计算机实际上必须记住一个变量现在有多少字节,这需要额外的内存来存储这些信息。在对变量进行每次操作之前,必须对这些信息进行分析,以选择正确的处理器指令。

为了更好地理解计算机是如何工作的,以及为什么变量具有不变的大小,学习汇编语言的基础知识。

尽管如此,我认为使用 conexpr 值也可以实现类似的功能。但是,这会使代码对于程序员来说更难预测。我认为一些编译器优化可能会做类似的事情,但是他们对程序员隐藏它,以保持事情的简单性。

我在这里只描述了与程序性能有关的问题。我省略了所有必须通过减少变量大小来节省内存的问题。说实话,我觉得这根本不可能。


总之,只有在编译期间知道变量的值时,使用比声明的变量更小的变量才有意义。现代编译器很有可能这样做。在其他情况下,它会导致太多难以解决甚至无法解决的问题。

为什么一个类型只有一个相关的大小,当空间 需要表示的值可能小于该大小?

主要是因为校准要求。

根据 对齐/1:

对象类型具有对齐要求,这些要求对 可以分配该类型对象的地址。

想象一座有很多层的建筑,每一层都有很多房间。
每个房间是你的 尺寸(一个固定的空间)能够容纳 N 数量的人或物体。
由于房间的大小事先已知,它使建筑物的结构组成部分 结构合理

如果房间不对齐,那么建筑骨架就不会有良好的结构。

编译器应该为某些机器生成汇编程序(以及最终的机器代码) ,通常 C + + 尝试同情这些机器。

同情底层机器大致意味着: 使编写 C + + 代码变得容易,这将有效地映射到机器可以快速执行的操作上。因此,我们希望在硬件平台上提供对数据类型和操作的快速和“自然”访问。

具体来说,考虑一个特定的机器架构。

英特尔64和 IA-32架构软件开发者手册第一卷(链接)第3.4.1节说:

32位通用寄存器 EAX、 EBX、 ECX、 EDX、, 提供了 ESI、 EDI、 EBP 和 ESP 来保存 以下项目:

•逻辑运算和算术运算的操作数

•用于地址计算的操作数

•内存指针

因此,我们希望编译器在编译简单的 C + + 整数算法时使用这些 EAX、 EBX 等寄存器。这意味着当我声明一个 int时,它应该与这些寄存器兼容,这样我就可以有效地使用它们。

寄存器总是相同的大小(这里是32位) ,所以我的 int变量也总是32位。我将使用相同的布局(little-endian) ,这样就不必在每次将变量值加载到寄存器或将寄存器存储回变量时进行转换。

使用 Godbolt,我们可以准确地看到编译器对一些琐碎的代码做了什么:

int square(int num) {
return num * num;
}

(为简单起见,使用 GCC 8.1和 -fomit-frame-pointer -O3)编译为:

square(int):
imul edi, edi
mov eax, edi
ret

这意味着:

  1. int num参数在寄存器 EDI 中传递,这意味着它的大小和布局正是 Intel 期望的本机寄存器。这个函数不需要转换任何东西
  2. 乘法是一个单指令(imul) ,这是非常快的
  3. 返回结果只需要将其复制到另一个寄存器(调用方希望将结果放入 EAX)

编辑: 我们可以添加一个相关的比较,以显示使用非本机布局的差异。最简单的情况是在本机宽度以外的地方存储值。

再次使用 Godbolt,我们可以比较一个简单的本机乘法

unsigned mult (unsigned x, unsigned y)
{
return x*y;
}


mult(unsigned int, unsigned int):
mov eax, edi
imul eax, esi
ret

与非标准宽度的等效代码

struct pair {
unsigned x : 31;
unsigned y : 31;
};


unsigned mult (pair p)
{
return p.x*p.y;
}


mult(pair):
mov eax, edi
shr rdi, 32
and eax, 2147483647
and edi, 2147483647
imul eax, edi
ret

所有额外的指令都涉及到将输入格式(两个31位无符号整数)转换为处理器可以本机处理的格式。如果我们想要将结果存储回一个31位的值,那么将会有另外一两个指令来完成这个操作。

这种额外的复杂性意味着,只有在节省空间非常重要的时候,您才会为此而烦恼。在这种情况下,与使用本机 unsigneduint32_t类型相比,我们只节省了两位,因为使用本机 unsigneduint32_t类型可以生成更简单的代码。


关于动态尺寸的说明:

上面的示例仍然是固定宽度值,而不是可变宽度值,但是宽度(和对齐方式)不再与本机寄存器匹配。

X86平台有几种本机大小,除了主要的32位之外,还包括8位和16位(为了简单起见,我将略去64位模式和其他各种模式)。

这些类型(char、 int8 _ t、 uint8 _ t、 int16 _ t 等等)是体系结构直接支持的 还有——部分原因是为了向后兼容旧的8086/286/386/等等指令集。

当然,选择最小的 自然固定尺寸类型就足够了,这是一个很好的实践——它们仍然很快,单指令加载和存储,你仍然可以得到全速的本机算术,你甚至可以通过减少缓存丢失来提高性能。

这与变长编码非常不同-我曾经使用过其中的一些,它们非常可怕。每个加载都变成一个循环,而不是一条指令。每个商店也是一个循环。每个结构都是可变长度的,所以不能自然地使用数组。


关于效率的进一步说明

在后面的评论中,您一直在使用“高效”这个词,就我所知,它与存储大小有关。我们有时候确实会选择最小化存储空间——当我们要保存大量的值到文件或者通过网络发送它们时,这一点很重要。折衷之处在于,我们需要将这些值加载到 的寄存器中,并且执行转换并不是免费的。

当我们讨论效率时,我们需要知道我们正在优化什么,以及什么是权衡。使用非本机存储类型是用处理速度换取空间的一种方法,有时这种方法是有意义的。使用可变长度存储(至少对于算术类型) ,以 更多处理速度(以及代码复杂性和开发人员时间)换取进一步节省的空间通常最少。

为此付出的速度代价意味着,只有当你需要绝对最小化带宽或长期存储时,它才是值得的。在这种情况下,通常使用简单自然的格式更容易——然后只需用一个通用的系统(如 zip、 gzip、 bzip2、 xy 或其他)压缩它。


博士

每个平台都有一个体系结构,但是您可以提供基本上无限多种不同的数据表示方式。对于任何语言来说,提供无限数量的内置数据类型都是不合理的。因此,C + + 提供了对平台的本机、自然数据类型集的隐式访问,并允许您自己编写任何其他(非本机)表示形式。

因为在 C + + 这样的语言中,设计目标是将简单的操作编译成简单的机器指令。

所有主流的 CPU 指令集都与 固定宽度类型一起工作,如果您想要处理 可变宽度类型,则必须执行多个机器指令来处理它们。

至于 为什么的底层计算机硬件是这样的: 这是因为它更简单,更有效的 很多的情况下(但不是所有)。

把计算机想象成一段磁带:

| xx | xx | xx | xx | xx | xx | xx | xx | xx | xx | xx | xx | xx | ...

如果您只是告诉计算机查看磁带上的第一个字节 xx,它如何知道类型是否在那里停止,或继续到下一个字节?如果您有一个类似于 255(十六进制 FF)或者类似于 65535(十六进制 FFFF)的数字,那么第一个字节总是 FF

你怎么知道的?您要么只是选择一个大小并坚持它,要么必须添加额外的逻辑,并“超载”至少一位或字节值的含义,以表明该值继续到下一个字节。这种逻辑从来都不是“免费的”,要么你在软件中模拟它,要么你在 CPU 中添加一堆额外的晶体管来模拟它。

C 和 C + + 等语言的固定宽度类型反映了这一点。

不是这样的,更抽象的语言不太关心映射到最高效的代码,可以自由使用可变宽度编码(也称为“可变长度数量”或 VLQ)的数值类型。

进一步阅读: 如果你搜索“可变长度数量”,你可以找到一些例子,这种编码 实际上是有效的,值得额外的逻辑。通常需要存储大量的值,这些值可能位于大范围内的任何地方,但是大多数值倾向于一些小的子范围。


注意,如果一个编译器可以 证明,它可以在不破坏任何代码的情况下在更小的空间内存储值(例如,它是一个只在单个翻译单元内部可见的变量) ,那么 还有的优化启发式表明,它将在目标硬件上更有效率,它完全是 允许相应地优化它,并将它存储在更小的空间内,只要剩下的代码“好像”它做了标准的事情。

但是 ,当代码必须与可能单独编译的其他代码一起使用 互相协作时,大小必须保持一致,或者确保每段代码都遵循相同的约定。

因为如果它不是一致的,就会出现这样的复杂情况: 如果我有 int x = 255;,但是在后面的代码中执行 x = y会怎么样?如果 int可以是可变宽度的,那么编译器必须提前知道预先分配所需的最大空间量。这并不总是可能的,因为如果 y是从另一段单独编译的代码传入的参数,那该怎么办?

计算机内存被细分为一定大小的连续寻址块(通常为8位,称为字节) ,大多数计算机被设计为有效地访问具有连续地址的字节序列。

如果对象的地址在对象的生存期内从未更改,那么给定其地址的代码可以快速访问有问题的对象。然而,这种方法的一个基本限制是,如果为地址 X 分配了一个地址,然后为地址 Y 分配了另一个地址,这个地址相隔 N 个字节,那么除非移动 X 或 Y,否则 X 在 Y 的生命周期内不能增长超过 N 个字节。为了使 X 移动,宇宙中包含 X 地址的所有东西都必须更新以反映新地址,同样,Y 也必须移动。尽管设计一个系统来促进这样的更新是可能的(Java 和。NET 很好地管理它)处理那些在其生命周期中始终保持相同位置的对象要有效得多,这反过来又通常要求它们的大小必须保持不变。

它是一种优化和简化。

您可以有固定大小的对象。
或者你可以有不同大小的物体,但是存储价值和大小。

固定尺寸的物体

操作数字的代码不需要担心大小。您假设您总是使用4个字节,并使代码非常简单。

动态尺寸的物体

操作数字的代码在读取变量时必须理解它必须读取的值和大小。使用的大小,以确保所有的高位是零出在寄存器。

如果值没有超过其当前大小,那么只需将该值放回内存中。但是,如果该值已经缩小或增长,则需要将对象的存储位置移动到内存中的另一个位置,以确保它不会溢出。现在你必须跟踪这个数字的位置(因为它可以移动,如果它变得太大了以至于它的大小)。您还需要跟踪所有未使用的变量位置,以便它们可能被重用。

摘要

为固定大小的对象生成的代码要简单得多。

注意

压缩使用的事实是,255将放入一个字节。存储大型数据集的压缩方案主动地对不同的数字使用不同的大小值。但是,由于这不是实时数据,因此不存在上述复杂性。用较少的空间存储数据,代价是压缩/解压缩用于存储的数据。

Java 使用名为“ BigInteger”和“ BigDecimal”的类来实现这一点,显然 C + + 的 GMP C + + 类接口也是如此(感谢 Digital Trauma)。如果你愿意,你可以用几乎任何一种语言轻松地做到。

中央处理器总是有能力使用 BCD(二进码十进数) ,这是为支持任何长度的操作而设计的(但是你倾向于一次手动操作一个字节,按照今天的图形处理器标准,这将是很慢的)

为什么我们不使用这些或其他类似的解决方案?表演。最高性能的语言不能在某些紧密循环操作中展开变量——这将是非常不确定的。

在大规模存储和运输的情况下,打包值通常是您将使用的值的唯一类型。例如,传输到计算机的音乐/视频数据包可能需要花费一些时间来指定下一个值是2字节还是4字节作为大小优化。

一旦你的计算机上可以使用它,内存是便宜的,但速度和复杂的可调变量不是。.这是唯一的原因。

简短的回答是: 因为 C + + 标准是这么说的。

长远的答案是: 你能在计算机上做什么最终受到硬件的限制。当然,将一个整数编码成可变数量的字节存储是可能的,但是读取它要么需要特殊的 CPU 指令才能执行,要么你可以在软件中实现它,但是那样会非常慢。固定大小的操作在 CPU 中可用于加载预定义宽度的值,而对于可变宽度则没有。

另一个需要考虑的问题是计算机内存是如何工作的。假设您的整数类型可以占用1到4个字节的存储空间。假设您将值42存储到您的整数中: 它占用1个字节,并将其放置在内存地址 X 处。然后将下一个变量存储在位置 X + 1处(此时我不考虑对齐) ,以此类推。然后您决定将您的值更改为6424。

但这不是一个字节所能容纳的!那你是做什么的?剩下的放哪儿了?你已经有了 X + 1的值,所以不能把它放在那里。别的地方?你以后怎么知道在哪里?计算机内存不支持插入语义: 您不能仅仅将某些东西放在某个位置,然后将其后的所有东西推到一边以腾出空间!

旁白: 你所说的其实是数据压缩的领域。压缩算法的存在是为了把所有东西都压缩得更紧,所以至少其中一些算法会考虑不要为整数使用比它所需要的更多的空间。但是,压缩数据不容易修改(如果可能的话) ,并且每次对其进行更改时都会被重新压缩。

在某种意义上,有些物体的大小是可变的,比如 C++标准程式库。然而,这些都是动态分配它们所需要的额外内存。如果使用 sizeof(std::vector<int>),您将得到一个常量,该常量与对象管理的内存无关,如果分配一个包含 std::vector<int>的数组或结构,它将保留这个基本大小,而不是将额外的存储放在同一个数组或结构中。有一些 C 语法支持类似的东西,特别是可变长度的数组和结构,但是 C + + 没有选择支持它们。

语言标准以这种方式定义对象大小,以便编译器可以生成高效的代码。例如,如果在某些实现中,int碰巧是4字节长,而你声明 a作为指向或数组 int值的指针,那么 a[i]转换成伪代码,“取消引用地址 a + 4 × i。”这可以在恒定的时间内完成,这是一个非常常见和重要的操作,许多指令集架构,包括 x86和最初开发 C 的 DEC pDP 机器,可以在一个指令内完成。

将数据连续存储为可变长度单位的一个常见现实示例是编码为 UTF-8的字符串。(但是,编译器使用的 UTF-8字符串的底层类型仍然是 char,宽度为1。这允许将 ASCII 字符串解释为有效的 UTF-8,并允许许多库代码(如 strlen()strncpy())继续工作。)任何 UTF-8编码点的编码长度都可以是1到4个字节,因此,如果您想要字符串中的第五个 UTF-8编码点,它可以从数据的第五个字节到第十七个字节之间的任何位置开始。找到它的唯一方法是从字符串的开头扫描并检查每个代码点的大小。如果你想找到第五个 字母,你还需要检查字符类。如果您想在一个字符串中找到第一百万个 UTF-8字符,那么您需要运行这个循环一百万次!如果您知道需要经常使用索引,那么可以遍历字符串一次并构建它的索引ーー或者可以转换为固定宽度的编码,比如 UCS-4。在一个字符串中找到第100万个 UCS-4字符只需要向数组的地址添加400万个字符即可。

可变长度数据的另一个复杂性是,在分配数据时,要么需要分配尽可能多的内存,要么根据需要动态重新分配。为最坏的情况分配资金可能是极其浪费的。如果需要连续的内存块,重新分配可能会迫使您将所有数据复制到不同的位置,但允许以非连续的内存块存储内存会使程序逻辑复杂化。

因此,可以使用可变长度的 bignum 代替固定宽度的 short intintlong intlong long int,但是分配和使用它们的效率很低。此外,所有主流 CPU 都被设计用于在固定宽度寄存器上进行算术运算,而且没有一个指令直接在某种可变长度的 bignum 上进行操作。这些需要在软件中实现,速度要慢得多。

在现实世界中,大多数(但不是所有)程序员都认为 UTF-8编码的好处,尤其是兼容性,是非常重要的,除了从前到后扫描字符串或复制内存块,我们很少关心其他任何事情,变宽的缺点是可以接受的。我们可以使用类似于 UTF-8的打包的、可变宽度的元素来处理其他事情。但是我们很少这样做,而且它们不在标准库中。

这样做对运行时性能有相当大的好处。如果要对可变大小的类型进行操作,那么在进行操作之前必须对每个数字进行解码(机器码指令通常是固定宽度的) ,然后进行操作,然后在内存中找到一个足够大的空间来保存结果。这些都是非常困难的操作。简单地低效地存储所有数据要容易得多。

并不总是这样做的。想想 Google 的 Protobuf 协议。Protobufs 被设计用来高效地传输数据。在对数据进行操作时,减少传输的字节数抵得上额外指令的成本。因此,Protobufs 使用一种编码方式,将整数编码为1、2、3、4或5个字节,较小的整数占用较少的字节。然而,一旦接收到消息,就会将其解压缩为一种更传统的固定大小的整数格式,这种格式更容易操作。只有在网络传输过程中,他们才会使用这种节省空间的可变长度整数。

那么 myInt在我的编译器中将占用4个字节。但是,实际值 255只能用1字节表示,那么为什么 myInt不能只占用1字节的内存呢?

这就是所谓的 变长编码变长编码,有各种编码定义,例如 VLQ。然而,其中最著名的可能是 UTF-8: UTF-8在一个可变数量的字节(从1到4)上编码代码点。

或者更一般化的问法是: 为什么一个类型只有一个与之相关联的大小,而表示该值所需的空间可能小于这个大小?

和往常一样,在工程学中,一切都是关于权衡的。没有只有优势的解决方案,因此在设计解决方案时必须权衡优势和取舍。

最终确定的设计方案是使用固定大小的基本类型,硬件/语言就从那里飞了下来。

那么,什么是 变量编码的基本弱点呢? 是什么导致了它被拒绝以支持更多的内存饥饿方案呢。

UTF-8字符串中第4个代码点开始的字节的索引是多少?

它取决于前面代码点的值,需要进行线性扫描。

当然有可变长度的编码方案,这是更好的随机寻址?

是的,但是他们也更复杂。如果有一个理想的,我从来没有见过。

随机寻址真的重要吗?

哦,是的!

问题是,任何类型的聚合/数组都依赖于固定大小的类型:

  • 访问 struct的第三个字段? 随机寻址!
  • 访问数组的第三个元素? 随机寻址!

这意味着你必须做出以下权衡:

固定大小类型或线性内存扫描

有几个原因。一个是处理任意大小的数字的额外复杂性,以及由此带来的性能损失,因为编译器不再能够基于每个 int 正好是 X 字节长度的假设进行优化。

第二个问题是,以这种方式存储简单类型意味着它们需要额外的字节来保存长度。所以,在这个新系统中,值小于或等于255实际上需要两个字节,而不是一个,在最坏的情况下,您现在需要5个字节而不是4个。这意味着在内存使用方面的性能优势比您想象的要小,在某些边缘情况下,实际上可能是净损失。

第三个原因是计算机内存通常在 文字中寻址,而不是在字节中寻址。 单词是字节的倍数,通常在32位系统上是4个字节,在64位系统上是8个字节。通常不能读取单个字节,只能读取一个单词并从中提取第 n 个字节。这意味着从一个单词中提取单个字节比仅仅读取整个单词要花费更多的精力,而且如果整个内存被均匀地划分为单词大小(即4字节大小)的块,那么效率会非常高。 因为,如果有任意大小的整数漂浮在周围,那么整数的一部分可能在一个单词中,而另一部分在下一个单词中,因此需要两次读取才能得到完整的整数。

脚注: 更准确地说,在以字节寻址时,大多数系统忽略了“不均匀”字节。即,地址0,1,2和3都读相同的单词,4,5,6和7读下一个单词,以此类推。

另外,这也是为什么32位系统最大内存为4GB 的原因。用于寻址内存中位置的寄存器通常足够容纳一个单词,即4字节,其最大值为(2 ^ 32)-1 = 4294967295。4294967296字节是4GB。

我喜欢 谢尔盖的房子类比,但我认为用汽车来比喻会更好。

把变量类型想象成汽车类型,把人想象成数据。当我们在寻找一辆新车时,我们会选择一辆最符合我们目标的车。我们想要一辆只能容纳一两个人的小型智能车吗?或者一辆载人豪华轿车?两者都有各自的优缺点,比如速度和耗油里程(想想速度和内存使用)。

如果你有一辆豪华轿车,而且你独自驾驶,它不会缩小到只适合你一个人。要做到这一点,你必须卖掉汽车(读: 释放) ,为自己买一个新的小一点的。

继续这个类比,你可以把记忆想象成一个巨大的停车场,停满了汽车,当你去阅读的时候,一个专门为你的汽车类型训练的专业司机会去为你取回它。如果你的车可以根据车里的人来改变车型,那么每次你想开车的时候,你就需要带上一大群司机,因为他们永远不会知道停在那里的是什么样的车。

换句话说,试图确定您在运行时需要读取多少内存将是非常低效的,并且超过您可以在停车场中多放几辆车的事实。

那么为什么 myInt 不只占用1字节的内存呢?

因为你告诉它要用那么多。当使用 unsigned int时,一些标准规定使用4个字节,可用的范围从0到4,294,967,295。如果使用的是 unsigned char,那么您可能只使用所需的1字节(取决于标准,C + + 通常使用这些标准)。

如果没有这些标准,你必须记住这一点: 编译器或 CPU 怎么知道只使用1字节而不是4字节?稍后在您的程序中您可能会添加或乘以该值,这将需要更多的空间。无论何时进行内存分配,操作系统都必须查找、映射并给予空间(可能还要将内存交换到虚拟 RAM) ; 这可能会花费很长时间。如果提前分配内存,则不必等待另一个分配完成。

至于为什么我们使用每字节8位的原因,你可以看看这个: 字节为什么是8位的历史是什么?

顺便说一句,你可以允许整数溢出,但是如果你使用有符号整数,C + + 标准规定整数溢出会导致未定义行为。 整数溢出

大多数答案似乎都忽略了一些简单的东西:

因为它符合 C + + 的设计目标。

能够在编译时计算出类型的大小允许编译器和程序员做出大量的简化假设,这带来了很多好处,特别是在性能方面。当然,固定大小的类型也有相应的缺陷,比如整数溢出。这就是为什么不同的语言会做出不同的设计决策。(例如,Python 整数本质上是可变大小的。)

C + + 如此强烈地倾向于固定大小类型的主要原因可能是它的 C 兼容性目标。然而,由于 C + + 是一种静态类型的语言,它试图生成非常高效的代码,并避免添加程序员没有明确指定的内容,因此固定大小的类型仍然有很大的意义。

那么,为什么 C 首先要选择固定大小的类型呢?很简单。它被设计用于编写70年代的操作系统、服务器软件和实用程序; 这些程序为其他软件提供基础设施(比如内存管理)。在如此低的级别上,性能是至关重要的,编译器完全按照您的要求执行也是如此。

它可以更少,考虑一下函数:

int foo()
{
int bar = 1;
int baz = 42;
return bar+baz;
}

它编译成汇编代码(g + + ,x64,去掉细节)

$43, %eax
ret

在这里,barbaz最终使用零字节来表示。

要改变变量的大小需要重新分配,与浪费几个字节的内存相比,这通常不值得额外的 CPU 周期。

局部变量放在堆栈上,当这些变量的大小没有变化时,堆栈的操作速度非常快。如果您决定要将变量的大小从1字节扩展到2字节,那么您必须将堆栈上的所有内容移动一个字节来为它创建空间。根据需要移动的内容数量,这可能会消耗大量的 CPU 周期。

另一种方法是将每个变量都作为堆位置的指针,但实际上这样做会浪费更多的 CPU 周期和内存。指针是4字节(32位寻址)或8字节(64位寻址) ,所以您已经使用4或8作为指针,然后是堆上数据的实际大小。在这种情况下,重新分配还是有代价的。如果您需要重新分配堆数据,那么您可能很幸运,并且有内联扩展的空间,但是有时您必须将它移动到堆上的其他地方,以获得您想要的大小的连续内存块。

事先决定要使用多少内存总是更快的。如果可以避免动态调整大小,就可以提高性能。浪费内存通常是值得的性能收益。这就是为什么电脑有大量的内存。:)

编译器允许对代码进行大量更改,只要代码仍然可以工作(“原样”规则)。

可以使用8位的文本 move 指令来代替移动一个完整的 int所需的更长的指令(32/64位)。但是,您需要两个指令来完成加载,因为在加载之前必须先将寄存器设置为零。

处理32位的值更有效(至少主编译器是这样认为的)。实际上,我还没有看到一个 x86/x86 _ 64编译器能够在没有内联汇编的情况下执行8位加载。

然而,当涉及到64位时,情况就不同了。在设计之前的处理器扩展(从16位到32位)时,英特尔犯了一个错误。给你是它们看起来像什么的一个很好的代表。这里的主要观点是,当你写到 AL 或 AH 时,另一个不会受到影响(公平地说,这就是重点,在当时是有意义的)。但是当他们把它扩展到32位时就变得有趣了。如果你编写底部位(AL、 AH 或 AX) ,EAX 的上16位不会发生任何变化,这意味着如果你想把 char提升为 int,你需要先清除内存,但是你没有办法实际上只使用这些上16位,使得这个“特性”比任何东西都痛苦。

现在有了64位,AMD 做得更好。如果你触摸低于32位的任何东西,高于32位只是设置为0。这导致了一些实际的优化,您可以在这个 Godbolt中看到。您可以看到,加载8位或32位的内容是以同样的方式完成的,但是当您使用64位变量时,编译器根据文本的实际大小使用不同的指令。

所以你可以看到,编译器可以完全改变你的变量在 CPU 内部的实际大小,如果它会产生相同的结果,但这样做是没有意义的,为较小的类型。