作者:張道遠
链接:https://www.zhihu.com/question/27862634/answer/38506197
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

3 个因素导致现在的地址对齐约定:
  1. 生活很艰难
  2. 世界多姿多彩,世上有各种不同的人存在
  3. 但我们还是要在一起呀在一起

以下以最简,理想的模型进行讨论。

计算机的不同组件对“对齐”有不同的看法。比如一个最小存储单位为 8 字节的内存来说。访问地址1, 大小为 4 字节的数据。只需读取地址 0 的 8 字节的数据。然后在出口处移位下就行。因为只需在出口处做一次处理,所以可以不计成本进行移位优化。因为 RISC CPU 的设计,大多精简指令集的指令长就是字长。而指令还需区分取立即数和各种 action, 所以一字长的指令无法全部用来表示地址空间。所以大多 RISC CPU 强制地址对齐,地址的低位脑补成 0。顺便也减少了地址线的宽度。

访问内存的速度是非常非常非常……慢的。再加上 CPU 及其指令设计的限制。在这种艰苦条件下,我们必须无所不用其极地减少随机内存访问次数:

  • 在一个访问周期里读写最多,但不更多的数据。也就是一字长大小的数据。
  • 对于 CPU, 内存的最小存储单元的大小为最大,但不更大的 CPU 字长。

基于此,一般的 RISC CPU 的地址线宽度为 . 比如一个 32 位 CPU 的字长是 32bit, 字节大小为 8bit, 那么地址线宽度为 可选择 个地址单位, 每个单位的大小是 4 字节 于是总共可管理 字节的内存。这也是逻辑地址最低 位总是为 0 的来历了。对于 32bit 字长的 CPU, 就是最低 2 位为 0 了。注意,此段所述只是基于 de-facto 习惯(1 byte = 8 bit, mem 空间大小为 byte), 为了便于讨论而做的假设。并无特别的意义和强行规定。

以一个字长为 4 字节的 RISC CPU 来进行讨论。单位为字节。
此时,如果我们需要访问地址为 2 的大小为 1 字长也就是 4 字节的数据,也就是 2-5 的数据. 地址 2 mod 大小 4 = 2 不为 0. 这是未对齐的访问。(地址线以二进制表示。最后两位空置,所以始终为逻辑 0)我们需要将地址线设为 0(00) 读出 0-4, 取 2-3, 然后将地址线设为 1(00) 读出 4-7 取 4-5, 然后再合成所需数据。共两次访问。如果要访问地址为 0(00) 或者 1(00) 的大小为 1 字长的数据。则一次访问即可。
这就是地址不对齐导致访问变慢的来历了。

此时若要访问 地址 2 的半个字长大小的数据。也就是 2-3 的数据。我们可以将地址设为 0(00) 读出 0-3 的数据,然后将其在寄存器中右移 2 字节即可。

那么,问题来了。既然如此,我也可以访问地址 1, 大小为 2 字节的数据啊。也就是 1-2 的数据。将地址线设成 0(00) 读出数据,然后在寄存器内左移 1 字节,再右移 2 字节就行了啊。
这时候 1 mod 2= 1, 不为 0, 但需要访问内存的次数还是一次。
但世界上有许多地方,那儿的 CPU 字长只有 2 字节。当数据到那些地方去旅行时。那儿的 CPU 访问地址为 1, 大小为 2 字节的数据的时候还是需要两次的。

那么,问题又来了。字长为 4 字节的 CPU, 访问 8 字节长度的数据,这个数据反正始终都要读两次,那么它不对齐也是可以的呀。只要他的地址 mod 4 为 0 就可以了。
但世界上还有些地方,那儿的 CPU 字长是 8 字节的,当数据到那些地方去旅行时。那儿的 CPU 访问大小为 8 字节的数据,若其地址 mod 8 为 0 时,只要读一次就够了。此时读两次就是一种浪费了。

我们的世界是个艰难但又多姿多彩的世界。为了大家的数据都有一个兼容且一致的模型,方便交换,分析。我们郑重做出约定:

大小为 size 的字段,他的结构内偏移 offset 需符合 offset mod size 为 0.
引用的 wikipedia 的第一段就是对这句话的精确表述。

最后,问题又来了。

struct hi {
let: 4 // padding 4
us: 8 // padding 0
play: 1 // padding 1
together: 2 // padding ?
}

together 字段的 padding 是要多少?是的 padding 0 就行了。所以大小是 8 + 8 + 2 + 2= 20
那为什么 gcc 告诉我们应该是 24 呢。

因为我们的世界不是孤单的世界。
数据们可以欢乐地组成团队。

hi group[2];

如果我们不能相互体谅,自私地将最后的 padding 设为 0 的话。
假设第一个 hi 位于地址 0, 那么第二个 hi 就得从地址 20 开始了。此时 us 的地址是 , 而 28 mod 8= 4 不为 0.
如果 hi 的大小为其中最大单元的整数倍也就是 8 * 3= 24 的话。那么 第二个 hi 的 us 字段的地址是 24+8= 32. 而 32 mod 8= 0, 对齐了。所以,最后我们还需要 padding 4 字节。
在这个不孤单的世界里,为了同一类数据能和谐相处。所以我们郑重做出约定:

整个结构的大小必须是其中最大字段大小的整数倍。

于是,不管是在一个数组里没羞没臊地在一起。还是在这个如此多姿多彩各不相同的世界里到处旅行。数据们的美好的生活都可以快速,和谐,一致地进行啦。

最后,若题主有闲,推荐看一下哈佛的 CS101, From NAND to Tetris 课程。从最简单的逻辑门开始,自己动手打造一遍 latch, flip-flop, register,RAM, ALU, CPU, assembler, compiler. 相信到时候你会有更深的体会。

 
====================================华丽的分隔符======================================================
以下是我个人的一点总结,字对齐的原因是为了能够一次读取当前的数据结构,但是由于不明确机器字长或者代码移植到别的机器的原因,字长是不固定,为了使在与字长与该结构相同的CPU一次读取当前值,而采用了对齐的方式,对于最后一个数据结构的对齐主要原因是考虑到数组的关系,所以选择了与最长数据结构的长度对齐。上文解决了困扰我许久的问题,在此表示发自内心的感谢。

转一下关于struct字对齐的原因的更多相关文章

  1. struct内存对齐1:gcc与VC的差别

    struct内存对齐:gcc与VC的差别 内存对齐是编译器为了便于CPU快速访问而采用的一项技术,对于不同的编译器有不同的处理方法. Win32平台下的微软VC编译器在默认情况下采用如下的对齐规则:  ...

  2. struct 字节对齐详解

    一.什么是字节对齐,为什么要对齐? 现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定类型变量的时候经常在特 定的内存地址访问, ...

  3. 重新学struct,边界对齐,声明……与Union的区别

    在内存中,编译器按照成员列表顺序分别为每个结构体变量成员分配内存,当存储过程中需要满足边界对齐的要求时,编译器会在成员之间留下额外的内存空间. 如果想确认结构体占多少存储空间,则使用关键字sizeof ...

  4. 利用php unpack读取c struct的二进制数据,struct内存对齐引起的一些问题

    c语言代码 #include <stdio.h> struct test{ int a; unsigned char b; int c; }; int main(){ FILE *fp; ...

  5. struct内存对齐

    内存对齐其实是为了在程序运行的时候更快的查找内存而做的一种编译器优化. 我们先看这样一个例子: #include <iostream> using namespace std; struc ...

  6. struct字节对齐原则

    原则1:windows下,k字节基本类型以k字节倍数偏移量对齐,自定义结构体则以结构体中最高p字节基本类型的p字节倍数偏移量对齐,Linux下则以2或4字节对齐; 原则2:整体对齐原则,例如数组结构体 ...

  7. c\c++里struct字节对齐规则

    规则一.: 每个成员变量在其结构体内的偏移量都是成员变量类型的大小的倍数.   规则二: 如果有嵌套结构体,那么内嵌结构体的第一个成员变量在外结构体中的偏移量,是内嵌结构体中那个数据类型大小最大的成员 ...

  8. jQuery dataTables 列不对齐的原因

    如果把 jQuery dataTables 用在初始化时为隐藏的区域中,会发现表头和内容的列是不对齐的. 解决方案: 如果是折叠的,可以加上: $('#myCollapsible').on('show ...

  9. C 语言结构体 struct 及内存对齐

    struct 结构体 对于复杂的数据类型(例如学生.汽车等),C 语言允许我们将多种数据封装到一起,构成新类型. 跟面向对象语言中的对象相比,结构体只能包含成员变量,不支持操作. #include & ...

随机推荐

  1. Java数组扩容算法及Java对它的应用

    1)Java数组对象的大小是固定不变的,数组对象是不可扩容的.利用数组复制方法可以变通的实现数组扩容.System.arraycopy()可以复制数组.Arrays.copyOf()可以简便的创建数组 ...

  2. js页面跳转(含框架跳转)整理

    js方式的页面跳转1.window.location.href方式    <script language="javascript" type="text/java ...

  3. SQL SERVER 2005 DBCC PAGE命令说明

    夏日福利: 小泽玛利亚的“专业摄影”性感写真集:http://947kan.com/video/player-52475-0-0.html ------------------------------ ...

  4. C++语法-指针 (1)

    <C++程序设计> 谭浩强  清华大学出版社 2016-08-03 1.P167 一般的C++编译系统为每个指针变量分配4个字节的存储单元,用来存放变量的地址. 2.P169 .cpp文件 ...

  5. C语言执行时报错“表达式必须是可修改的左值,无法从“const char [3]”转换为“char [120]” ”,原因:字符串不能直接赋值

    解决该问题的方法:使用strcpy函数进行字符串拷贝   原型声明:char *strcpy(char* dest, const char *src); 头文件:#include <string ...

  6. easyui的textbox赋值小结

    使用的系统中有个后台,需要填充单号,如下图: 每次往框里面填充都是一样的数据,复制.粘贴,而且当人数颇多的时候,就是体力活. 于是就想到通过执行js代码,自动填充这些数据. chrome下F12,查看 ...

  7. JS数组常用函数以及查找数组中是否有重复元素的三种常用方法

    阅读目录: DS01:常用的查找数组中是否有重复元素的三种方法 DS02:常用的JS函数集锦 DS01.常用的查找数组中是否有重复元素的三种方法  1. var ary = new Array(&qu ...

  8. cloudfoundry上搭建go服务端

    虽然只有60天试用期,我还是把教程公布一下: 1. 注册一个cloudfoundry账号 https://console.run.pivotal.io/register 2. 安装CLI部署工具包 h ...

  9. OC--编码建议

      原文 http://www.cocoachina.com/ios/20151118/14242.html   本文是投稿文章,作者: IOS_Tips(微信公众号) “神在细节之中” Object ...

  10. Linq to entities 学习笔记

    Linq to  entities ---提供语言集成查询支持用于在概念模型中定义的实体类型. 首先可以根据http://msdn.microsoft.com/en-us/data/jj206878该 ...