【问题标题】:Memory efficient AI objects for physics game物理游戏的内存高效 AI 对象
【发布时间】:2016-02-23 03:07:47
【问题描述】:

我正在使用 box2d 在 java 中创建一个物理游戏。

我正在编写一个 AI 类,并希望确保我的数据尽可能高效地存储,同时考虑到内存对齐。

最小的增加可能会产生巨大的影响,因为我实际上是在运行“尽可能多的 AI 对象”,直到系统变慢。程序已经在碰撞检测中使用了大量内存,因为再一次,我希望能够支持尽可能多的代理。

到目前为止,我所了解的是,最小的 Java 类型是 8 字节,并且对象被填充为 8 的倍数。我在布尔数组中构建了我的 AI 控件,表示运动:x+/-1,y+/-1 ,以及某些灯具的顺时针/CCW 旋转。

由于 Java 没有布尔值的空设置,我将控件嵌套在命令对象中,布尔值 on_off 和 pos_neg。通过移动和旋转,每个“默认”操作(例如向右移动)处理大约 7 个命令对象。所以我为每个动作创建命令数组。

我的问题是:我这样做有效率吗?

我还没有完成设计,所以我不确定每个数组的大小。但是,考虑到内存对齐要求,我猜我至少会有 一些 填充,这最终会浪费内存。我正在考虑做一些事情,比如削减对象大小以适应填充限制和然后将剩余数据从多个对象推送到“溢出”对象中......或类似的东西。

这会加快速度吗?为什么或为什么不?

我也在考虑使用位集,尽管我认为我的命令对象可能已经达到了类似的结果,而且有人告诉我位移很慢。

public class Command {

        boolean on_off  = false;
        boolean pos_neg = false;
}

public class DefaultMoves {

    //Note: these arrays are 2d in the event multiples are necessary
    //to complete a single action, I may change this later.
    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)  //   
              },   
        };
}

【问题讨论】:

  • 为了提高您的描述的清晰度(并更有效地吸引答案),请在您的问题中包含一些代码 sn-ps 以说明您的观点。

标签: java object memory-management artificial-intelligence memory-alignment


【解决方案1】:

这只是一个评论,但有点冗长,我不想把它写成 3 cmets。

由于这是另一个问题的后续问题,我将从“停止担心填充”开始。担心您如何存储您的数据。

而且,如果您要担心您的东西占用多少空间,请分配一个由 7 个对象组成的数组,而不是 7 个单独的对象。我确信 Java 在每次分配中都有开销。在典型的 C 或 C++ 实现中,newmalloc 的每个分配占用超过实际分配数据大小的 16-32 个字节,并且大小四舍五入为 16 或 32 个字节。在 java 中,有一个建议 here 对象的内存开销是 8 个字节 - 这可能不适用于所有 Java VM 和实现。

此外,所有空间和时间优化都是空间和时间之间的妥协[几乎总是至少],因此以更紧凑的形式存储数据将花费时间以节省空间。例如,我可以认为将 on_offpos_neg 对作为较大整数结构中的两个位。因此,您的 7 个命令将存储在一个整数中。但是,现在您必须进行转换和屏蔽才能获得正确的数据。同样,如果您要存储某些东西,则可以进行移位和 oring。 (我把它写成 C,因为我不太了解 Java)。

/* This is big enough for 16 pairs of on_off and pos_neg */
/* In a pair of bits, bit 0 = on_off, bit 1 = pos_neg */
uint32_t cmdData;

/* Get values of on_off and pos_neg for element number n */
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;
}

/* Store values for element 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; /* Clear bits */
    cmdData |= bits << (n * 2);
}

如您所见,这可以更有效地存储数据,因为我们可以将 16 对 {on_off, pos_neg} 存储在 4 个字节中,而不是每个(可能)占用一个字节。但是要达到每一个,你每次都必须做一些额外的操作(并且代码变得更加混乱)。这是否“值得拥有”很大程度上取决于情况,您访问这些的频率与系统的内存有多低(假设这些对象的内存使用是导致问题的原因 - 如果您有 100 个命令结构和40000000 个使用命令的对象,那么命令就不会成为问题)。

我将存储方向/移动命令的方式可能是两个整数值(int8_t [byte in java],如果空间很紧),持有+1 用于向右或向下移动, -1 向左或向上移动。这不是最紧凑的形式,但它便于访问和计算新位置。

这对可以用来描述所有可能的方向:

struct direction
{
     int x, y;
};

direction directions[] =
{
     { 0, 0 },    // Don't move.
     { 0, 1 },    // Right.
     { 0, -1 },   // Left.
     { 1, 0 },    // Down.
     { -1, 0 },    // Up.
 };

如果你也想沿对角线移动,你必须再添加四对,用{ 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单元的盒子,然后剪下一个的一个单元,贴在第一个的旁边,然后把剩下的两个单元放在下一个空间,然后剪一个从下一个中取出两个单元,并将其贴在第 2 包旁边,依此类推。

1 2 3 4
a a a b
b b c c
c d d d 

现在,您存储它们的空间减少了 25%,但您花了时间将它们拆分,当您以后需要使用数据时,您必须再次花时间将它们提取成三个单位。

现在,假设您将东西放入手提箱中获得报酬,并且您根据放入的物品获得报酬,您选择哪种方法?

然后考虑一下,您必须为行李箱空间付费,而不是按件付费(因为您是公司的所有者)。现在你想尽可能多地挤进空间。但这需要额外的时间,对吧?如果手提箱很贵,那么它可能是值得的。如果它不是那么昂贵,您可能更喜欢节省时间而不是空间。这是一种妥协。

[我们可以用更现实的 8、32 或 64 单位来做同样的事情,但我认为这只会使其更难阅读,而且肯定会更难打字]

【讨论】:

  • 很公平,我想我将不得不开始研究 CPU 效率的做法。虽然有人说 Java 是最好的,但你不要尝试手动优化。
  • 我认为您误解了:您需要确定您的问题是来自“使用大量内存”还是来自“耗尽所有 CPU 时间”。并且大多数具有任何形式优化的语言对于“自然代码”来说表现最好 - 换句话说,编译器/JIT 将更有可能识别它可以聪明的东西的正常版本,而不是一些“非常聪明”的解决方案。
  • 是的。但我仍然对编译器不填充填充感到困惑。
  • 填什么?填充的全部意义在于通过以易于访问的方式对齐数据来提高性能(有时也“不会崩溃”,具体取决于系统,但现代系统通常确实接受未对齐的数据,只是比对齐慢)。
  • 对,但是填充不应该包含实际的程序数据吗?插入空填充看起来效率不高吗?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-01-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-02-04
  • 1970-01-01
相关资源
最近更新 更多