【D3D11游戏编程】学习笔记十三:内存对齐的一点思考

(注:【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中的对齐要求,从而正确地设计相应的结构/类。

本文完

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值