解析内存对齐 Data alignment: Straighten up and fly right的详解

2023-11-02 04:41

本文主要是介绍解析内存对齐 Data alignment: Straighten up and fly right的详解,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

解析内存对齐 Data alignment: Straighten up and fly right的详解


对于所有直接操作内存的程序员来说,数据对齐都是很重要的问题.数据对齐对你的程序的表现甚至能否正常运行都会产生影响。为了速度和正确性,请对齐你的数据.

    概述:对于所有直接操作内存的程序员来说,数据对齐都是很重要的问题.数据对齐对你的程序的表现甚至能否正常运行都会产生影响.就像本文章阐述的一样,理解了对齐的本质还能够解释一些处理器的"奇怪的"行为.

 

内存存取粒度

   程序员通常倾向于认为内存就像一个字节数组.C及其衍生语言中,char * 用来指代"一块内存",甚至在JAVA中也有byte[]类型来指代物理内存.

 


Figure 1. 程序员是如何看内存的

 

   然而,你的处理器并不是按字节块来存取内存的.它一般会以双字节,四字节,8字节,16字节甚至32字节为单位来存取内存.我们将上述这些存取单位称为内存存取粒度.

 


Figure 2. 处理器是如何看内存的

 

   高层(语言)程序员认为的内存形态和处理器对内存的实际处理方式之间的差异产生了许多有趣的问题,本文旨在阐述这些问题.

   如果你不理解内存对齐,你编写的程序将有可能产生下面的问题,按严重程度递增:

程序运行速度变慢

应用程序产生死锁

操作系统崩溃

你的程序会毫无征兆的出错,产生错误的结果(silently fail如何翻译?)

 

内存对齐基础

   为了说明内存对齐背后的原理,我们考察一个任务,并观察内存存取粒度是如何对该任务产生影响的.这个任务很简单:先从地址0读取4个字节到寄存器,然后从地址1读取4个字节到寄存器.

   首先考察内存存取粒度为1byte的情况:

 


Figure 3. 单字节存取

 

   这迎合了那些天真的程序员的观点:从地址0和地址1读取4字节数据都需要相同的4次操作.现在再看看存取粒度为双字节的处理器(像最初的68000处理器)的情况:

 


Figure 4. 双字节存取

 

   从地址0读取数据,双字节存取粒度的处理器读内存的次数是单字节存取粒度处理器的一半.因为每次内存存取都会产生一个固定的开销,最小化内存存取次数将提升程序的性能.

   但从地址1读取数据时由于地址1没有和处理器的内存存取边界对齐,处理器就会做一些额外的工作.地址1这样的地址被称作非对齐地址.由于地址1是非对齐的,双字节存取粒度的处理器必须再读一次内存才能获取想要的4个字节,这减缓了操作的速度.


   
最后我们再看一下存取粒度为4字节的处理器(68030,PowerPC® 601)的情况:


Figure 5. 四字节存取

 

   在对齐的内存地址上,四字节存取粒度处理器可以一次性的将4个字节全部读出;而在非对齐的内存地址上,读取次数将加倍.

   既然你理解了内存对齐背后的原理,那么你就可以探索该领域相关的一些问题了.

懒惰的处理器

   处理器对非对齐内存的存取有一些技巧.考虑上面的四字节存取粒度处理器从地址1读取4字节的情况,你肯定想到了下面的解决方法:


Figure 6. 处理器如何处理非对齐内存地址

 

   处理器先从非对齐地址读取第一个4字节块,剔除不想要的字节,然后读取下一个4字节块,同样剔除不要的数据,最后留下的两块数据合并放入寄存器.这需要做很多工作.

   有些处理器并不情愿为你做这些工作.

   最初的68000处理器的存取粒度是双字节,没有应对非对齐内存地址的电路系统.当遇到非对齐内存地址的存取时,它将抛出一个异常.最初的Mac OS并没有妥善处理这个异常,它会直接要求用户重启机器.悲剧.

   随后的680x0系列,68020,放宽了这个的限制,支持了非对齐内存地址存取的相关操作.这解释了为什么一些在68020上正常运行的旧软件会在68000上崩溃.这也解释了为什么当时一些老Mac编程人员会将指针初始化成奇数地址.在最初的Mac机器上如果指针在使用前没有被重新赋值成有效地址,Mac会立即跳到调试器.通常他们通过检查调用堆栈会找到问题所在.

   所有的处理器都使用有限的晶体管来完成工作.支持非对齐内存地址的存取操作会消减"晶体管预算",这些晶体管原本可以用来提升其他模块的速度或者增加新的功能.

   以速度的名义牺牲非对齐内存存取功能的一个例子就是MIPS.为了提升速度,MIPS几乎废除了所有的琐碎功能.

    PowerPC各取所长.目前所有的PowPC都硬件支持非对齐的32位整型的存取.虽然牺牲掉了一部分性能,但这些损失在逐渐减少.

   另一方面,现今的PowPC处理器缺少对非对齐的64-bit浮点型数据的存取的硬件支持.当被要求从非对齐内存读取浮点数时,PowerPC会抛出异常并让操作系统来处理内存对齐这样的杂事.软件解决内存对齐要比硬件慢得多.

psting 1. 每次处理一个字节

复制代码 代码如下:

void Munge8( void *data, uint32_t size ){
    uint8_t *data8 = (uint8_t*)data;
    uint8_t *data8End = data8 +size;

    while( data8 != data8End ){
        *data8++ = -*data8;
    }
}



   运行这个函数需要67364微秒,现在修改成每次处理2个字节,这将使存取次数减半:

psting 2.每次处理2个字节

复制代码 代码如下:

void Munge16( void *data, uint32_t size ){
    uint16_t *data16 = (uint16_t*)data;
    uint16_t *data16End = data16 + (size>> 1); /* Divide size by 2. */
    uint8_t *data8 = (uint8_t*)data16End;
    uint8_t *data8End = data8 + (size& 0x00000001); /* Strip upper 31 bits. */

    while( data16 != data16End ){
        *data16++ = -*data16;
    }
    while( data8 != data8End ){
        *data8++ = -*data8;
    }
}

   如果处理的内存地址是对齐的话,上述函数处理同一个缓冲区需要48765微秒--比Munge8快38%.如果缓冲区不是对齐的,处理时间会增加到66385微秒--比对齐情况下慢了27%.下图展示了对齐内存和非对齐内存之间的性能对比.

速度

   下面编写一些测试来说明非对齐内存对性能造成的损失.过程很简单:从一个10MB的缓冲区中读取,取反,并写回数据.这些测试有两个变量:

处理缓冲区的处理粒度,单位bytes. 一开始每次处理1个字节,然后2个字节,4个字节和8个字节.

缓冲区的对准. 用每次增加缓冲区的指针来交错调整内存地址,然后重新做每个测试.

   这些测试运行在800MHzPowerBook G4.为了最小化中断引起的波动,这里取十次结果的平均值.第一个是处理粒度为单字节的情况:


psting 1. 每次处理一个字节

复制代码 代码如下:

void Munge8( void *data, uint32_t size ){
    uint8_t *data8 = (uint8_t*)data;
    uint8_t *data8End = data8 +size;

    while( data8 != data8End ){
        *data8++ = -*data8;
    }
}



   运行这个函数需要67364微秒,现在修改成每次处理2个字节,这将使存取次数减半:

psting 2.每次处理2个字节

复制代码 代码如下:

void Munge16( void *data, uint32_t size ){
    uint16_t *data16 = (uint16_t*)data;
    uint16_t *data16End = data16 + (size>> 1); /* Divide size by 2. */
    uint8_t *data8 = (uint8_t*)data16End;
    uint8_t *data8End = data8 + (size& 0x00000001); /* Strip upper 31 bits. */

    while( data16 != data16End ){
        *data16++ = -*data16;
    }
    while( data8 != data8End ){
        *data8++ = -*data8;
    }
}

   如果处理的内存地址是对齐的话,上述函数处理同一个缓冲区需要48765微秒--比Munge8快38%.如果缓冲区不是对齐的,处理时间会增加到66385微秒--比对齐情况下慢了27%.下图展示了对齐内存和非对齐内存之间的性能对比.


Figure7. 单字节存取 vs.双字节存取

 

   第一个让人注意到的现象是单字节存取结果很均匀,且都很慢.第二个是双字节存取时,每当地址是单数时,变慢的27%就会出现.

   下面加大赌注,每次处理4个字节:

psting 3. 每次处理4个字节

复制代码 代码如下:

void Munge32( void *data, uint32_t size ){
    uint32_t *data32 = (uint32_t*)data;
    uint32_t *data32End = data32 + (size>> 2); /* Divide size by 4. */
    uint8_t *data8 = (uint8_t*)data32End;
    uint8_t *data8End = data8 + (size& 0x00000003); /* Strip upper 30 bits. */

    while( data32 != data32End ){
        *data32++ = -*data32;
    }
    while( data8 != data8End ){
        *data8++ = -*data8;
    }
}

   对于对齐的缓冲区,函数需要43043微秒;对于非对齐的缓冲区,函数需要55775微秒.因此,在所测试的机器上,非对齐地址的四字节存取速度比对齐地址的双字节存取速度要慢.

 


Figure8. 单字节vs.双字节vs.四字节存取

 

现在来最恐怖的:每次处理8个字节:

psting 4.每次处理8个字节

复制代码 代码如下:

void Munge64( void *data, uint32_t size ){
    double *data64 = (double*)data;
    double *data64End = data64 + (size>> 3); /* Divide size by 8. */
    uint8_t *data8 = (uint8_t*)data64End;
    uint8_t *data8End = data8 + (size& 0x00000007); /* Strip upper 29 bits. */

    while( data64 != data64End ){
        *data64++ = -*data64;
    }
    while( data8 != data8End ){
        *data8++ = -*data8;
    }
}

    Munge64处理对齐的缓冲区需要39085微秒--大约比对齐的Munge3210%.但是,在非对齐缓冲区上的处理时间是让人惊讶的1841155微秒--比对齐的慢了两个数量级,慢了足足4610%.

   怎么回事?因为我们现今所使用的PowerPC缺少对存取非对齐内存的浮点数的硬件支持.对每次非对齐内存的存取,处理器都抛出一个异常.操作系统获取该异常并软件实现内存对齐.下图显示了非对齐内存存取带来的不利后果.

 


Figure 9. 多字节存取对比

 

   单字节,双字节和四字节的细节都被掩盖了.或许去除顶部以后的图形,如下图,更清晰:

 


Figure 10. 多字节存取对比 #2

 

   在这些数据背后还隐藏着一个微妙的现象.比较8字节粒度时边界是4的倍数的内存的存取速度:

 


Figure10. 多字节存取对比 #3

 

   你会发现8字节粒度时边界为412字节的内存存取速度要比相同情况下的42字节粒度的慢.即使PowerPC硬件支持4字节对齐的8字节双浮点型数据的存取,你还是要承担额外的开销造成的损失.诚然,这种损失绝不会像4610%那么大,但还是不能忽略的.这个实验告诉我们:存取非对齐内存时,大粒度的存取可能会比小粒度存取还要慢

您可能感兴趣的文章:
  • 深入理解c/c++ 内存对齐
  • 深入理解C语言内存对齐
  • 浅析内存对齐与ANSI C中struct型数据的内存布局
  • 深入内存对齐的详解
  • c++动态内存空间示例(自定义空间类型大小和空间长度)
  • C/C++语言中结构体的内存分配小例子
  • C/C++动态分配与释放内存的区别详细解析
  • 深入解析C++ Data Member内存布局
  • C/C++ 传递动态内存的深入理解
  • 关于C++内存中字节对齐问题的详细介绍
  • 基于C++中常见内存错误的总结
  • VC++中内存对齐实例教程

这篇关于解析内存对齐 Data alignment: Straighten up and fly right的详解的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/328186

相关文章

MySQL中的交叉连接、自然连接和内连接查询详解

《MySQL中的交叉连接、自然连接和内连接查询详解》:本文主要介绍MySQL中的交叉连接、自然连接和内连接查询,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、引入二、交php叉连接(cross join)三、自然连接(naturalandroid join)四

Go 语言中的select语句详解及工作原理

《Go语言中的select语句详解及工作原理》在Go语言中,select语句是用于处理多个通道(channel)操作的一种控制结构,它类似于switch语句,本文给大家介绍Go语言中的select语... 目录Go 语言中的 select 是做什么的基本功能语法工作原理示例示例 1:监听多个通道示例 2:带

mysql的基础语句和外键查询及其语句详解(推荐)

《mysql的基础语句和外键查询及其语句详解(推荐)》:本文主要介绍mysql的基础语句和外键查询及其语句详解(推荐),本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋... 目录一、mysql 基础语句1. 数据库操作 创建数据库2. 表操作 创建表3. CRUD 操作二、外键

Spring Boot项目部署命令java -jar的各种参数及作用详解

《SpringBoot项目部署命令java-jar的各种参数及作用详解》:本文主要介绍SpringBoot项目部署命令java-jar的各种参数及作用的相关资料,包括设置内存大小、垃圾回收... 目录前言一、基础命令结构二、常见的 Java 命令参数1. 设置内存大小2. 配置垃圾回收器3. 配置线程栈大小

鸿蒙中@State的原理使用详解(HarmonyOS 5)

《鸿蒙中@State的原理使用详解(HarmonyOS5)》@State是HarmonyOSArkTS框架中用于管理组件状态的核心装饰器,其核心作用是实现数据驱动UI的响应式编程模式,本文给大家介绍... 目录一、@State在鸿蒙中是做什么的?二、@Spythontate的基本原理1. 依赖关系的收集2.

Redis实现延迟任务的三种方法详解

《Redis实现延迟任务的三种方法详解》延迟任务(DelayedTask)是指在未来的某个时间点,执行相应的任务,本文为大家整理了三种常见的实现方法,感兴趣的小伙伴可以参考一下... 目录1.前言2.Redis如何实现延迟任务3.代码实现3.1. 过期键通知事件实现3.2. 使用ZSet实现延迟任务3.3

C语言函数递归实际应用举例详解

《C语言函数递归实际应用举例详解》程序调用自身的编程技巧称为递归,递归做为一种算法在程序设计语言中广泛应用,:本文主要介绍C语言函数递归实际应用举例的相关资料,文中通过代码介绍的非常详细,需要的朋... 目录前言一、递归的概念与思想二、递归的限制条件 三、递归的实际应用举例(一)求 n 的阶乘(二)顺序打印

Python Faker库基本用法详解

《PythonFaker库基本用法详解》Faker是一个非常强大的库,适用于生成各种类型的伪随机数据,可以帮助开发者在测试、数据生成、或其他需要随机数据的场景中提高效率,本文给大家介绍PythonF... 目录安装基本用法主要功能示例代码语言和地区生成多条假数据自定义字段小结Faker 是一个 python

Java Predicate接口定义详解

《JavaPredicate接口定义详解》Predicate是Java中的一个函数式接口,它代表一个判断逻辑,接收一个输入参数,返回一个布尔值,:本文主要介绍JavaPredicate接口的定义... 目录Java Predicate接口Java lamda表达式 Predicate<T>、BiFuncti

详解如何通过Python批量转换图片为PDF

《详解如何通过Python批量转换图片为PDF》:本文主要介绍如何基于Python+Tkinter开发的图片批量转PDF工具,可以支持批量添加图片,拖拽等操作,感兴趣的小伙伴可以参考一下... 目录1. 概述2. 功能亮点2.1 主要功能2.2 界面设计3. 使用指南3.1 运行环境3.2 使用步骤4. 核