(注:【D3D11游戏编程】学习笔记系列由CSDN作者BonChoix所写,转载请注明出处:http://blog.csdn.net/BonChoix,谢谢~)
不知你是否还有印象,在上一篇中提到三种光源的结构体时,无论是C++中的定义还是HLSL中的定义,都存在着名为"unused"的成员(平行光和点光源)。如下为C++程序中对平行光的定义:
//平行光
struct DirLight
{
XMFLOAT4 ambient; //环境光
XMFLOAT4 diffuse; //漫反射光
XMFLOAT4 specular; //高光
XMFLOAT3 dir; //光照方向
float unused; //用于与HLSL中"4D向量"对齐规则匹配
};
显然,从名字上可以直接看出,这些成员没有任何意义,我们仅仅用这些成员来填充内存,以满足我们特定的内存对齐要求。这就涉及到HLSL中特殊的内存对齐的规则。
注意区别C++程序中的”内存“与HLSL程序的”内存“。
1. HLSL中的内存对齐
先来了解一下HLSL中的对齐要求:在HLSL中,内存布局是以"4D向量“为单位的,所有的数据类型都必须位于一个“4D向量”对应的内存当中,且同一个数据不允许跨越两个4D向量而存放。
这样的表述可能比较晦涩,通过下面的例子来理解一下:
考虑这样一个结构体:
struct Test
{
float3 f1;
float3 f2;
};
这里包含两个float3成员,由于两个成员必须位于一个4D向量对应的内存中,实际的存放是这样的:
vector1: (f1.x, f1.y, f1.z, X)
vector2: (f2.x, f2.y, f2.z, X)
X表示内存当中空出来的一个float空间。这样,f1和f2正好位于两个4D向量中。可见,f1和f2并不是挨着存放的。设想,如果f2是接着f1存放的,则f2.x将位于f1.z后,与f1位于同一个4D向量当中,而f2.y, f2.z将位于另一个4D向量中,这样的话f2将位于两个4D向量中,根据上面的规则,显然是不允许的。因此,惟一的存放方式即上面所示,f1和f2后面各空出一个float内存空间。
再看另一个例子:
struct Test2
{
float3 f1;
float f2;
float2 f3;
float3 f4;
};
同样,按照上面的规则,该结构中成员的内存布局如下:
vector1: (f1.x, f1.y, f1.z, f2)
vector2: (f3.x, f3.y, X, X)
vector3: (f4.x, f4.y, f4.z, X)
现在来考虑与HLSL对应的C++程序。在C++中,内存对齐的规则与HLSL不一样。关于C++中内存对齐的规则在本文后面讨论。C++程序中并无按4D向量对齐的要求,这样,如果不显式地用额外的空间来满足与HLSL中完全相同的对齐方式,在C++程序中使用ID3DX11EffectVariable接口给Effect中的变量赋值时,会出现未定义的问题。
考虑下面的例子:
HLSL中对应的结构:
struct Hlsl
{
float3 f1;
float3 f2;
};
C++中对应的结构:
struct Cpp
{
XMFLOAT3 f1;
XMFLOAT3 f2;
};
Hlsl中成员的布局为:【f1.x, f1.y, f1.z, X, f2.x, f2.y, f2.z, X】(8字节)。Cpp中布局为:【f1.x, f1.y, f1.z, f2.x, f2.y, f2.z】(6字节)。这种情况下,如果在C++程序中使用Cpp结构来给Effect中Hlsl结构赋值,显然会出现问题,即f2.x会赋值到HLSL中f1.z后面空出的内存处,后面全部成员将会因为错位而发生错误的赋值!
因此,C++中正确的定义方式应该为:
struct Cpp
{
XMFLOAT3 f1;
float unused1;
XMFLOAT3 f2;
float unused2;
};
这时的Cpp的内存布局为:【f1.x, f1.y, f1.z, unused1, f2.x, f2.y, f2.z, unused2】。这样,f1和f2与HLSL中的f1和f2将会正好对齐,因而能够正确赋值。
实际上,如果仅仅是单个结构变量赋值的话,Cpp中最后的unused2是可以去掉的。因为这时f1和f2依然可以满足对齐。之所以在最后加上它,主要的好处就是它允许我们对该结构的数组进行赋值。设想C++程序中有数组 Cpp c[3], 用它来直接对HLSL中的数组Hlsl h[3]赋值的话,三个Cpp中的f1和f2都将能满足与HLSL的对齐。如果没有unused2,则从第二个Cpp开始,f1和f2将不再满足对齐,从而造成未定义的赋值结果。
这也就是为什么在定义平行光时,我们在最后加上了unused成员来对齐。在光照计算示例程序中,我们使用了三个平行光,放在一个数组当中,并直接对HLSL中对应的数组赋值。要想对结构的数组进行赋值,unused是必须的。如果仅仅对单个光源进行赋值,则unused可以省去。为了适用了更通用的情况,我们在末尾加上了unused。
此外应该注意,HLSL中内存对齐是强制性的,因此即使不用unused来显式对齐,系统也会自动加上去的。因此对于HLSL中平行光和点光源的定义,其实可以省略里面的unused。之所示加上去,只是为了更好地展示C++程序与HLSL程序的结构严格的对应关系。
2. C++中的内存对齐
下面来讨论普通C++程序中对内存对齐的要求。
CPU在读、写内存时,对于满足字节对齐要求的数据,其操作将会快很多。这就是为什么我们在写程序时特别需要了解内存对齐的有关规则。内存对齐在不同操作系统之间有略微的差别,我这里提到的以Windows为主。
Windows操作系统中,对于不同的内置类型,其字节对齐要求与该内置类型的大小有关。如果该类型占N字节,则其地址需要是N的倍数。因此,对于char类型变量,任何一个字节位置都可以;对于short,其地址需要是2的位数;对于int、float,其地址则应该为4的位数,依次类推。
下面通过C++的结构体例子来进一步理解。
考虑如下结构:
struct Test
{
char c1;
int i1;
};
该结构中char类型的c1满足对齐要求,int型的i1为了满足4字节对齐,则c1和i1之间会空出3个字节的无用空间。因此,该结构大小sizeof(Test)为8,而不是5!
再考虑这个例子:
struct Test
{
int i1;
char c1;
};
这时int型的i1满足4字节对齐,后面char型的c1显然也满足。i1和c1之间不再有未用空间。但是在c1末尾,依然会有3字节的无用空间来对齐,否则,正如上面刚提到的,如果以数组进行存放Test时,从第二个结构开始,i1将不满足4字节对齐。因此,这里的Test结构大小仍然为8!
根据上面所述,来总结下C++中的内存对齐要求:
1. 所有的内置类型在内存中的地址为该类型大小的倍数
2. 对于结构体或类,除了其各个成员遵循内存对齐要求外,该结构/类的的大小为其成员中占用字节最大的成员大小的整数倍。
第2点就解释了刚刚的例子中c1末尾添加3个对齐字节的原因:由于该结构中最大成员为int型的i1,其大小为4,因此整个结构大小需为4个整数倍,因此末尾添加3个字节以达到8个字节的大小。
了解这些内存对齐的要求在游戏编程中很重要。比如在定义类或结构体时,时刻考虑各成员的对齐要求来安排成员的声明顺序,可以尽可能地减小类的内存占用量。比如下面这两个结构,其意义完全一样,只是各成员声明顺序有所区别:
struct Test1
{
char c1;
int i1;
char c2;
float f1;
};
struct Test2
{
int i1;
float f1;
char c1;
char c2;
};
尽管如此,其造成的两个结构的大小差别却相当大。按照对应要求,sizeof(Test1)为16字节,而sizeof(Test2)仅仅为12个字节,相差4个字节!游戏中无论是速度还是空间,都是相当重要的因素,因此类似这样的问题,只要简单安排下成员顺序就可以有效地提升性能,为什么不做呢?
C++程序中,编译器默认会自动实现内存对齐要求,因此上面所有的例子都是默认情况下的结果。当然可以通过一定手段取消这个默认的行为,这时程序依然可以正常运行,但是运行速度却会大打折扣。在《Game Coding Complete, 4rd Edition》(第3版应该也有,国内有中文版了)一书中,作者在第3章专门针对满足对齐要求和不满足对齐要求的同一个结构,其运行时间进行了测试,从而印证了内存对齐的重要性。
此外,在HLSL中,对内存对齐的要求是十分严格的,不满足的程序会出错。因此,我们在C++程序中必须考虑HLSL中的对齐要求,从而正确地设计相应的结构/类。
本文完