我正在使用Box2D在Java中创建一个物理游戏。
我正在编写一个AI类,并希望确保我的数据存储尽可能高效,同时考虑到内存对齐。
哪怕是最小的增加也可能会产生巨大的差异,因为我实际上是在运行’尽可能多的AI对象’,直到系统变慢。程序已经在碰撞检测上使用了大量的内存,因为我再次希望能够支持尽可能多的代理。
据我所知,Java中最小的类型是8字节,并且对象会被填充到8的倍数。我已经将我的AI控制结构化为布尔数组,代表移动:x+/-1,y+/-1,以及某些固定装置的顺时针/逆时针旋转。
由于Java没有布尔值的null设置,我将控制嵌套在命令对象中,布尔值为on_off和pos_neg。对于移动和旋转,我处理的是每个’默认’动作大约7个命令对象,(例如向右移动)。所以我为每个动作创建了命令数组。
我的问题是:我这样做是否高效?
我还没有最终确定设计,所以我不确定每个数组的大小。但是,考虑到内存对齐要求,我猜测我至少会有一些填充,这最终是浪费的内存。我正在考虑做一些事情,比如将对象大小调整到填充限制,然后将多个对象的剩余数据推入一个’溢出’对象…或者类似的东西。
这会加速吗?为什么会或不会?
我也在考虑使用位集,尽管我认为我的命令对象可能已经达到了类似的结果,而且我被告知位移操作很慢。
public class Command { boolean on_off = false; boolean pos_neg = false;}public class DefaultMoves { //注意:这些数组是2D的,以防需要多个来完成一个动作,我可能会稍后更改。 Command[][] mvRight = { { new Command(false, false), // new Command(false, false), // new Command(false, false), // new Command(false, false), // new Command(false, false), // new Command(true, true), //moveX new Command(false, false) // }, }; Command[][] mvLeft = { { new Command(false, false), // new Command(false, false), // new Command(false, false), // new Command(false, false), // new Command(false, false), // new Command(true, false), //moveX new Command(false, false) // }, };}
回答:
这本来只是一个评论,但有点长了,我不想把它写成3个评论。
由于这是另一个问题的后续问题,我会建议“不要担心填充”。你应该关心的是如何存储你的数据。
而且,如果你要担心你的东西占用了多少空间,不如分配一个包含7个对象的数组,而不是7个独立的对象。我相信Java在每次分配时都有开销。在典型的C或C++实现中,每次使用new
或malloc
进行分配时,会占用16-32字节的额外空间,并且大小会四舍五入到16或32字节。在Java中,这里有一个建议这里表明对象的内存开销是8字节 – 这可能并不适用于所有Java虚拟机和实现。
此外,所有的空间和时间优化几乎总是在空间和时间之间进行妥协,因此以更紧凑的形式存储数据将会在时间上付出代价,以节省空间。例如,我可以想到将on_off
和pos_neg
的对作为一个较大整数结构中的两个位来存储。所以你的7个命令将存储在一个整数中。然而,现在你必须进行移位和掩码操作才能获取正确的数据。同样,如果你要存储一些东西,你需要进行移位和或操作。(我用C语言写这个,因为我不太熟悉Java)。
/* 这足以容纳16对on_off和pos_neg *//* 在一对位中,位0 = on_off,位1 = pos_neg */uint32_t cmdData;/* 获取元素编号n的on_off和pos_neg的值 */void getData(int n, bool& on_off, bool& pos_neg){ uint32_t shifted = cmdData >> (2 * n); on_off = (shifted & 1) != 0; pos_neg = (shifted & 2) != 0;}/* 存储元素n的值 */void setData(int n, bool on_off, bool pos_neg){ uint32_t bits = (int)on_off + (2 * (int)pos_neg); uint32_t mask = 3 << (n * 2); cmdData &= ~mask; /* 清除位 */ cmdData |= bits << (n * 2);}
如你所见,这更有效地存储了数据,因为我们可以在4字节中存储16对{on_off, pos_neg}
,而不是每个(可能)占用一个字节。但每次获取数据时,你必须进行一些额外的操作(而且代码会变得更混乱)。这是否“值得拥有”高度依赖于具体情况,你访问这些数据的频率与系统的内存使用情况(假设这些对象的内存使用是导致问题的根源 – 如果你有100个命令结构和40000000个使用这些命令的对象,那么命令就不是问题所在)。
我存储方向/移动命令的方式可能是两个整数值(如果空间紧张,则使用int8_t
[byte
在Java中]),例如,移动右或下时为+1
,移动左或上时为-1
。这不是最紧凑的形式,但它便于访问和计算新位置。
然后可以使用这对值来描述所有可能的方向:
struct direction{ int x, y;};direction directions[] ={ { 0, 0 }, // 不移动。 { 0, 1 }, // 右。 { 0, -1 }, // 左。 { 1, 0 }, // 下。 { -1, 0 }, // 上。 };
如果你还想斜向移动,你需要再添加四个组合{ 1, 1 }, {-1, 1}
等。
同样的方法可以应用于可以移动的对象,作为一对xDir, yDir
值。
但关键在于,你首先需要很好地理解什么更重要:空间还是计算。从中找出哪些对象占用了大部分空间(哪些数量最多)。调整你只有一个或几十个的对象大小不会有太大不同,而你有数百万个的对象则会。如果空间不是问题(说实话,在一个拥有千兆字节RAM的系统中,很难编写出有效使用大量数据的代码 – 如果你对每个对象每帧都做一些事情,通常是CPU先耗尽速度,而不是内存耗尽)。
手提箱类比:
想象你有一个手提箱,它的宽度刚好可以容纳4个小盒子(长度可以是任意数量 – 这是一个奇怪的手提箱!),你有更大的盒子,它们是1、2、3或4个小盒子的单位。这些盒子是用“魔术贴”做的,所以它们可以粘在一起,并且可以按你喜欢的方式分开,但你必须跟踪哪些属于一起,每次你“分开”或“重新组合”这些单位时,都需要额外的时间。
如果你想偷懒并简化操作,你只需将你的三盒单位放入手提箱,每个旁边留一个空位。
1 2 3 4 a a a x b b b x c c c x d d d x
依此类推。
如果你想紧密打包,你可以拿一个3单位的盒子,然后切下一单位的下一个盒子,放在第一个旁边,然后将剩余的两单位放在下一个空间,然后从下一个盒子中切下一块两单位的,放在第二个包旁边,依此类推。
1 2 3 4a a a bb b c cc d d d
现在,你使用了25%的空间来存储它们,但你花时间分开它们,并且你需要花时间再次将它们取出成三单位的形式,当你稍后需要使用这些数据时。
现在,想象你因为放东西进手提箱而获得报酬,你按放入的物品数量获得报酬,你会选择哪种方法?
然后考虑到你不是按物品数量获得报酬,而是要为手提箱空间付费(因为你是公司的主人)。现在你想尽可能多地塞进空间。但这需要额外的时间,对吗?如果手提箱空间昂贵,那么这可能是值得的。如果不是那么昂贵,你可能更愿意节省时间而不是空间。这是一个妥协。
[我们可以用更现实的8、32或64单位做同样的事情,但我认为这只会使它更难读,并且肯定会使它更难输入]