为什么类型总是不管大小而定大小?


149

类型的实际大小之间的实现可能有所不同,但是在大多数情况下,像unsigned int和float这样的类型始终为4个字节。但是,为什么一个类型无论其值如何总是占用一定数量的内存?例如,如果我创建了以下值为255的整数

int myInt = 255;

然后myInt将占用我的编译器4个字节。但是,实际值255只能用1个字节表示,那么为什么myInt不只占用1个字节的内存呢?还是更普遍的询问方式:当表示值所需的空间可能小于该大小时,为什么一个类型仅与该大小相关联?


15
1)“ 但是,实际值256只能用1个字节表示 ”错误,unsinged可以用1个字节表示的最大值是255。2)考虑变量值的最佳存储大小计算和缩小/扩展存储区域的开销。
AlgirdasPreidžius18年6

99
那么,当需要从内存中读取值时,您如何建议机器将确定要读取多少字节?机器如何知道在哪里停止读取值?这将需要额外的设施。通常情况下,这些附加功能的内存和性能开销将比仅使用固定的4个字节作为unsigned int值的情况高得多。
AnT

74
我真的很喜欢这个问题。尽管回答起来似乎很简单,但我认为给出准确的解释需要充分了解计算机和计算机体系结构的实际工作方式。大多数人可能只是将其视为理所当然,而没有对其进行全面的解释。
andreee

37
考虑如果将变量的值加1,使其变为256,将发生什么情况,因此需要扩展。它扩展到哪里?您是否移动其余的内存以腾出空间?变量本身会移动吗?如果是,它将移动到何处以及如何找到需要更新的指针?
molbdnilo

13
@someidiot不,你错了。std::vector<X>始终具有相同的大小,即sizeof(std::vector<X>)一个编译时常量。
SergeyA

Answers:


131

编译器应该为某些机器生成汇编器(最终是机器代码),并且通常C ++会同情该机器。

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

具体而言,请考虑特定的机器体系结构。让我们以当前的Intel x86家族为例。

英特尔®64和IA-32体系结构软件开发人员手册卷1(链接)第3.4.1节说:

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

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

•用于地址计算的操作数

•内存指针

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

寄存器的大小始终相同(此处为32位),因此int变量也始终为32位。我将使用相同的布局(小端),这样就不必每次将变量值加载到寄存器中或将寄存器存储回变量中时都进行转换。

使用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类型相比,我们只节省了两位,这将生成简单得多的代码。


有关动态尺寸的说明:

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

x86平台具有多种本机大小,除了主要的32位外,还包括8位和16位(为了简化起见,我对64位模式进行了修饰)。

这些类型(char,int8_t,uint8_t,int16_t等)直接受体系结构支持-部分是为了与较旧的8086/286/386 / etc向后兼容。等指令集。

当然,选择满足要求的最小自然固定大小类型是一种很好的做法-它们仍然快速,单指令加载和存储,您仍然可以获得全速本机算法,甚至可以通过以下方式提高性能:减少缓存丢失。

这与可变长度编码非常不同-我已经处理了其中的一些,而且它们太可怕了。每个加载都变成一个循环,而不是一条指令。每个商店也是一个循环。每个结构都是可变长度的,因此您不能自然地使用数组。


关于效率的进一步说明

据我所知,在随后的评论中,您一直在使用“有效”一词。有时我们确实选择最小化存储大小-当我们将大量的值保存到文件或通过网络发送它们时,这一点很重要。折衷是我们需要将这些值加载到寄存器中以对其进行任何操作,并且执行转换不是免费的。

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

您为此付出的速度损失意味着只有在您需要绝对最小化带宽或长期存储时才值得,并且在这种情况下,使用简单自然的格式通常更容易-然后使用通用系统对其进行压缩(例如zip,gzip,bzip2,xy或其他)。


tl; dr

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


我正在查看所有不错的答案,同时试图理解所有答案。.因此,关于您的答案,不是动态大小,例如整数小于32位,而不仅仅是允许寄存器中包含更多变量?如果尾数相同,为什么这不是最佳选择?
Nichlas Uden

7
@asd,但是您将在代码中使用多少个寄存器来计算出当前在一个寄存器中存储了多少个变量?
user253751 '18

1
FWIW通常将多个值打包到可用的最小空间中,在此,您认为节省空间比打包和解压缩它们的速度成本更为重要。您通常不能以打包的形式自然地对它们进行操作,因为处理器不知道如何对其内置寄存器以外的任何事物进行正确的算术运算。查找BCD以获取处理器支持的部分异常
无用的

3
如果我确实确实需要全部32位来获取某个值,那么我仍然需要在某个地方存储该长度,因此现在在某些情况下我需要超过 32位。
没用的

1
+1。关于“简单自然的格式然后压缩”的注释通常会更好:这通常是正确的但是:对于某些数据,VLQ-每个值-然后压缩-整体处理要比仅压缩-压缩处理好得多-whole-的事情,并且对于一些应用,数据不能被压缩在一起,因为它要么完全不同的(如在git或的元数据),你实际上是保持它在内存中偶尔需要随机访问或修改一些,但不是最值(如HTML + CSS渲染引擎中的值),因此只能使用就地VLQ之类的方法缩小。
mtraceur

139

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

一个非常简单的类比就是房屋-房屋的大小是固定的,而不管其中有多少人居住。此外,还有一部建筑法规规定了可以居住在一定尺寸房屋中的最大人数。

然而,即使一个人居住在可容纳10人的房屋中,房屋的大小也不会受到当前居住人数的影响。


31
我喜欢这个比喻。如果稍微扩展一下,我们可以想象使用一种不使用固定内存大小作为类型的编程语言,这类似于在不使用房间时拆掉我们房间中的房间,并在必要时对其进行重建(即,当我们可以建造一堆房屋并在需要时将其留作备用时,会有大量的开销)。
ahouse101 '18

5
“因为类型从根本上代表存储”,并非所有语言(例如打字稿)都适用
corvus_192

56
@ corvus_192标记具有含义。这个问题是用C ++而不是'
typescript

4
@ ahouse101确实,有许多语言具有无限精度的整数,它们会根据需要增长。这些语言不需要您为变量分配固定的内存,它们在内部实现为对象引用。示例:Lisp,Python。
Barmar

2
@jamesqf在Lisp中首次采用MP算术可能也不是一件容易的事,它也执行自动内存管理。设计师认为,性能影响仅次于编程的简便性。并开发了优化技术以最小化影响。
Barmar

44

这是一种优化和简化。

您可以具有固定大小的对象。从而存储值。
或者,您可以具有可变大小的对象。但是存储价值和规模。

固定大小的对象

操纵数字的代码无需担心大小。您假设您始终使用4个字节并使代码非常简单。

动态大小的对象

在读取变量时,操纵编号必须理解的代码必须读取值和大小。使用该大小来确保寄存器中的所有高位都为零。

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

摘要

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

注意

压缩使用一个事实,即255将适合一个字节。存在用于存储大型数据集的压缩方案,这些压缩方案将为不同的数字主动使用不同的大小值。但是,由于这不是实时数据,因此您没有上述复杂性。您使用较少的空间来存储数据,但需要压缩/解压缩要存储的数据。


4
这是对我最好的答案:如何跟踪尺寸?有更多的内存?
在线Thomas

@ThomasMoors是的,完全是:具有更多的内存。例如,如果您有一个动态数组,那么有些int将存储该数组中的元素数量。这int本身将再次有一个固定的大小。
Alfe

1
@ThomasMoors通常有两个选项,两个选项都需要额外的内存-您有一个(固定大小)字段来告诉您有多少数据(例如,数组大小为int,或者第一个字符串为“ pascal样式”字符串)元素包含多少个字符),或者您也可以拥有一条链(或更复杂的结构),其中每个元素都以某种方式注明它是否是最后一个-例如零终止字符串或大多数形式的链表。
Peteris

27

因为在像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从另一个单独编译的代码中传入一个参数,该怎么办?


26

Java使用称为“ BigInteger”和“ BigDecimal”的类来做到这一点,显然C ++的GMP C ++类接口也是如此(感谢Digital Trauma)。如果需要,您几乎可以用任何一种语言轻松完成此操作。

CPU一直具有使用BCD(二进制编码的十进制)的能力,该功能旨在支持任何长度的操作(但是您倾向于一次手动操作一个字节,而这在当今的GPU标准下是很慢的。)

我们不使用这些或其他类似解决方案的原因?性能。您的性能最高的语言无力在某些紧缩循环操作的中间扩展变量-这将是不确定的。

在大容量存储和运输情况下,打包值通常是您将使用的唯一值类型。例如,流式传输到您计算机上的音乐/视频数据包可能花费一点时间来指定下一个值是2字节还是4字节,以进行大小优化。

一旦将其放在可以使用的计算机上,内存就便宜了,但是可调整大小的变量的速度和复杂性却不是..这确实是唯一的原因。


4
很高兴看到有人提到BigInteger。并不是说这是一个愚蠢的主意,只是为了大数量地这样做才有意义。
Max Barraclough

1
要学究,实际上是指非常精确的数字:)至少在BigDecimal的情况下……
比尔·K

2
而且由于它被标记为c ++,所以它可能值得一提的是GMP C ++类接口,它与Java的Big *相同。
Digital Trauma

20

因为具有动态大小的简单类型将非常复杂且计算繁重。我不确定这是否有可能。
在每次更改其值后,计算机必须检查该数字占用多少位。将会有很多额外的操作。当您在编译过程中不知道变量的大小时,执行计算将更加困难。

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

为了更好地理解计算机的工作原理以及变量为何具有恒定大小,请学习汇编语言的基础知识。

虽然,我想可以用constexpr值实现类似的事情。但是,这会使代码对于程序员来说难以预测。我想一些编译器优化可能会做类似的事情,但是它们使程序员看不到它,以使事情变得简单。

我在这里仅描述了与程序性能有关的问题。我省略了通过减小​​变量大小来节省内存必须解决的所有问题。老实说,我什至认为这是不可能的。


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


我非常怀疑这样的事情是在编译时完成的。这样节省编译器内存没有什么意义,这是唯一的好处。
Bartek Banachewicz

1
我当时正在考虑将constexpr变量乘以普通变量之类的操作。例如,我们有(理论上)带有值的8字节constexpr变量,56然后将其乘以某个2字节的变量。在某些体系结构上,64位操作会增加计算量,因此编译器可以对其进行优化以仅执行16位乘法。
NO_NAME

SNOBOL系列中的某些APL实现和某些语言(我认为是SPITBOL?也许是Icon)确实做到了这一点(具有粒度):根据实际值动态更改表示格式。APL从布尔值到整数再到浮点数再返回。SPITBOL将从布尔值的列表示形式(8个独立的布尔数组存储在字节数组中)变为整数(IIRC)。
davidbak

16

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

这称为可变长度编码,定义了多种编码,例如VLQ。但是,最著名的方法之一可能是UTF-8:UTF-8在可变数量的字节(从1到4)上编码代码点。

还是更普遍的询问方式:当表示值所需的空间可能小于该大小时,为什么一个类型仅与该大小相关联?

与工程中一样,这都是折衷方案。没有唯一具有优势的解决方案,因此在设计解决方案时必须权衡利弊。

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

那么,变量编码根本弱点是什么,导致它被更多的内存需求大的方案所拒绝呢?没有随机寻址

UTF-8字符串中第四个代码点开始的字节的索引是什么?

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

当然有可变长度编码方案在随机寻址方面更好吗?

是的,但是它们也更复杂。如果有理想的选择,我还从未见过。

随机寻址真的很重要吗?

哦,是的!

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

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

这意味着您基本上需要进行以下权衡:

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


这听起来并不像您说的那么麻烦。您始终可以使用向量表。有内存开销和额外的获取,但是不需要线性扫描。
Artelius

2
@Artelius:当整数具有可变宽度时,如何编码向量表?另外,对使用1到4个字节的整数对内存进行编码时,向量表的内存开销是多少?
Matthieu M.

看,您是对的,在OP给出的特定示例中,使用向量表的优势为零。除了建立向量表,您还可以将数据放入固定大小的元素数组中。但是, OP也要求提供更一般的答案。在Python中,整数数组可变大小整数的向量表!那不是因为它解决了这个问题,而是因为Python在编译时不知道列表元素是整数,浮点数,字典,字符串还是列表,它们当然都有不同的大小。
Artelius

@Artelius:请注意,在Python中,数组包含指向元素的固定大小的指针。这使得O(1)以间接的代价到达一个元素。
Matthieu M.

16

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

如果对象的地址在其生存期内从未改变,则给出其地址的代码可以快速访问该对象。但是,此方法的一个主要限制是,如果为地址X分配了一个地址,然后为地址Y分配了另一个地址(距离N个字节),则X在生命周期内将无法增长到大于N个字节Y的值,除非X或Y被移动。为了使X移动,必须更新宇宙中保存X地址的所有内容以反映新地址,并且同样要使Y移动。虽然可以设计一个系统来促进此类更新(Java和.NET都可以很好地管理它),但使用在整个生命周期中都位于同一位置的对象的效率要高得多,


“除非移动X或Y,否则X在Y的生命周期内将无法增长到大于N个字节。为了移动X,必须更新宇宙中保存X的地址的所有内容以反映新的,同样Y也要移动。” 这是最关键的一点IMO:对象尽可能多的大小,只有用它们的当前值的需求将需要添加的开销用于尺寸/哨兵,内存移动,参考图表等而很明显当一个人的几点思考如何都不能工作……但仍然值得非常清楚地说明,尤其是很少有人这样做。
underscore_d

@underscore_d:像Javascript这样的语言是从头开始设计的,用于处理可变大小的对象,其效率令人惊讶。另一方面,虽然可以使可变大小的对象系统变得简单,并且可以使它们变快,但简单的实现速度很慢,而快速实现则非常复杂。
超级猫

13

简短的答案是:因为C ++标准是这样说的。

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

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

但这不适合单个字节!所以你会怎么做?其余的放在哪里?您已经在X + 1处有东西,因此不能将其放置在那里。别的地方?以后你怎么知道?计算机内存不支持插入语义:您不能只是将某物放置在某个位置并将其放到一边以腾出空间!

旁白:您所说的实际上是数据压缩领域。压缩算法可以将所有内容压缩得更加紧凑,因此至少其中一些算法会考虑为整数使用不超过所需空间的空间。但是,压缩的数据不容易修改(如果可能的话),并且每次进行任何更改都最终会被重新压缩。


11

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

这并非总是如此。考虑一下Google的Protobuf协议。Protobuf旨在非常有效地传输数据。在数据上进行操作时,减少传输的字节数值得增加指令的成本。因此,protobuf使用一种编码方式,该编码方式将整数编码为1、2、3、4或5个字节,较小的整数占用较少的字节。但是,一旦收到该消息,它将被解压缩为更传统的固定大小的整数格式,该格式更易于操作。只有在网络传输期间,他们才使用这种节省空间的可变长度整数。


11

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

可以将变量类型想象为汽车类型,将人们想象为数据。当我们正在寻找一辆新车时,我们会选择最适合我们目的的车。我们要一辆只能容纳一两个人的小型智能汽车吗?还是一辆豪华轿车来载更多人?两者都有其优缺点,例如速度和油耗(想想速度和内存使用情况)。

如果您有豪华轿车,而且自己一个人开车,那么它就不会缩小以适合您。为此,您必须出售汽车(请阅读:解除分配),然后自己购买一辆新的较小的汽车。

继续类推,您可以将记忆想像成一个巨大的停车场,里面装满了汽车,当您阅读时,专门为您的汽车类型培训的专门司机会帮您取回它。如果您的汽车可以根据里面的人改变类型,那么每次您想买车时都需要带一大批司机,因为他们永远都不知道会坐哪辆车。

换句话说,试图确定在运行时需要读取多少内存将大大降低效率,并且超过您可能在停车场中容纳更多汽车的事实。


10

有几个原因。第一个是处理任意大小数字的复杂性,这给性能带来了打击,因为编译器无法再基于每个int长度均为X个字节的假设进行优化。

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

第三个原因是计算机存储器通常可以用而不是字节来寻址。(但请参阅脚注)。字是字节的倍数,在32位系统上通常为4,在64位系统上通常为8。通常,您无法读取单个字节,而是读取一个字并从该字中提取第n个字节。这意味着从单词中提取单个字节比读取整个单词要花费更多的精力,而且如果将整个内存均匀地划分为单词大小(即4字节大小)的块,这将非常有效。因为,如果有任意大小的整数在附近浮动,则最终可能会在一个单词中出现整数部分,而在下一个单词中出现另一部分,因此必须进行两次读取才能获得完整的整数。

脚注:更精确地说,当您以字节为单位寻址时,大多数系统会忽略“不均匀”字节。即,地址0、1、2和3都读取同一个字,4、5、6和7都读取下一个字,依此类推。

顺带一提,这也是32位系统最多具有4 GB内存的原因。用于寻址存储器中位置的寄存器通常足够大以容纳一个字,即4个字节,最大值为(2 ^ 32)-1 =4294967295。4294967296字节为4 GB。


8

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

语言标准以这种方式定义对象大小,以便编译器可以生成有效的代码。例如,如果int在某些实现中碰巧是4个字节长,并且您将其声明a为指向int值的指针或值的数组,则将其a[i]转换为伪代码,“对地址+ 4×i进行引用”。这可以在固定的时间内完成,并且是一种常见且重要的操作,因此许多指令集体系结构(包括x86和最初开发C的DEC PDP机器)都可以在一条机器指令中完成。

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

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

因此,它可能有变长的大数,而不是固定宽度的short intintlong intlong long int,但它会是低效的分配和使用它们。此外,所有主流CPU均设计为在固定宽度寄存器上执行算术运算,而没有一个具有直接在某种可变长度bignum上运行的指令。这些将需要用软件来实现,要慢得多。

在现实世界中,大多数(但不是全部)程序员都认为UTF-8编码的好处(尤其是兼容性)很重要,因此除了从头到尾扫描字符串或复制UTF-8的块外,我们很少关心其他事情。存储器,可变宽度的缺点是可以接受的。我们可以在其他方面使用类似于UTF-8的打包可变宽度元素。但是我们很少这样做,而且它们不在标准库中。


7

当表示值所需的空间可能小于该大小时,为什么一个类型仅与该大小相关联?

主要是因为对齐要求。

按照basic.align / 1

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

想象一下一个有很多楼层并且每层都有很多房间的建筑物。
每个房间都是您的大小(固定空间),可容纳N个人或物体。
借助事先已知的房间大小,它使建筑物的结构组件结构良好

如果房间未对齐,则建筑物骨架将无法很好地结构化。


7

它可以更少。考虑以下功能:

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

编译为汇编代码(g ++,x64,详细信息已删除)

$43, %eax
ret

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


5

那么为什么myInt不仅会占用1个字节的内存?

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

如果不是针对这些标准,则必须牢记这一点:编译器或CPU应该如何知道仅使用1个字节而不是4个字节?稍后,您可以在程序中添加或乘以该值,这将需要更多空间。每当您进行内存分配时,操作系统都必须查找,映射并为您提供该空间(也可能将内存交换到虚拟RAM)。这可能需要很长时间。如果您事先分配了内存,则不必等待其他分配完成。

至于为什么每个字节使用8位的原因,您可以看一下: 为什么字节为8位的历史是什么?

另外,您可以允许整数溢出;但是,如果您使用带符号的整数,则C \ C ++标准指出整数溢出会导致未定义的行为。 整数溢出


5

简单的东西,大多数答案似乎都没有:

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

能够在编译时确定类型的大小,允许编译器和程序员进行大量简化的假设,这带来了很多好处,尤其是在性能方面。当然,固定大小类型会伴随整数陷阱之类的陷阱。这就是为什么不同的语言会做出不同的设计决策的原因。(例如,Python整数本质上是可变大小的。)

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

那么,为什么C首先选择固定大小类型呢?简单。它旨在编写70年代时代的操作系统,服务器软件和实用程序。为其他软件提供基础结构(例如内存管理)的事物。在如此低的水平上,性能至关重要,编译器也正是按照您的指示进行操作。


5

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

局部变量放在堆栈上,当这些变量的大小不变时,堆栈可以非常快速地进行操作。如果您决定要将变量的大小从1个字节扩展到2个字节,则必须将堆栈上的所有内容移动一个字节以为其留出空间。根据需要移动多少东西,这可能会花费大量CPU周期。

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

事先确定要使用多少内存总是更快的。如果可以避免动态调整大小,则可以提高性能。通常,浪费内存值得提高性能。这就是为什么计算机拥有大量内存的原因。:)


3

只要事情仍然可以进行,编译器就可以对您的代码进行很多更改(“原样”规则)。

可以使用8位立即数移动指令,而不是移动full所需的更长指令(32/64位)int。但是,您将需要两条指令来完成加载,因为在执行加载之前必须先将寄存器设置为零。

(至少根据主要编译器而言)将值处理为32位只是效率更高。实际上,我还没有看到x86 / x86_64编译器可以在没有内联汇编的情况下进行8位加载。

但是,对于64位,情况有所不同。在设计其处理器的先前扩展(从16位到32位)时,英特尔犯了一个错误。很好地表示了它们的外观。这里的主要要点是,当您写AL或AH时,另一个不会受到影响(很公平,这就是重点,那时才有意义)。但是当他们将其扩展到32位时,它变得很有趣。如果您写入最低位(AL,AH或AX),则EAX的高16位不会发生任何变化,这意味着如果您想将a提升char为aint,则需要先清除该内存,但无法清除实际上仅使用这16个最高位,这使得“功能”比什么都痛苦。

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

因此,您可以在这里看到,如果编译器可以产生相同的结果,则编译器可以完全更改CPU内部变量的实际大小,但是对于较小的类型,这样做是没有意义的。


更正:视情况而定。另外,我不知道如果可以使用较短的加载/存储,如何释放其他字节供使用-这似乎是OP所想的:不仅避免避免触摸当前值不需要的内存,但是能够知道要读取多少个字节,并在运行时神奇地移动所有RAM,因此满足了一些关于空间效率的怪异哲学思想(不必担心巨大的性能成本!)……只是获得了较低占用空间的指令'不'解决'。CPU / OS需要做的事情是如此复杂,以至于它最清楚地回答了IMO的问题。
underscore_d

1
但是,您不能真正在寄存器中“节省内存”。除非您尝试通过滥用AH和AL来做一些奇怪的事情,否则无论如何在同一个通用寄存器中不能有几个不同的值。局部变量通常保留在寄存器中,如果不需要,则永远不会进入RAM。
meneldal
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.