做嵌入式系统软件开发,经常在代码中看到各种各样的对齐,很多时候我们都是知其然不知其所以然,知道要做好各种对齐,但是不明白为什么要对齐,不对齐会有哪些后果,这篇文章大概总结了内存对齐的理由。
CPU体系结构和MMU的要求
目前有一些RISC指令集的CPU不支持非对齐的内存变量访问操作,比如 MIPS/PowerPC/某些DSP等等,如果发生非对齐的内存访问,会产生unaligned exception 异常。
ARM指令集是从ARMv6(ARM11)开始支持非对齐内存访问的,以前老一点的ARM9的CPU也是不支持非对齐访问的。ARM指令集支持的部分特性迭代如下:
尽管现代的ARMv7 ARMv8 指令集的Cortex-AXX系列CPU都支持非对齐内存访问,但是考虑到如下图所示现代SOC芯片里面多种异构CPU协调工作的情况,主CPU用于跑Linux/Android操作系统的ARM64可以支持非对齐内存访问,但是SOC里面还有其它不知道体系结构和版本的协CPU(可能是MIPS, ARM7,Cortex-R/M系列, 甚至51单片机核),这些协CPU都和主ARM64主CPU共享物理内存的不同地址段,并且有自己的固件程序在内存上运行,所以在划分地址空间的时候还是要注意内存对齐的问题,尤其是考虑到这些协CPU可能不支持非对齐访问,同样在编写协CPU固件程序的时候,也要清晰认识到该CPU是否支持非对齐内存访问。
image.png
同样在ARM的MMU虚拟地址管理中,也有内存地址对齐的要求,下图是ARM的MMU的工作原理和多级页表(Translation Tables)的索引关系图
ARM体系架构的MMU要求
arm 32位体系结构要求L1第一级页表基地址(The L1 Translation Table Base Addr)对齐到16KB的地址边界,L2第二级页表地址(The L2 Translation Table Add)对齐到1KB的地址边界。
ARM 64位体系结构要求虚拟地址的第21-28位VA[28:21]对齐到64 KB granule, 第16到20位VA[20:16]对齐到4 KB granule。
ARM 的Memory ordering特性中的不同Memory types对非对齐内存访问的支持的要求是不同的。下图是ARM Memory ordering特性中三种不同的Memory types访问规则
只有Normal Memory是支持非对齐内存访问的
Strongly-ordered 和 Device Memory不支持非对齐内存访问
对原子操作的影响
尽管现代的ARMv7 ARMv8 指令集的ARM CPU支持非对齐内存访问,但是非对齐内存访问是无法保证操作的原子性。下图分别是一个变量在内存对齐和非对齐的时候的内存布局:
内存对齐的变量访问,使用单个通用的CPU寄存器暂存,一个内存对齐的变量的读写操作能保证是单次原子操作.
非对齐的变量的内存访问是非原子操作,他们通常情况下访问一个非对齐的内存中的变量需要2次分别的对内存进行访问,因而不能保证原子性,一旦发生2次分别内存访问,2次分别的访问中间就有可能被异步事件打断,造成变量改变,因而不能保证原子性。
ARM NEON的要求
现代ARM CPU一般都有一个NEON的协处理器,一般用在浮点计算中用来做SIMD并行矢量加速计算。下图是NEON SIMD并行矢量计算的基本原理图:
NEON本身是支持非对齐内存访问的
但是NEON访问非对齐的内存一般会有2个指令周期的时间penalty
通常情况下,为了灵活应用NEON的并行计算特性,在做SIMD并行矢量加速运算时,我们要根据NEON寄存器的Lane的bits数对齐相应的变量。如果是配置成8-bits的计算,就做8-bits对齐,如果是16-bits计算,就做16-bits对齐,以此类推,NEON的并行矢量计算的lane根据spec手册,有各种灵活配置的方法。
对性能perf的影响
通常而言,尽管现代的ARM CPU已经支持非对齐内存的访问,但是ARM访问非对齐的内存地址还是会造成明显的性能下降。因为访问一个非对齐的内存,需要增加多次load/store内存变量次数,进而增加了程序运行的指令周期
才有perf工具进行性能分析,能看到非对齐内存访问的性能下降,在perf工具中有一个alignment-faults的事件,可以观察程序访问非对齐内存的事件统计
cache line 对齐
除了通常所讲的根据CPU访问内存的地址位数的内存对齐之外,在程序优化的时候,还要考虑到cache存在的情况,根据cache line的长度来对齐你的访问变量。
cache和cache line的结构原理图如下(其中图2从该文章引用自: cenalulu),cache line是cache和内存进行数据传输的最小单位,一般cache都是以cache line的长度一次读写内存中的映射地址。
在ARM 系列的CPU中,不同型号的ARM CPU的cache line长度是不一样的,因此同样是基于ARM平台的CPU,从A平台移植优化过的程序到B平台时,一定要注意不同CPU的cache line大小是否一致,是否要重新调整cache line对齐优化。下图是ARMv7几款公版CPU的cache line的资料手册,ARMv8 64位的公版CPU(A53, A57, A72, A73)目前的cache line大小都是64 bytes, 但是各家公司基于公版ARM的定制版CPU的cache line大小可能有差异,一定要参考相关TRM手册进行调整、对齐、优化.
下图是一个例子关于未做cache line对齐的情况下,进行内存读写性能抖动的例子,引用自cenalulu.测试代码如下程序的大意,对不同大小的数组进行1亿次读写操作,统计不同数组size时的读写时间。从测试的结果可以看出,当数组大小小于cache line size时,读写时间基本变化不大,当数组大小刚刚超过cache line size的时候,读写时间发生了剧烈的抖动。这是因为超过cache line 大小的数组元素可能没有提前预读到cache line中,在访问完cache line中的数组元素之后,要重新从内存读取数据,刷新cache line,因而产生了性能抖动。通过这个例子告诉我们,充分利用系统cache特性,根据cache line对齐你的数据,保证程序访问的局部数据都在一个cache line中可以提升系统性能。
#include"stdio.h" #include#include longtimediff(clock_tt1,clock_tt2){ longelapsed; elapsed=((double)t2-t1)/CLOCKS_PER_SEC*1000; returnelapsed; } intmain(intargc,char*argv[]) #******* { intarray_size=atoi(argv[1]); intrepeat_times=1000000000; longarray[array_size]; for(inti=0;i image.jpg
没有对齐到同一个cache line中的变量,在多核SMP系统中,cross cache line操作是非原子操作,存在篡改的风险。该例子引用自kongfy)测试代码如下,程序大意是,系统cpu的cache line是64字节,一个68字节的结构体struct data, 其中前面填充60字节的pad[15]数组,最后一个8字节的变量v, 这样结构体大小超过了64字节,最后一个变量v的前后部分可定不在同一个cache line中,整个结构体没法根据cache line对齐。全局变量value.v初始值是0, 程序开多线程,对全局变量value.v进行多次~位取反操作,直觉上最后结果value.v的位结果不是全0就是全1,但是最后value.v的位结果居然是一半1一半0, 这就是由于cross cache line 操作是非原子性的,导致一个线程对value.v前半部分取反的时候,另外的线程对后半部分在另一个cache line同时取反,然后前一个线程再对另一个cache line的value.v后半部分取反,导致和直觉不一致。
#include#include #include #include usingnamespacestd; staticconstint64_tMAX_THREAD_NUM=128; staticint64_tn=0; staticint64_tloop_count=0; #pragmapack(1) structdata { int32_tpad[15]; int64_tv; }; #pragmapack() staticdatavalue__attribute__((aligned(64))); staticint64_tcounter[MAX_THREAD_NUM]; voidworker(int*cnt) { for(int64_ti=0;i< loop_count; ++i) { const int64_t t = value.v; if (t != 0L && t != ~0L) { *cnt += 1; } value.v = ~t; asm volatile("" ::: "memory"); } } int main(int argc, char *argv[]) { pthread_t threads[MAX_THREAD_NUM]; /* Check arguments to program*/ if(argc != 3) { fprintf(stderr, "USAGE: %s ",argv[0]); exit(1); } /*Parseargument*/ n=min(atol(argv[1]),MAX_THREAD_NUM); loop_count=atol(argv[2]);/*Don'tbotherwithformatchecking*/ /*Startthethreads*/ for(int64_ti=0L;i< n; ++i) { pthread_create(&threads[i], NULL, (void* (*)(void*))worker, &counter[i]); } int64_t count = 0L; for (int64_t i = 0L; i < n; ++i) { pthread_join(threads[i], NULL); count += counter[i]; } printf("data size: %lu ", sizeof(value)); printf("data addr: %lX ", (unsigned long)&value.v); printf("final: %016lX ", value.v); return 0; }
-
ARM
+关注
关注
134文章
9080浏览量
367294 -
嵌入式系统
+关注
关注
41文章
3586浏览量
129410 -
内存
+关注
关注
8文章
3016浏览量
73989
原文标题:【内存管理】ARM嵌入式系统为什么要做内存对齐
文章出处:【微信号:嵌入式与Linux那些事,微信公众号:嵌入式与Linux那些事】欢迎添加关注!文章转载请注明出处。
发布评论请先 登录
相关推荐
评论