类型的实际大小之间的实现可能不同,但在大多数情况下,unsigned int和float等类型总是4个字节.但是为什么类型总是占用一定量的内存而不管它的价值呢?例如,如果我创建了下面的值为255的整数
int myInt = 255;
Run Code Online (Sandbox Code Playgroud)
然后myInt用我的编译器占用4个字节.但是,实际值255只能用1个字节表示,那么为什么myInt不占用1个字节的内存呢?或者更通用的询问方式:当表示值所需的空间可能小于该大小时,为什么类型只有一个与之关联的大小?
Ser*_*eyA 139
因为类型从根本上代表存储,并且它们是根据它们可以容纳的最大值来定义的,而不是当前值.
非常简单的比喻是房子 - 房子有固定的大小,无论有多少人住在房子里,还有一个建筑规范,规定了可以住在一定规模的房子里的最大人数.
然而,即使一个人住在可容纳10人的房子里,房屋的大小也不会受到当前居住人数的影响.
Use*_*ess 131
编译器应该为某些机器生成汇编程序(最终是机器代码),并且通常C++会试图同情该机器.
同情底层机器意味着:使编写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位.我将使用相同的布局(little-endian),这样每次将变量值加载到寄存器中时都不需要进行转换,或者将寄存器存储回变量.
使用godbolt,我们可以准确地看到编译器为一些简单的代码做了什么:
int square(int num) {
return num * num;
}
Run Code Online (Sandbox Code Playgroud)
编译(使用GCC 8.1并-fomit-frame-pointer -O3简化):
square(int):
imul edi, edi
mov eax, edi
ret
Run Code Online (Sandbox Code Playgroud)
这意味着:
int num参数在寄存器EDI中传递,这意味着它正是英特尔对本机寄存器所期望的大小和布局.该功能不必转换任何东西imul),速度非常快编辑:我们可以使用非原生布局制作添加相关比较来显示差异.最简单的情况是将值存储在除原生宽度之外的其他内容中.
再次使用godbolt,我们可以比较一个简单的原生乘法
unsigned mult (unsigned x, unsigned y)
{
return x*y;
}
mult(unsigned int, unsigned int):
mov eax, edi
imul eax, esi
ret
Run Code Online (Sandbox Code Playgroud)
使用非标准宽度的等效代码
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
Run Code Online (Sandbox Code Playgroud)
所有额外的指令都涉及将输入格式(两个31位无符号整数)转换为处理器本身可以处理的格式.如果我们想将结果存储回31位值,则会有另外一条或两条指令来执行此操作.
这种额外的复杂性意味着当节省空间非常重要时,您只会对此感到烦恼.在这种情况下,与使用native unsigned或uint32_ttype 相比,我们只保存了两位,这将产生更简单的代码.
上面的示例仍然是固定宽度值而不是可变宽度,但宽度(和对齐)不再与本机寄存器匹配.
x86平台有几种本机大小,包括8位和16位以及主32位(我是64位模式的光泽和其他各种简单的方法).
这些类型(char,int8_t,uint8_t,int16_t等)也由架构直接支持 - 部分用于向后兼容旧版8086/286/386 等.等指令集.
当然,选择最小的自然固定尺寸类型就足够了,可以是很好的做法 - 它们仍然很快,单个指令加载和存储,你仍然可以获得全速原生算术,你甚至可以通过减少缓存未命中.
这与可变长度编码非常不同 - 我已经使用了其中的一些,而且它们非常糟糕.每个加载都变成循环而不是单个指令.每个商店也是一个循环.每个结构都是可变长度的,因此您无法自然地使用数组.
在随后的评论中,就存储大小而言,你一直在使用"高效"这个词.我们有时会选择最小化存储大小 - 当我们将大量值保存到文件或通过网络发送时,这很重要.权衡是我们需要将这些值加载到寄存器中以对它们执行任何操作,并且执行转换不是免费的.
当我们讨论效率时,我们需要知道我们正在优化什么,以及权衡取舍.使用非本机存储类型是交换空间处理速度的一种方式,有时是有意义的.使用可变长度存储(至少对于算术类型),可以交换更多的处理速度(以及代码复杂性和开发人员时间),从而最大程度地节省空间.
您为此付出的速度代价意味着只有在您需要绝对最小化带宽或长期存储时才有价值,对于这些情况,通常更容易使用简单自然的格式 - 然后只需使用通用系统对其进行压缩(比如zip,gzip,bzip2,xy或者其他).
每个平台都有一个架构,但您可以提出基本上无限数量的不同方式来表示数据.任何语言提供无限数量的内置数据类型都是不合理的.因此,C++提供了对平台本机,自然数据类型集的隐式访问,并允许您自己编写任何其他(非本机)表示.
Mar*_*ork 44
这是一种优化和简化.
您可以拥有固定大小的对象.从而存储价值.
或者你可以有可变大小的objets.但存储价值和大小.
操纵数字的代码不需要担心大小.您假设您总是使用4个字节并使代码非常简单.
操作数字的代码在读取必须读取值和大小的变量时必须理解.使用该大小确保寄存器中的所有高位都为零.
如果值未超过当前大小,则将值重新放入内存中,然后将值重新放回内存中.但是如果值缩小或增长,则需要将对象的存储位置移动到内存中的另一个位置,以确保它不会溢出.现在你必须跟踪那个数字的位置(如果它的大小变得太大,它就可以移动).您还需要跟踪所有未使用的变量位置,以便可以重复使用它们.
为固定大小的对象生成的代码要简单得多.
压缩使用255将适合一个字节的事实.存在用于存储大数据集的压缩方案,其将主动针对不同数量使用不同大小的值.但由于这不是实时数据,因此您没有上述复杂性.使用较少的空间来存储数据,但代价是压缩/解压缩数据以进行存储.
mtr*_*eur 27
因为在像C++这样的语言中,设计目标是将简单的操作编译成简单的机器指令.
所有主流CPU指令集都使用固定宽度类型,如果要进行可变宽度类型,则必须执行多个机器指令来处理它们.
至于为什么底层计算机硬件是这样的:这是因为它更简单,并且对许多情况(但不是全部)更有效.
想象一下计算机是一块磁带:
| xx | xx | xx | xx | xx | xx | xx | xx | xx | xx | xx | xx | xx | ...
Run Code Online (Sandbox Code Playgroud)
如果你只是告诉计算机查看磁带上的第一个字节xx,它是如何知道类型是否在那里停止,或继续到下一个字节?如果你有一个像255(十六进制FF)或数字65535(十六进制FFFF)这样的数字,那么第一个字节总是如此FF.
那你怎么知道的?您必须添加其他逻辑,并"重载"至少一个位或字节值的含义,以指示该值继续到下一个字节.这种逻辑永远不会"自由",要么你在软件中模拟它,要么在CPU中添加一堆额外的晶体管来实现它.
C和C++等固定宽度类型的语言反映了这一点.
它不具有成为这种方式,和更抽象的语言,其是与映射较少关注最大限度地有效的代码可以自由地使用可变宽度编码(也被称为"可变长度数量"或VLQ),用于数字类型.
延伸阅读:如果你搜索"变长量",你可以找到的地方那种编码的一些例子是真正有效的,值得额外的逻辑.通常情况下,当您需要存储大量可能在大范围内的任何值时,但大多数值倾向于某个小的子范围.
请注意,如果编译器可以证明它可以在不破坏任何代码的情况下将值存储在较小的空间中(例如,它是仅在单个转换单元内部可见的变量),并且其优化启发式表明它'在目标硬件上效率更高,完全允许相应地对其进行优化并将其存储在较小的空间中,只要其余的代码"好像"就像标准的那样.
但是,当代码必须与可能单独编译的其他代码互操作时,大小必须保持一致,或者确保每段代码遵循相同的约定.
因为如果它不一致,就会出现这种复杂情况:如果我有,int x = 255;但后来在我的代码中x = y呢?如果int可以是可变宽度,则编译器必须提前知道预分配它所需的最大空间量.这并不总是可能的,因为如果y是从另一段代码中分别编译的参数传入的话?
Bil*_*l K 26
Java使用名为"BigInteger"和"BigDecimal"的类来完成这一点,C++的GMP C++类接口显然也是如此(感谢Digital Trauma).如果您愿意,您可以使用几乎任何语言轻松完成.
的CPU一直有使用的能力BCD(二进制编码的十进制),它被设计为支持任意长度的操作(但你往往在时间,这将是今天的GPU标准SLOW一个字节的手动操作.)
我们之所以不使用这些或其他类似的解决方案?性能.你性能最高的语言无法在一些紧密循环操作中扩展变量 - 这将是非常不确定的.
在大容量存储和传输情况下,打包值通常是您将使用的唯一值类型.例如,流式传输到您的计算机的音乐/视频数据包可能会花费一些时间来指定下一个值是2个字节还是4个字节作为大小优化.
一旦它在您的计算机上可以使用它,虽然内存很便宜,但可调整大小的变量的速度和复杂性不是..这才是真正的唯一原因.
NO_*_*AME 20
因为具有动态尺寸的简单类型会非常复杂并且计算量很大.我不确定这是否可能.
计算机必须检查每次更改其值后所需的位数.这将是非常多的额外操作.如果在编译期间不知道变量的大小,那么执行计算会更加困难.
为了支持变量的动态大小,计算机实际上必须记住变量现在有多少字节,哪些......需要额外的内存来存储该信息.并且必须在变量的每个操作之前分析该信息以选择正确的处理器指令.
为了更好地理解计算机的工作原理以及变量具有恒定大小的原因,请学习汇编语言的基础知识.
虽然,我想有可能用constexpr值实现类似的东西.但是,这会使代码对程序员的可预测性降低.我想一些编译器优化可能会做类似的事情,但是它们会将它隐藏在程序员之外以保持简单.
我在这里只描述了与程序性能有关的问题.我省略了所有必须通过减少变量大小来节省内存的问题.老实说,我认为它甚至不可能.
总之,使用比声明的更小的变量只有在编译期间知道它们的值时才有意义.现代编译器很可能会这样做.在其他情况下,它会导致太多困难甚至无法解决的问题.
sup*_*cat 16
计算机存储器被细分为具有特定大小的连续寻址的块(通常为8位,并且称为字节),并且大多数计算机被设计为有效地访问具有连续地址的字节序列.
如果对象的地址永远不会在对象的生命周期内发生变化,那么给定其地址的代码可以快速访问相关对象.然而,这种方法的一个基本限制是,如果为地址X分配了一个地址,然后为地址Y分配了另一个地址,该地址是N字节,那么X将不能在生命周期内增长大于N个字节除非移动X或Y,否则为Y. 为了让X移动,宇宙中包含X的地址的所有内容都必须更新以反映新的地址,同样要移动Y. 虽然可以设计一个系统来促进这样的更新(Java和.NET都能很好地管理它),但是使用在整个生命周期内保持在同一位置的对象会更有效率,这反过来通常要求它们的大小必须保持不变.
Mat*_* M. 16
然后
myInt用我的编译器占用4个字节.但是,实际值255只能用1个字节表示,那么为什么myInt不占用1个字节的内存呢?
这被称为可变长度编码,定义了各种编码,例如VLQ.然而,其中最着名的可能是UTF-8:UTF-8对可变字节数的代码点进行编码,从1到4.
或者更通用的询问方式:当表示值所需的空间可能小于该大小时,为什么类型只有一个与之关联的大小?
与工程一样,这都是权衡利弊.没有解决方案只有优势,所以在设计解决方案时必须平衡优势和权衡.
解决的设计是使用固定大小的基本类型,硬件/语言就是从那里飞过来的.
那么,变量编码的根本弱点是什么,导致它被拒绝而有利于更多的内存饥饿方案呢?没有随机寻址.
第四个代码点以UTF-8字符串开头的字节索引是多少?
它取决于先前代码点的值,需要线性扫描.
当然有可变长度编码方案,它们在随机寻址方面更好吗?
是的,但它们也更复杂.如果有一个理想的,我还没见过它.
无论如何,随机寻址真的很重要吗?
哦,是的!
问题是,任何类型的聚合/数组都依赖于固定大小的类型:
struct?随机寻址!这意味着您基本上有以下权衡:
固定大小类型或线性内存扫描
Joh*_*ous 13
简短的回答是:因为C++标准是这样说的.
答案很长:您在计算机上可以做的事情最终受到硬件的限制.这是当然,可能的整数编码到存储字节数量可变的,但后来读它要么需要特殊的CPU指令是高性能的,或者你可以实现它的软件,但那么这将是非常缓慢的.CPU中可以使用固定大小的操作来加载预定义宽度的值,对于可变宽度,没有.
需要考虑的另一点是计算机内存的工作原理.假设您的整数类型可能占用1到4个字节的存储空间.假设您将值42存储到整数中:它占用1个字节,然后将其放在内存地址X处.然后将下一个变量存储在位置X + 1(此时我不考虑对齐)等等.之后您决定将值更改为6424.
但这不适合单个字节!所以你会怎么做?你把剩下的放在哪里?你已经有了X + 1的东西,所以不能把它放在那里.别的地方?你怎么知道以后在哪里?计算机内存不支持插入语义:你不能只是在某个位置放置一些东西并将其后的所有内容推到一边以腾出空间!
旁白:你所说的实际上是数据压缩领域.存在压缩算法以将所有内容打包得更紧密,因此至少其中一些算法将考虑不为您的整数使用更多空间.但是,压缩数据不容易修改(如果可能的话),并且每次对其进行任何更改时最终都会被重新压缩.
Cor*_*ica 11
这样做有很大的运行时性能优势.如果您要操作可变大小类型,则必须在执行操作之前解码每个数字(机器代码指令通常是固定宽度),执行操作,然后在内存中找到足够大的空间来保存结果.那些操作非常困难.简单地存储所有数据要容易得多.
这并不总是如何完成.考虑谷歌的Protobuf协议.Protobufs旨在非常有效地传输数据.在对数据进行操作时,减少传输的字节数值得额外指令的成本.因此,protobufs使用编码,该编码对1,2,3,4或5字节中的整数进行编码,较小的整数使用较少的字节.然而,一旦收到消息,它就被解压缩成更传统的固定大小整数格式,这种格式更容易操作.只有在网络传输过程中,他们才会使用这样一个节省空间的可变长度整数.
sco*_*001 11
我喜欢谢尔盖的房子比喻,但我认为汽车比喻会更好.
想象一下变量类型作为汽车类型和人类作为数据.当我们正在寻找新车时,我们会选择最适合我们目的的车.我们想要一款只适合一两个人的小型智能车吗?还是一辆豪华轿车可以载更多人?两者都有它们的优点和缺点,如速度和汽油里程(想想速度和内存使用).
如果你有一辆豪华轿车并且你一个人开车,它就不会缩小到只适合你.要做到这一点,你必须出售汽车(阅读:deallocate)并为自己购买一个新的小汽车.
继续比喻,您可以将记忆视为一个充满汽车的巨大停车场,当您去阅读时,专门为您的汽车类型培训的专业司机会为您取得.如果您的车可以根据车内人员的不同而改变车型,那么每次想要上车时都需要带上一大批司机,因为他们永远不会知道那辆车会坐在哪里.
换句话说,尝试确定在运行时需要读取多少内存将是非常低效的,并且超过了您可能在停车场中安装更多汽车的事实.
Buu*_*man 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字节为4GB.
在某些意义上,有些对象在C++标准库中具有可变大小,例如std::vector.但是,这些都会动态分配他们需要的额外内存.如果你采用sizeof(std::vector<int>),你将获得一个与对象管理的内存无关的常量,如果你分配一个包含的数组或结构std::vector<int>,它将保留这个基本大小,而不是将额外的存储放在同一个数组或结构中.有一些C语法支持这样的东西,特别是可变长度数组和结构,但C++没有选择支持它们.
语言标准以这种方式定义对象大小,以便编译器可以生成有效的代码.例如,如果int在某些实现上恰好是4个字节长,并且您声明a为指向int值或数组的指针,则a[i]转换为伪代码,"取消引用地址char+ 4× strlen()."这可以在恒定时间内完成,并且是一种常见且重要的操作,许多指令集体系结构,包括最初开发C的x86和DEC PDP机器,都可以在单个机器指令中完成.
作为可变长度单元连续存储的数据的一个常见的现实世界示例是编码为UTF-8的字符串.(但是,编译器的UTF-8字符串的基础类型仍然是strncpy()宽度1.这允许将ASCII字符串解释为有效的UTF-8,以及许多库代码,例如short int和int继续工作.)任何UTF-8代码点的编码长度可以是1到4个字节,因此,如果您希望字符串中的第五个UTF-8代码点,它可以从数据的第五个字节到第二十个字节开始.找到它的唯一方法是从字符串的开头扫描并检查每个代码点的大小.如果要查找第五个字素,还需要检查字符类.如果你想在一个字符串中找到第一百万个UTF-8字符,你需要运行这个循环一百万次!如果您知道需要经常使用索引,则可以遍历字符串一次并构建索引 - 或者您可以转换为固定宽度编码,例如UCS-4.在字符串中查找百万分之一的UCS-4字符只需要在数组的地址中添加四百万个字符.
可变长度数据的另一个复杂因素是,当您分配它时,您需要分配尽可能多的内存,或者根据需要动态重新分配.为最坏情况分配可能非常浪费.如果需要连续的内存块,重新分配可能会强制您将所有数据复制到其他位置,但允许将内存存储在非连续的块中会使程序逻辑复杂化.
因此,它可能有变长的大数,而不是固定宽度的long int,long long int,std::vector和sizeof(std::vector<int>),但它会是低效的分配和使用它们.此外,所有主流CPU都设计为在固定宽度寄存器上进行算术运算,并且没有一个具有直接在某种可变长度bignum上运行的指令.这些需要在软件中实现,速度要慢得多.
在现实世界中,大多数(但不是全部)程序员已经确定UTF-8编码的好处,特别是兼容性很重要,除了从前到后扫描字符串或复制块之外,我们很少关心任何事情.记忆可变宽度的缺点是可以接受的.我们可以使用类似于UTF-8的压缩,可变宽度元素来做其他事情.但我们很少这样做,而且他们不在标准库中.
当表示值所需的空间可能小于该大小时,为什么类型只有一个与之关联的大小?
主要是因为对齐要求.
对象类型具有对齐要求,这些对齐要求对可以分配该类型的对象的地址施加限制.
想想一栋有很多楼层的建筑,每层楼都有很多房间.
每个房间都是您的尺寸(固定空间),能够容纳N量的人或物品.
事先知道房间大小,它使建筑物的结构部件结构良好.
如果房间没有对齐,那么建筑骨架将不会结构良好.
它可以更少.考虑功能:
int foo()
{
int bar = 1;
int baz = 42;
return bar+baz;
}
Run Code Online (Sandbox Code Playgroud)
它编译成汇编代码(g ++,x64,细节剥离)
$43, %eax
ret
Run Code Online (Sandbox Code Playgroud)
在这里,bar和baz最终使用零个字节来表示.
那么为什么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年代的操作系统,服务器软件和实用程序; 为其他软件提供基础设施(如内存管理)的东西.在如此低的水平上,性能至关重要,编译器正在按照您的要求进行操作.
要更改变量的大小需要重新分配,与浪费更多字节的内存相比,这通常不值得额外的CPU周期.
局部变量在堆栈上运行,当这些变量的大小没有变化时,操作非常快.如果您决定将变量的大小从1个字节扩展到2个字节,则必须将堆栈上的所有内容移动一个字节以为其创建该空间.这可能会花费很多CPU周期,具体取决于需要移动的内容.
另一种方法是通过使每个变量成为指向堆位置的指针,但实际上你会以这种方式浪费更多的CPU周期和内存.指针是4个字节(32位寻址)或8个字节(64位寻址),因此您已经使用4或8作为指针,然后是堆上数据的实际大小.在这种情况下,重新分配仍然需要付出代价.如果你需要重新分配堆数据,你可能会很幸运,并有空间扩展它内联,但有时你必须将它移动到堆上的其他地方,以获得所需大小的连续内存块.
决定事先使用多少内存总是更快.如果您可以避免动态调整大小,则可以获得性能.浪费内存通常值得获得性能提升.这就是计算机拥有大量内存的原因.:)