zoukankan      html  css  js  c++  java
  • (转)【D3D11游戏编程】学习笔记十三:内存对齐的一点思考

    (注:【D3D11游戏编程】学习笔记系列由CSDN作者BonChoix所写,转载请注明出处:http://blog.csdn.net/BonChoix,谢谢~)

           不知你是否还有印象,在上一篇中提到三种光源的结构体时,无论是C++中的定义还是HLSL中的定义,都存在着名为"unused"的成员(平行光和点光源)。如下为C++程序中对平行光的定义:

    [cpp] view plain copy
    1. //平行光  
    2. struct DirLight  
    3. {  
    4.     XMFLOAT4    ambient;    //环境光  
    5.     XMFLOAT4    diffuse;    //漫反射光  
    6.     XMFLOAT4    specular;   //高光  
    7.   
    8.     XMFLOAT3    dir;        //光照方向  
    9.     float       unused;     //用于与HLSL中"4D向量"对齐规则匹配  
    10. };  

           显然,从名字上可以直接看出,这些成员没有任何意义,我们仅仅用这些成员来填充内存,以满足我们特定的内存对齐要求。这就涉及到HLSL中特殊的内存对齐的规则。

    注意区别C++程序中的”内存“与HLSL程序的”内存“。

           1. HLSL中的内存对齐

           先来了解一下HLSL中的对齐要求:在HLSL中,内存布局是以"4D向量“为单位的,所有的数据类型都必须位于一个“4D向量”对应的内存当中,且同一个数据不允许跨越两个4D向量而存放。

           这样的表述可能比较晦涩,通过下面的例子来理解一下:

           考虑这样一个结构体:

    [cpp] view plain copy
    1. struct Test  
    2. {  
    3.     float3  f1;  
    4.     float3  f2;  
    5. };  

           这里包含两个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内存空间。

           再看另一个例子:

    [cpp] view plain copy
    1. struct Test2  
    2. {  
    3.     float3  f1;  
    4.     float   f2;  
    5.     float2  f3;  
    6.     float3  f4;  
    7. };  

           同样,按照上面的规则,该结构中成员的内存布局如下:

           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中对应的结构:

    [cpp] view plain copy
    1. struct Hlsl  
    2. {  
    3.     float3  f1;  
    4.     float3  f2;  
    5. };  

           C++中对应的结构:

    [cpp] view plain copy
    1. struct Cpp  
    2. {  
    3.     XMFLOAT3   f1;  
    4.     XMFLOAT3   f2;  
    5. };  

           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++中正确的定义方式应该为:

    [cpp] view plain copy
    1. struct Cpp  
    2. {  
    3.     XMFLOAT3    f1;  
    4.     float       unused1;  
    5.     XMFLOAT3    f2;  
    6.     float       unused2;  
    7. };  

           这时的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++的结构体例子来进一步理解。

           考虑如下结构:

    [cpp] view plain copy
    1. struct Test  
    2. {  
    3.     char c1;  
    4.     int  i1;  
    5. };  

           该结构中char类型的c1满足对齐要求,int型的i1为了满足4字节对齐,则c1和i1之间会空出3个字节的无用空间。因此,该结构大小sizeof(Test)为8,而不是5!

           再考虑这个例子:

    [cpp] view plain copy
    1. struct Test  
    2. {  
    3.     int  i1;  
    4.     char c1;  
    5. };  

           这时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个字节的大小。

          

           了解这些内存对齐的要求在游戏编程中很重要。比如在定义类或结构体时,时刻考虑各成员的对齐要求来安排成员的声明顺序,可以尽可能地减小类的内存占用量。比如下面这两个结构,其意义完全一样,只是各成员声明顺序有所区别:

    [cpp] view plain copy
    1. struct Test1  
    2. {  
    3.     char    c1;  
    4.     int     i1;  
    5.     char    c2;  
    6.     float   f1;  
    7. };  
    8. struct Test2  
    9. {  
    10.     int     i1;  
    11.     float   f1;  
    12.     char    c1;  
    13.     char    c2;  
    14. };  

           尽管如此,其造成的两个结构的大小差别却相当大。按照对应要求,sizeof(Test1)为16字节,而sizeof(Test2)仅仅为12个字节,相差4个字节!游戏中无论是速度还是空间,都是相当重要的因素,因此类似这样的问题,只要简单安排下成员顺序就可以有效地提升性能,为什么不做呢?

           C++程序中,编译器默认会自动实现内存对齐要求,因此上面所有的例子都是默认情况下的结果。当然可以通过一定手段取消这个默认的行为,这时程序依然可以正常运行,但是运行速度却会大打折扣。在《Game Coding Complete, 4rd Edition》(第3版应该也有,国内有中文版了)一书中,作者在第3章专门针对满足对齐要求和不满足对齐要求的同一个结构,其运行时间进行了测试,从而印证了内存对齐的重要性。

           此外,在HLSL中,对内存对齐的要求是十分严格的,不满足的程序会出错。因此,我们在C++程序中必须考虑HLSL中的对齐要求,从而正确地设计相应的结构/类。

           本文完

  • 相关阅读:
    【LeetCode】731. 图像渲染
    【LeetCode】130. 被围绕的区域
    小白之HTTP协议熟悉篇
    Java和js常用表达式
    Mysql主主复制高可用解决方案
    解决vue报错:Module build failed (from ./node_modules/_eslint-loader@2.2.1@eslint-loader/index.js): TypeError: Cannot read property 'range' of null
    centos7使用yum安装jdk并配置jdkhome
    阿里nacos安装及使用指南
    js实现给html固定区域增加水印
    MySQL安装教程
  • 原文地址:https://www.cnblogs.com/wodehao0808/p/6603916.html
Copyright © 2011-2022 走看看