实现可能在类型的实际大小之间有所不同,但在大多数情况下,unsigned int和float等类型总是4个字节。但是为什么类型总是占用某些的内存量而不管它的价值?例如,如果我创建了以下整数,其值为255
int myInt = 255;
然后myInt
将占用我的编译器的4个字节。但是,实际值255
只能用1个字节表示,那么为什么myInt
不仅占用1个字节的内存?或者更通用的询问方式:当表示值所需的空间可能小于该值时,为什么类型只有一个与之关联的大小?
答案 0 :(得分:139)
因为类型从根本上代表存储,并且它们是根据最大值定义的,而不是当前值。
非常简单的类比就是房子 - 房子有固定的大小,不管有多少人住在房子里,还有一个建筑规范,规定了住在一个房子里的最多人数。一定的规模。
然而,即使一个人居住在可容纳10人的房屋内,房屋的大小也不会受到当前居住人数的影响。
答案 1 :(得分:44)
这是一种优化和简化。
您可以拥有固定大小的对象。从而存储价值 或者你可以有可变大小的objets。但存储价值和大小。
操纵数字的代码不需要担心大小。您假设您总是使用4个字节并使代码非常简单。
操作数字的代码在读取必须读取值和大小的变量时必须理解。使用该大小确保寄存器中的所有高位都为零。
如果值未超过当前大小,则将值重新放入内存中,然后将值重新放回内存中。但是如果值缩小或增长,则需要将对象的存储位置移动到内存中的另一个位置,以确保它不会溢出。现在你必须跟踪那个数字的位置(如果它的大小变得太大,它就可以移动)。您还需要跟踪所有未使用的变量位置,以便可以重复使用它们。
为固定大小的对象生成的代码要简单得多。
压缩使用255将适合一个字节的事实。存在用于存储大数据集的压缩方案,其将主动针对不同数量使用不同大小的值。但由于这不是实时数据,因此您不具备上述复杂性。您可以使用较少的空间来存储数据,但代价是压缩/解压缩数据以进行存储。
答案 2 :(得分: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 ++等固定宽度类型的语言反映了这一点。
它没有 这样,更少关注映射到最高效代码的抽象语言可以自由使用可变宽度编码(也称为&#) 34;数字类型的可变长度数量"或VLQ。
进一步阅读:如果您搜索"可变长度数量"您可以找到一些示例,其中 的编码实际上是有效的,值得额外的逻辑。通常情况下,当您需要存储大量可能在大范围内的任何值时,但大多数值往往会出现一些较小的子范围。
请注意,如果编译器可以证明它可以在不破坏任何代码的情况下将值存储在较小的空间中(例如,它只是在内部可见的变量)。单个翻译单元),和其优化启发式建议它在目标硬件上更有效率,它完全允许相应地优化它将它存储在较小的空间中,只要其余的代码可以正常运行,并且好像"它做了标准的事情。
但是,当代码必须与可能单独编译的其他代码进行互操作时,大小必须保持一致,或确保每一段代码都遵循同样的惯例。
因为如果它不一致,就会出现这种复杂情况:如果我有int x = 255;
但稍后在代码x = y
中怎么办?如果int
可以是可变宽度,则编译器必须提前知道预先分配它所需的最大空间量。这并不总是可行的,因为如果y
是从另一段代码中分别编译的参数,那该怎么办?
答案 3 :(得分:26)
Java使用名为“BigInteger”和“BigDecimal”的类来完成这一点,C ++的GMP C ++类接口显然也是如此(感谢Digital Trauma)。如果你愿意,你可以用几乎任何语言轻松完成。
CPU总是能够使用BCD(二进制编码的十进制)来设计支持任何长度的操作(但是你倾向于一次手动操作一个字节,这将是今天GPU的慢速操作标准)。
我们不使用这些或其他类似解决方案的原因?性能。你性能最高的语言无法在一些紧密循环操作中扩展变量 - 这将是非常不确定的。
在大容量存储和传输情况下,打包值通常是您将使用的唯一值类型。例如,流式传输到您的计算机的音乐/视频数据包可能会花费一些时间来指定下一个值是2个字节还是4个字节作为大小优化。
一旦它在您的计算机上可以使用它,虽然内存很便宜,但可调整大小的变量的速度和复杂性不是......这是唯一的原因。
答案 4 :(得分:20)
因为具有动态大小的简单类型会非常复杂并且计算量很大。我不确定这是否可行。
计算机必须检查每次更改其值后所需的位数。这将是非常多的额外操作。
如果在编译期间不知道变量的大小,那么执行计算会更加困难。
为了支持变量的动态大小,计算机实际上必须记住变量现在有多少字节......需要额外的内存来存储该信息。并且必须在变量的每个操作之前分析此信息以选择正确的处理器指令。
为了更好地理解计算机的工作原理以及变量大小不变的原因,请学习汇编语言的基础知识。
虽然,我认为有可能用constexpr值实现类似的东西。但是,这会使代码对程序员的可预测性降低。我想一些编译器优化可能会做类似的事情,但是它们会把它从程序员那里隐藏起来以保持简单。
我在这里只描述了与程序性能有关的问题。我省略了所有必须通过减少变量大小来节省内存的问题。老实说,我认为它甚至不可能。
总之,使用比声明的更小的变量只有在编译期间知道它们的值时才有意义。现代编译器很可能会这样做。在其他情况下,它会导致太多困难甚至无法解决的问题。
答案 5 :(得分:16)
计算机内存被细分为一定大小的连续寻址块(通常为8位,称为字节),大多数计算机设计用于有效访问具有连续地址的字节序列。
如果对象的地址永远不会在对象的生命周期内发生变化,那么给定其地址的代码可以快速访问相关对象。然而,这种方法的一个基本限制是,如果为地址X分配了一个地址,然后为地址Y分配了另一个地址,该地址是N字节,那么X将不能在生命周期内增长大于N个字节除非移动X或Y,否则为Y.为了让X移动,宇宙中包含X的地址的所有内容都必须更新以反映新的地址,同样要移动Y.虽然可以设计一个系统来促进这样的更新(Java和.NET都能很好地管理它),但是使用在整个生命周期内保持在同一位置的对象会更有效率,这反过来通常要求它们的大小必须保持不变。
答案 6 :(得分:16)
然后
myInt
将占用我的编译器的4个字节。但是,实际值255
只能用1个字节表示,那么为什么myInt
不只占用1个字节的内存?
这称为可变长度编码,定义了各种编码,例如VLQ。然而,其中最着名的可能是 UTF-8 :UTF-8对可变字节数的代码点进行编码,从1到4。
或者更通用的询问方式:当表示值所需的空间可能小于该值时,为什么类型只有一个与之关联的大小?
与工程一样,所有这些都是权衡利弊。没有解决方案只有优势,因此在设计解决方案时必须平衡优势和权衡。
解决的设计是使用固定大小的基本类型,硬件/语言就是从那里飞过来的。
那么,变量编码的基本弱点是什么呢?这导致它被拒绝而更多的内存饥饿方案? 无随机寻址。
第四个代码点以UTF-8字符串开头的字节索引是什么?
这取决于前面代码点的值,需要进行线性扫描。
当然有可变长度编码方案,它们在随机寻址方面更好吗?
是的,但它们也更复杂。如果有一个理想的,我还没见过。
无论如何,随机寻址真的很重要吗?
哦,是的!
问题是,任何类型的聚合/数组都依赖于固定大小的类型:
struct
的第3个字段?随机寻址!这意味着您基本上有以下权衡:
固定大小类型或线性内存扫描
答案 7 :(得分:13)
简短的回答是:因为C ++标准是这样说的。
答案很长:您在计算机上可以做的事情最终受到硬件的限制。当然,可以将整数编码为可变数量的字节用于存储,但是读取它可能需要特殊的CPU指令才能执行,或者您可以在软件中实现它,但是它会非常慢。 CPU中可以使用固定大小的操作来加载预定义宽度的值,对于可变宽度,没有。
要考虑的另一点是计算机内存的工作原理。假设您的整数类型可能占用1到4个字节的存储空间。假设您将值42存储到整数中:它占用1个字节,然后将其放在内存地址X处。然后将下一个变量存储在位置X + 1(此时我不考虑对齐)等等。稍后您决定将值更改为6424。
但这不适合单个字节!所以你会怎么做?你把剩下的放在哪里?你已经有了X + 1的东西,所以不能把它放在那里。别的地方?你怎么知道以后在哪里?计算机内存不支持插入语义:你不能只是在某个位置放置一些东西并将其后的所有内容推到一边以腾出空间!
除此之外:您所谈论的实际上是数据压缩领域。存在压缩算法以将所有内容打包得更紧密,因此至少其中一些算法将考虑不为您的整数使用更多空间。但是,压缩数据不容易修改(如果可能的话),并且每次对其进行任何更改时最终都会被重新压缩。
答案 8 :(得分:11)
这样做有很大的运行时性能优势。如果您要操作可变大小类型,则必须在执行操作之前解码每个数字(机器代码指令通常是固定宽度),执行操作,然后在内存中找到足够大的空间来保存结果。那些操作非常困难。简单地存储所有数据的效率要低得多。
这并不总是如何完成的。考虑谷歌的Protobuf协议。 Protobufs旨在非常有效地传输数据。在对数据进行操作时,减少传输的字节数值得额外指令的成本。因此,protobufs使用编码,该编码对1,2,3,4或5字节中的整数进行编码,较小的整数使用较少的字节。然而,一旦收到消息,它就被解压缩成更传统的固定大小整数格式,这种格式更容易操作。只有在网络传输过程中,他们才会使用这样一个节省空间的可变长度整数。
答案 9 :(得分:11)
我喜欢Sergey's house analogy,但我认为汽车类比会更好。
将变量类型设想为汽车类型和人员数据。当我们正在寻找新车时,我们会选择最适合我们目的的车。我们想要一款只适合一两个人的小型智能车吗?还是一辆豪华轿车可以载更多人?两者都有它们的优点和缺点,如速度和汽油里程(想想速度和内存使用)。
如果你有一辆豪华轿车并且你独自开车,它不会缩小到只适合你。要做到这一点,你必须出售汽车(阅读:deallocate)并为自己购买一个新的小汽车。
继续比喻,你可以把记忆想象成一个充满汽车的大型停车场,当你去阅读时,专门为你的汽车类型训练的专业司机会为你取得它。如果您的车可以根据车内人员的不同而改变车型,那么每次您想要开车时都需要带上一大批司机,因为他们永远不会知道现场会坐哪种车。
换句话说,尝试确定在运行时需要读取多少内存将是非常低效的,并且超过了您可能在停车场中安装更多汽车的事实。
答案 10 :(得分:10)
有几个原因。一个是处理任意大小的数字增加的复杂性以及这给出的性能影响,因为编译器不能再基于每个int正好是X字节长的假设进行优化。
第二种方法是以这种方式存储简单类型意味着它们需要一个额外的字节来保持长度。因此,在这个新系统中,255或更小的值实际上需要两个字节,而不是一个,在最坏的情况下,您现在需要5个字节而不是4个。这意味着在使用的内存方面的性能提升小于您可能的在某些边缘情况下,我认为可能实际上是净亏损。
第三个原因是计算机内存通常可以words寻址,而不是字节。 (但见脚注)。 字是字节的倍数,通常在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。
答案 11 :(得分:8)
在某些意义上,有些对象在C ++标准库中具有可变大小,例如std::vector
。但是,这些都会动态分配他们需要的额外内存。如果你取sizeof(std::vector<int>)
,你将得到一个与对象管理的内存无关的常量,如果你分配一个包含std::vector<int>
的数组或结构,它将保留这个基本大小而不是将额外的存储放在相同的数组或结构中。有一些C语法支持这样的东西,特别是可变长度数组和结构,但C ++没有选择支持它们。
语言标准以这种方式定义对象大小,以便编译器可以生成有效的代码。例如,如果int
在某些实现上恰好是4个字节长,并且您将a
声明为int
值的指针或数组,那么a[i]
会转换为伪代码,“取消引用地址a
+ 4×i
。”这可以在恒定时间内完成,并且是许多指令集架构(包括x86和DEC PDP)的常见且重要的操作最初开发C的机器可以在一台机器指令中完成。
作为可变长度单位连续存储的数据的一个常见的现实世界示例是编码为UTF-8的字符串。 (但是,编译器的UTF-8字符串的基础类型仍然是char
并且宽度为1.这允许将ASCII字符串解释为有效的UTF-8,以及许多库代码,例如{{ 1}}和strlen()
继续工作。)任何UTF-8代码点的编码长度可以是一到四个字节,因此,如果你想在字符串中使用第五个UTF-8代码点,它可以开始从数据的第五个字节到第二十个字节的任何地方。找到它的唯一方法是从字符串的开头扫描并检查每个代码点的大小。如果要查找第五个字形,还需要检查字符类。如果你想在一个字符串中找到第一百万个UTF-8字符,你需要运行这个循环一百万次!如果您知道需要经常使用索引,则可以遍历字符串一次并构建索引 - 或者您可以转换为固定宽度编码,例如UCS-4。在字符串中查找百万分之一的UCS-4字符只需要在数组地址中添加四百万字符。
可变长度数据的另一个复杂因素是,当您分配它时,您需要分配尽可能多的内存,或者根据需要动态重新分配。为最坏情况分配可能非常浪费。如果需要连续的内存块,重新分配可能会强制您将所有数据复制到其他位置,但允许将内存存储在非连续的块中会使程序逻辑复杂化。
因此,可以使用可变长度的bignums而不是固定宽度strncpy()
,short int
,int
和long int
,但分配和使用它们。此外,所有主流CPU都设计为在固定宽度寄存器上进行算术运算,并且没有一个具有直接在某种可变长度bignum上运行的指令。这些需要用软件实现,速度要慢得多。
在现实世界中,大多数(但不是全部)程序员都认为UTF-8编码的好处,特别是兼容性很重要,除了从前到后扫描字符串之外,我们很少关心任何事情。复制可变宽度的缺点是可接受的内存块。我们可以使用类似于UTF-8的压缩,可变宽度元素来做其他事情。但我们很少这样做,而且他们不在标准库中。
答案 12 :(得分:7)
为什么类型在空格时只有一个与之关联的大小 表示该值所需的值可能小于该值?
主要是因为对齐要求。
对象类型具有对齐限制的对齐要求 可以分配该类型对象的地址。
想想一栋有很多楼层的建筑,每层楼都有很多房间 每个房间都是尺寸(固定空间),能够容纳N个人或物品 事先知道房间大小,它使建筑物的结构部分结构良好。
如果房间没有对齐,那么建筑骨架的结构将不合理。
答案 13 :(得分:7)
它可以更少。考虑一下这个功能:
int foo()
{
int bar = 1;
int baz = 42;
return bar+baz;
}
它编译为汇编代码(g ++,x64,细节剥离)
$43, %eax
ret
此处,bar
和baz
最终使用零字节来表示。
答案 14 :(得分:5)
那为什么myInt不会只占用1个字节的内存?
因为你告诉它使用那么多。使用unsigned int
时,某些标准规定将使用4个字节,并且它的可用范围将为0到4,294,967,295。如果您使用的是unsigned char
,那么您可能只会使用您正在寻找的1个字节(取决于标准和C ++通常使用这些标准)。
如果不符合这些标准,您必须牢记这一点:编译器或CPU应该知道如何仅使用1个字节而不是4个字节?稍后在您的程序中,您可能会添加或乘以该值,这将需要更多空间。无论何时进行内存分配,操作系统都必须查找,映射并为您提供该空间(可能还会将内存交换到虚拟RAM);这可能需要很长时间。如果您事先分配了内存,则不必等待另一个分配完成。
至于我们每字节使用8位的原因,你可以看看这个: What is the history of why bytes are eight bits?
在旁注中,您可以允许整数溢出;但是如果使用有符号整数,C \ C ++标准规定整数溢出会导致未定义的行为。 Integer overflow
答案 15 :(得分:5)
大多数答案似乎都错过了一些简单的事情:
能够在编译时计算出类型的大小允许编译器和程序员做出大量简化的假设,这带来了许多好处,特别是在性能方面。当然,固定大小的类型会伴随整数溢出等陷阱。这就是不同语言做出不同设计决策的原因。 (例如,Python整数本质上是可变大小的。)
可能C ++对固定大小类型如此强烈倾向的主要原因是它的C兼容性目标。但是,由于C ++是一种静态类型的语言,它试图生成非常高效的代码,并且避免添加程序员未明确指定的内容,因此固定大小的类型仍然很有意义。
那么为什么C首先选择固定尺寸类型呢?简单。它旨在编写70年代的操作系统,服务器软件和实用程序;为其他软件提供基础设施(如内存管理)的东西。在如此低的水平上,性能至关重要,编译器也正是按照你的意思做的。
答案 16 :(得分:5)
要更改变量的大小需要重新分配,与浪费更多字节的内存相比,这通常不值得额外的CPU周期。
局部变量进入堆栈,当这些变量的大小没有变化时,操作速度非常快。如果您决定将变量的大小从1个字节扩展到2个字节,则必须将堆栈上的所有内容移动一个字节以为其创建该空间。这可能会花费很多CPU周期,具体取决于需要移动的内容。
你能做到的另一种方法是让每个变量都成为一个指向堆位置的指针,但实际上你会以这种方式浪费更多的CPU周期和内存。指针是4个字节(32位寻址)或8个字节(64位寻址),因此您已经使用4或8作为指针,然后是堆上数据的实际大小。在这种情况下,重新分配仍然需要付出代价。如果你需要重新分配堆数据,你可能会很幸运,并有空间将其扩展为内联,但有时你必须将它移动到堆上的其他位置,以获得所需大小的连续内存块。
预先确定要使用多少内存总是更快。如果您可以避免动态调整大小,则可以获得性能。浪费内存通常值得获得性能提升。这就是计算机拥有大量内存的原因。 :)
答案 17 :(得分:3)
只要事情仍然有效(&#34; as-is&#34;规则),编译器就可以对代码进行大量更改。
可以使用8位文字移动指令代替移动完整int
所需的较长(32/64位)。但是,您需要两条指令才能完成加载,因为在进行加载之前必须先将寄存器设置为零。
将这个值处理为32位更有效(至少根据主编译器)。实际上,我还没有看到x86 / x86_64编译器在没有内联汇编的情况下进行8位加载。
然而,就64位而言,情况有所不同。在设计其处理器的先前扩展(从16位到32位)时,英特尔犯了一个错误。 Here很好地代表了它们的样子。这里的主要内容是,当你写到AL或AH时,另一个不受影响(公平,这就是重点,那时它才有意义)。但是当它们扩展到32位时会很有趣。如果你写下底部位(AL,AH或AX),EAX的高16位没有任何反应,这意味着如果你想将char
推广到int
,你需要清除那个记忆首先,但你没有办法实际只使用这些前16位,使这个&#34;功能&#34;更多的是痛苦。
现在64位,AMD的表现要好得多。如果触摸低32位中的任何内容,则高32位只会设置为0.这会导致您可以在此godbolt中看到一些实际的优化。您可以看到以相同的方式加载8位或32位的内容,但是当您使用64位变量时,编译器会根据文字的实际大小使用不同的指令。
所以你可以在这里看到,编译器可以完全改变你的变量的实际大小,如果它会产生相同的结果,但对于较小的类型这样做是没有意义的。