C语言:内存字节对齐详解
分类:并行开发技术--SSE2012-03-28 14:436人阅读评论(0)收藏 举报
一、什么是对齐,以及为什么要对齐:
1. 现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定变量的时候经常在特定的内存地址访问,这就需要各类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐。
2. 对齐的作用和原因:各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。其他平台可能没有这种情况,但是最常见的是如果不按照适合其平台的要求对数据存放进行对齐,会在存取效率上带来损失。比如有些平台每次读都是从偶地址开始,如果一个int型(假设为 32位)如果存放在偶地址开始的地方,那么一个读周期就可以读出,而如果存放在奇地址开始的地方,就可能会需要2个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该int数据。显然在读取效率上下降很多。这也是空间和时间的博弈。
二、对齐的实现
通常,我们写程序的时候,不需要考虑对齐问题。编译器会替我们选择适合目标平台的对齐策略。当然,我们也可以通知给编译器传递预编译指令而改变对指定数据的对齐方法。
但是,正因为我们一般不需要关心这个问题,所以因为编辑器对数据存放做了对齐,而我们不了解的话,常常会对一些问题感到迷惑。最常见的就是struct数据结构的sizeof结果,出乎意料。为此,我们需要对对齐算法所了解。
对齐的算法:
由于各个平台和编译器的不同,现以本人使用的gcc version 3.2.2编译器(32位x86平台)为例子,来讨论编译器对struct数据结构中的各成员如何进行对齐的。
设结构体如下定义:
struct A {
int a;
char b;
short c;
};
结构体A中包含了4字节长度的int一个,1字节长度的char一个和2字节长度的short型数据一个。所以A用到的空间应该是7字节。但是因为编译器要对数据成员在空间上进行对齐。
所以使用sizeof(strcutA)值为8。
现在把该结构体调整成员变量的顺序。
struct B {
char b;
int a;
short c;
};
这时候同样是总共7个字节的变量,但是sizeof(structB)的值却是12。
下面我们使用预编译指令#pragmapack (value)来告诉编译器,使用我们指定的对齐值来取代缺省的。
#progma pack (2) /*指定按2字节对齐*/
struct C {
char b;
int a;
short c;
};
#progma pack () /*取消指定对齐,恢复缺省对齐*/
sizeof(struct C)值是8。
修改对齐值为1:
#progma pack (1) /*指定按1字节对齐*/
struct D {
char b;
int a;
short c;
};
#progma pack () /*取消指定对齐,恢复缺省对齐*/
sizeof(struct D)值为7。
对于char型数据,其自身对齐值为1,对于short型为2,对于int,float,double类型,其自身对齐值为4,单位字节。
这里面有四个概念值:
1)数据类型自身的对齐值:就是上面交代的基本数据类型的自身对齐值。
2)指定对齐值:#pragmapack (value)时的指定对齐值value。
3)结构体或者类的自身对齐值:其成员中自身对齐值最大的那个值。
4)数据成员、结构体和类的有效对齐值:自身对齐值和指定对齐值中较小的那个值。
有了这些值,我们就可以很方便的来讨论具体数据结构的成员和其自身的对齐方式。有效对齐值N是最终用来决定数据存放地址方式的值,最重要。有效对齐N,就是表示“对齐在N上”,也就是说该数据的"存放起始地址%N=0".而数据结构中的数据变量都是按定义的先后顺序来排放的。第一个数据变量的起始地址就是数据结构的起始地址。结构体的成员变量要对齐排放,结构体本身也要根据自身的有效对齐值圆整(就是结构体成员变量占用总长度需要是对结构体有效对齐值的整数倍,结合下面例子理解)。这样就不难理解上面的几个例子的值了。
例子分析:
分析例子B;
struct B {
char b;
int a;
short c;
};
假设B从地址空间0x0000开始排放。该例子中没有定义指定对齐值,在笔者环境下,该值默认为4。第一个成员变量b的自身对齐值是1,比指定或者默认指定对齐值4小,所以其有效对齐值为1,所以其存放地址0x0000符合0x0000%1=0.第二个成员变量a,其自身对齐值为4,所以有效对齐值也为 4,所以只能存放在起始地址为0x0004到0x0007这四个连续的字节空间中,复核0x0004%4=0,且紧靠第一个变量。第三个变量c,自身对齐值为2,所以有效对齐值也是2,可以存放在0x0008到0x0009这两个字节空间中,符合0x0008%2=0。所以从0x0000到0x0009存放的都是B内容。再看数据结构B的自身对齐值为其变量中最大对齐值(这里是b)所以就是4,所以结构体的有效对齐值也是4。根据结构体圆整的要求, 0x0009到0x0000=10字节,(10+2)%4=0。所以0x0000A到0x000B也为结构体B所占用。故B从0x0000到0x000B共有12个字节,sizeof(structB)=12;
同理,分析上面例子C:
#pragma pack (2) /*指定按2字节对齐*/
struct C {
char b;
int a;
short c;
};
#pragma pack () /*取消指定对齐,恢复缺省对齐*/
第一个变量b的自身对齐值为1,指定对齐值为2,所以,其有效对齐值为1,假设C从0x0000开始,那么b存放在0x0000,符合0x0000%1=0;第二个变量,自身对齐值为4,指定对齐值为2,所以有效对齐值为2,所以顺序存放在0x0002、0x0003、0x0004、0x0005四个连续字节中,符合0x0002%2=0。第三个变量c的自身对齐值为2,所以有效对齐值为2,顺序存放
在0x0006、0x0007中,符合0x0006%2=0。所以从0x0000到0x00007共八字节存放的是C的变量。又C的自身对齐值为4,所以 C的有效对齐值为2。又8%2=0,C只占用0x0000到0x0007的八个字节。所以sizeof(structC)=8.
有了以上的解释,相信你对C语言的字节对齐概念应该有了清楚的认识了吧。在网络程序中,掌握这个概念可是很重要的喔,在不同平台之间(比如在Windows和Linux之间)传递2进制流(比如结构体),那么在这两个平台间必须要定义相同的对齐方式,不然莫名其妙的出了一些错,可是很难排查的哦^_^。
· 上一篇:__declspec关键字详细用法
· 下一篇:SSE指令算法及应用----入门篇
SSE指令算法及应用----入门篇
分类:并行开发技术--SSE2012-03-28 16:2912人阅读评论(0)收藏 举报
SSE是英特尔提出的即MMX之后新一代(当然是几年前了)CPU指令集,最早应用在PIII系列CPU上。现在已经得到了Intel PIII、P4、Celeon、Xeon、AMD Athlon、duron等系列CPU的支持。而更新的SSE2指令集仅得到了P4系列CPU的支持,这也是为什么这篇文章是讲SSE而不是SSE2的原因之一。另一个原因就是SSE和SSE2的指令系统是非常相似的,SSE2比SSE多的仅是少量的额外浮点处理功能、64位浮点数运算支持和64位整数运算支持。
SSE为什么会比传统的浮点运算更快呢?因为它使用了128位的存储单元,这对于32位的浮点数来讲,是可以存下4个的,也就是说,SSE中的所有计算都是一次性针对4个浮点数来完成的,这种批处理当然就会带来效率的提升。我们再来回顾一下SSE的全称:Stream SIMD Extentions(流SIMD扩展)。SIMD就是singleinstruction multiple data,连起来就是“数据流单指令多数据扩展”,从名字我们就可以更好的理解SSE是如何工作的了。
虽然SSE从理论上来讲要比传统的浮点运算会快,但是他所受的限制也很多,首先,虽然他执行一次相当于四次,会比传统的浮点运算执行4次的速度要快,但是他执行一次的速度却并没有想象中的那么快,所以要体现SSE的速度,必须有Stream做前提,就是大量的流数据,这样才能发挥SIMD的强大作用。其次,SSE支持的数据类型是4个32位(共计128位)浮点数集合,就是C、C++语言中的float[4],并且必须是以16字节边界对齐的(稍后会以代码来进行阐释,关于边界对齐的概念,读者可以参考论坛上的其它文章,都会有很详细的解答,我这里就恕不赘述了)。因此这也给输入和输出带来了不少的麻烦,实际上主要影响SSE发挥性能的就是不停的对数据进行复制以适用应它的数据格式。
我是一个C++程序员,对汇编并不很熟,但我又想用SSE来优化我的程序,我该怎么做呢?幸好VC++.net为我们提供了很方便的指令C函数级的封装和C格式数据类型,我们只需像平时写C++代码一样定义变量、调用函数就可以很好的应用SSE指令了。
当然了,我们需要包含一个头文件,这里面包括了我们需要的数据类型和函数的声明:
| #include <xmmintrin.h> |
SSE运算的标准数据类型只有一个,就是:
__m128,它是这样定义的:
| typedef struct __declspec(intrin_type) __declspec(align(16)) __m128 { float m128_f32[4]; } __m128; |
简化一下,就是:
| struct __m128 { float m128_f32[4]; }; |
比如要定义一个__m128变量,并为它赋四个float整数,可以这样写:
| __m128 S1 = { 1.0f, 2.0f, 3,0f, 4,0f }; |
要改变其中第2个(基数为0)元素时可以这样写:
| S1.m128_f32[2] = 6.0f; |
令外我们还会用到几个赋值的指令,它可以让我们更方便的使用这个数据结构:
| S1 = _mm_set_ps1( 2.0f ); |
它会让S1.m128_f32中的四个元素全部赋予2.0f,这样会比你一个一个赋值要快的多。
| S1 = _mm_setzero_ps(); |
这会让S1中的所有4个浮点数都置零。
还有一些其它的赋值指令,但执行起来还没有自己逐个赋值来的快,只做为一些特殊用途,如果你想了解更多的信息,可以参考MSDN -> VisualC++参考 -> C/C++Language-> C++Language Reference -> Compiler Intrinsics -> MMX, SSE, and SSE2Intrinsics -> Stream SIMD Extensions(SSE)章节。
一般来讲,所有SSE指令函数都有3个部分组成,中间用下划线隔开:
| _mm_set_ps1 |
mm表示多媒体扩展指令集
set表示此函数的含义缩写
ps1表示该函数对结果变量的影响,由两个字母组成,第一个字母表示对结果变量的影响方式,p表示把结果做为指向一组数据的指针,每一个元素都将参与运算,S表示只将结果变量中的第一个元素参与运算;第二个字母表示参与运算的数据类型。s表示32位浮点数,d表示64位浮点数,i32表示32位定点数,i64表示64位定点数,由于SSE只支持32位浮点数的运算,所以你可能会在这些指令封装函数中找不到包含非s修饰符的,但你可以在MMX和SSE2的指令集中去认识它们。
接下来我举一个例子来说明SSE的指令函数是如何使用的,必须要说明的是我以下的代码都是在VC7.1的平台上写的,不保证对其它如Dev-C++、Borland C++等开发平台的完全兼容。
为了方便对比速度,我会用常归方法和SSE优化两种写法写出,并会用一个测试速度的类CTimer来进行计时。
这个算法是对一组float值进行放大,函数ScaleValue1是使用SSE指令优化的,函数ScaleValue2则没有。我们用10000个元素的float数组数据来测试这两个算法,每个算法运算10000遍,下面是测试程序和结果:
| #include <xmmintrin.h> #include <windows.h> |
| class CTimer { public: __forceinline CTimer( void ) { QueryPerformanceFrequency( &m_Frequency ); QueryPerformanceCounter( &m_StartCount ); } __forceinline void Reset( void ) { QueryPerformanceCounter( &m_StartCount ); } __forceinline double End( void ) { static __int64 nCurCount; QueryPerformanceCounter( (PLARGE_INTEGER)&nCurCount ); return double( nCurCount * ( *(__int64*)&m_StartCount ) ) / double( *(__int64*)&m_Frequency ); } private: LARGE_INTEGER m_Frequency; LARGE_INTEGER m_StartCount; };
//使用SSE的函数 void ScaleValue1( float *pArray, DWORD dwCount, float fScale ) { |
| DWORD dwGroupCount = dwCount / 4; __m128 e_Scale = _mm_set_ps1( fScale ); for ( DWORD i = 0; i < dwGroupCount; i++ ) { *(__m128*)( pArray + i * 4 ) = _mm_mul_ps( *(__m128*)( pArray + i * 4 ), e_Scale ); } }
//不使用SSE的函数 void ScaleValue2( float *pArray, DWORD dwCount, float fScale ) { for ( DWORD i = 0; i < dwCount; i++ ) { pArray[i] *= fScale; } } #define ARRAYCOUNT 10000 int __cdecl main() { //设置数据按16字节对齐 float __declspec(align(16)) Array[ARRAYCOUNT]; memset( Array, 0, sizeof(float) * ARRAYCOUNT ); CTimer t; double dTime; t.Reset(); |
| for ( int i = 0; i < 100000; i++ ) { ScaleValue1( Array, ARRAYCOUNT, 1000.0f ); } dTime = t.End(); cout << "Use SSE:" << dTime << "秒" << endl; t.Reset(); for ( int i = 0; i < 100000; i++ ) { ScaleValue2( Array, ARRAYCOUNT, 1000.0f ); } dTime = t.End(); cout << "Not Use SSE:" << dTime << "秒" << endl; system( "pause" ); return 0; } Use SSE:0.997817 Not Use SSE:2.84963 |
这里要注意一下,我使用了__declspec(align(16))做为数组定义的修释符,这表示该数组是以16字节为边界对齐的,因为SSE指令只能支持这种格式的内存数据。
我的第一个SSE算法
分类:并行开发技术--SSE2012-03-28 16:5414人阅读评论(0)收藏 举报
/*************************************************
*文件名:SSEProject.cpp
*版本:0.0
*功能描述:利用SSE函数优化浮点数据相乘的算法
*创建日期:2012-03-28
*作者:Brandy Yin
*修改记录:
*备注:我的第一个SSE算法
*************************************************/
#include <windows.h>
#include <xmmintrin.h>
#include<iostream>
#include "Time.h"
using std::cout;
using std::endl;
#define ArrayCount 1000
//使用SSE的函数
void f1(float* i_array,DWORD i_num,float i_mul)
{
DWORD num=i_num/4;
__m128 mul=_mm_set_ps1(i_mul);
for (int i=0;i!=num;i++)
{
*(__m128*)(i_array+i*4)=_mm_mul_ps(*(__m128*)(i_array+i*4),mul);
}
}
//不使用SSE的函数
void f2(float* i_array,DWORD i_num,float i_mul)
{
for (int i=0;i!=i_num;i++)
{
*(i_array+i)=(*(i_array+i))*i_mul;
}
}
int main()
{
//声明数组在内存中按16字节对齐
float _declspec(align(16)) Array[ArrayCount];
//利用memset函数初始化数组
memset(Array,0,sizeof(float)*ArrayCount);
CTime t;
t.Reset();
for (int i=0;i!=100000;i++)
{
f1(Array,ArrayCount,1000.2f);
}
cout<<"Use SSE: "<<t.End()<<"ms"<<endl;
t.Reset();
for (int i=0;i!=100000;i++)
{
f2(Array,ArrayCount,1000.2f);
}
cout<<"Don't Use SSE:"<<t.End()<<"ms"<<endl;
system("pause");
return 0;
}
作为一个SSE初学者,我在写这个算法时遇到了以下问题:
1)什么是SSE?----简单地说,他是一组CPU指令集,VC++.net为我们提供了很方便的指令C函数级的封装和C格式数据类型,我们只需像平时写C++代码一样定义变量、调用函数就可以很好的应用SSE指令了。
参考:http://blog.csdn.net/miss_acha/article/details/7403446
2)什么是“__m128”类型?----SSE算法标准的数据类型。
它的定义参考:http://blog.csdn.net/miss_acha/article/details/7403446
3)函数调用方式?----这实际上是在看网上的一篇相关文章时接触到的,就看了看,觉得还不错。
参考:http://blog.csdn.net/miss_acha/article/details/7402728
4)内存对齐问题是怎么回事?
参考:http://blog.csdn.net/miss_acha/article/details/7402896
5)memset函数的使用?
参考:http://www.iteye.com/topic/353769
参考:http://blog.csdn.net/yangsen2016/article/details/1638503
利用SSE编程之前该思考的几个问题
分类:并行开发技术--SSE2012-04-13 09:1515人阅读评论(0)收藏 举报
最近做的项目要用SSE优化,拿到手的程序跑下来要花一两个小时,运算量相当大,不得不优化。下面来谈一谈从这次项目中总结到的几点经验,主要是关于在利用SSE优化之前我们该思考的几个问题,也就是什么情况下才合适用SSE优化:
1)是否存在大量浮点型数据的运算(SSE指令包括,单指令多数据浮点计算、以及额外的SIMD整数和高速缓存控制指令)。
2)每个数据和对应的运算结果是否独立与其他数据(独立是最简单的情况,有些情况虽然不独立,但也可以通过一些方式做转换,但不是所有的地方都合适用SSE优化,具体要到实际问题中学习了)。
3)是否有尾数需要单独处理(如果要处理的数据个数不是4的倍数,最后肯定存在几个数据需要单独考虑,为了通用性,最好总是添加尾数处理)。
以上是在利用SSE优化之前需要考虑的几个问题,在具体使用SSE指令的时候还有一些技巧,这个将会在后续文章中提到,尤其是关于每个指令本身的耗时问题,我们要尽量避免使用太耗时的指令。