c

时间:2015-10-01 08:59:35

标签: c optimization types int neural-network

我正在建立一个深度神经网络来玩连接4,它必须在非常有限的机器上与其他AI机器人竞争(不知道具体的限制,只是我只有几个核心和一个少量记忆)。因此,我希望以任何可能的方式优化我的训练集。它目前代表董事会中的州:

空白的

b(无件存在)

x表示“x”件存在

o表示“o”件存在

win获胜设置

loss丢失设置

draw用于绘制设置

基本上我正在尝试映射它,以便我的3位整数可以取代这些内存繁重的字符串。我考虑使用short,但它比16位的char差。我想像这样映射:

000 - > b

001 - > x

010 - > o

011 - > win

100 - > loss

101 - > draw

因为我可以在3比特的空间而不是字符中表示这些状态(每个字符8比特,哎呀!)我想尝试一下。但是我不知道如何在c中实例化这样的变量。

训练集长67557行,代表每行6x7板,后面有赢/输/抽奖子句。因此,每个字节保存5位将节省每行(5*6*7)+(5*1) = 215位和215*67557 = 14524755位整体(1.81 MB总共2.90 MB,整体空间减少62%)。

3 个答案:

答案 0 :(得分:7)

如果您使用bitfields呢?

sizeof(struct S) == 2

在大多数系统上,struct S myS; s.w = 0; // Okay s.x = 6; // Okay s.y = 8; // Will overflow/wrap-around (why do the standards differentiate between those two?) ,但你在其中包含 4 值!。

现在,你可以做点像......

struct S first;
struct S second;

,如果你做的话......

8 * 3 == 24; 24 % 8 == 0

你将失去你正在寻找的内存效率,因为编译器为两个对象提供一个地址,因此它们需要字节对齐,所以它们在一起(通常)保持32位,你可以在其中保存8个值,如果你有一个包含所有8个变量的结构,则内存使用量通常为24位。

请记住,保存使用所有可用空间的位域的结构(例如上面提到的8个成员:S)更适合您的目的,因为您可以拥有它们的数组,获得位域的好处,并在此过程中不浪费任何记忆。因此第一个结构是低效的,因为它为&s.x类型的每个对象浪费了4位。

BTW :请勿尝试使用sizeof(s.x)interp1d,因为显而易见的原因,它无法正常使用。

答案 1 :(得分:2)

如果机器是有限的并且您必须参加竞争(准时),那么您不希望在位域范围的整数上进行CPU繁重的操作。最好是使用本机机器字大小。接下来最好是使用字节大小的整数,因为许多机器对字节大小的实体进行了有效的操作。

始终是优化速度或内存使用的问题。

答案 2 :(得分:2)

你在这里有两三种不同的东西可以让你混淆。

  • 培训文件格式
  • 解析后训练集的内存存储格式(如果需要保留解析状态以供将来参考)
  • 单板状态的解压缩表示+可选? W / L / D标志

这三种格式都可以不同。培训文件可以是文本以便于编辑。当然,即使您的主程序以二进制格式读取训练集,该二进制文件也可以被编译为#34;通过一个单独的工具从一个易于编辑的文本格式。

使用单一董事会职位的内部代表:

这需要快速访问和循环。由于您正在训练神经网络,而不是直接编写AI,因此您可能不需要非常使用此表示法。如果您不需要做更多的事情而不是将每个元素应用于神经网络输入,那么就没有必要使用单独的格式:只需从更紧凑的表示中直接解包到神经网络输入。

但是,如果必须多次遍历单个板状态,则有一些有趣的选项。正如多人指出的那样,赢/输/抽奖/未决标志应该与董事会状态分开考虑。因此,每个电路板都有一个标志,而不是每个电路板位置的标志存储。

  • 位板:例如,我已经阅读了使用64位无符号整数来存储所有白色棋子的国际象棋引擎(例如狡猾)。您可以按位OR位图一起找到所有白色碎片的位置。

    位图(一个用于o,一个用于x)将记录整个状态。 connect-4板有6 * 7个网格位置,因此每个位图可以是64位,但32b太小。 popcount(board.o)告诉您电路板上有多少 o assert(o & x == 0)将是一个很好的理智检查,因为在同一位置永远不会有 o x

    在结构中使用两个打包的42b字段是个坏主意,因为加载/存储会很慢。即使将它们打包到48位字段(因此它们在字节边界上结束)也会导致加载/存储速度变慢。请记住,这是我们的快速格式。我们可以使用打包格式进行长期存储。

    board[0][0] && board[0][1] && board[0][2] && board[0][3](虽然不是那种语法)和编译时常量位置的东西在位板上非常快。一个按位AND只留下可能设置的那些位,然后你可以与掩码进行比较,看看 all 是否设置了这些位。要测试||而不是&&,请忽略第二步。您可以针对 o x 位图或o|x执行这些测试,以检查任何一种。但是,如果你必须在运行时从变量位置构建掩码,那么这并不高效。

    要扫描棋盘获胜,您可以检查左栏,然后对您的面具进行位移,以便检查下一栏。实际上,对这样的所有列进行强力检查可能比检查标记的邻居要慢,寻找2行的候选者。

    如果位图完全是64位,有些操作可能会更容易,代表8x8板,但实际上只使用左下角的7x6。这样,单独的列位于64位整数的单独字节中。将每个列放在一个单独的字节中可能比行更有用,因为在列中找到最高使用位置是您可能想要做的事情。它只是对列的find first set bit操作。从位图中提取8位块更快(不需要屏蔽)。但是,您可以解压缩42位位图以分隔每列的变量。在x86上,前4个寄存器是第一个和第二个 8位块的字节可寻址(AX(RAX的低16)由AL和AH组成),你(或编译器,但是他们可能不是很聪明)可以在4个寄存器中存储7列,并且仍然可以分别bsr(位扫描 - 反向)任何列。

// Sample bitboard implementation:
struct game_state {
    struct board_state {
       uint64_t o, x;
    } board;
    enum winlose { GAME_UNDECIDED=0, GAME_WIN_O, GAME_WIN_X, GAME_DRAW } victory;
};
  • 2位字段数组:不要使用。类似的实现将对每个位置使用2位位域。 It's impossible to use with nice board[row][col] syntax in C和42 * 2位不适合单个寄存器。交错位置没有任何优势,并使一些事情变得更糟,尤其是。因为整个东西不适合64位。 (如果你想在位板版本中查找未占用的空格,你会在o|x中寻找零位。在这里,你必须检查每对2位,而不是能够按位使用一位来适应整个但是,您可以创建一个宏来移动/屏蔽代表给定行/列的2位。它不会生成有效的代码。

  • 字节数组:使用此格式循环检查给定电路板位置的邻居可能会更快。在一个位板中,测试board[i][j] && board[i][j+1]可以通过对电路板进行位移来完成,使得两个感兴趣的位线,然后按位AND,然后对该位进行位测试。至少在x86上,存在具有小字节偏移的寻址模式,因此给定一个板位置的地址,并且另一个板位置可能只需要一条指令。

    在每个字节中,一位代表 x ,另一位代表 o ,如果位置是 x ,则应设置另一位 0 的。这允许通过将它们组合在一起来检查多个位置都被占用,并检查占用的位。否则,您必须检查每个网格中是否设置了 x o 位。

// Sample byte-array implementation:
enum boardpos {
    POS_EMPTY = 0,
    POS_O = 1<<0,
    POS_X = 1<<1,
    POS_OCCUPIED = 1<<3
};
// maybe #define for these constants instead?

struct game_state {
    struct board_state {
       uint8_t pos[6][7];
    } board;
    enum winlose { GAME_UNDECIDED=0, GAME_WIN_O, GAME_WIN_X, GAME_DRAW } victory;
    // or maybe stuff the winlose info into the high bits of board.pos[0][0]?
    // Not much point, since the struct will probably be the same size after padding anyway.
};

文件格式表示:

更紧凑但仍易于使用的格式为xbbb...ooxbbw。然后,您不必将行解析为字符串,就像43个字符的常量大小一样(如果每个记录由换行符分隔,则为43)。如果您有任何胜利,亏损或平局的董事会职位,请使用另一个角色来标记。空格,或'n'

只是遗漏了逗号,将你的体型减少了近一半。您不希望必须解析逗号和内容的输入。简单的游程编码符号可能会带来进一步的好处,例如xb2o1xb1w。看到一个数字意味着重复多次的最后一个字符。也许x表示一个x,大写X表示两个xes。这已经到了让人类难以阅读的程度。 LZOP或LZ4压缩可能很好地压缩东西。

二进制文件格式

显然,文本表示存在限制。固定大小的二进制记录可能非常小,因为没有太多信息需要存储。每个网格位置使用2位可能足够紧凑,但仍然存在冗余,因为它可以表示在同一位置的 x o 的不可能状态。为了做得更好,您需要将整个板的状态或整行或列映射到多位表示。 Wikipedia says所有游戏板有4,531,985,219,092个合法位置,填充0到42件。那刚好超过2 ^ 42。因此,43位应足以表示任何有效的电路板状态,包括所有尚未确定的位置。 IDK如何将游戏编码为43位整数,至少没有任何可用的东西(比实际列举所有可能的游戏并停在匹配的游戏更快)。

如果您使用位板作为内部快速表示,请以文件格式存储它们,因此ox板以及w / d / d状态适合12个字节,如果你喜欢圆形数字,则为16字节。

// do some pre-processor stuff to choose between GNU C __attribute__ ((__packed__))
// and the MSVC #pragma pack
struct __attribute__ ((__packed__)) compact_game_state {
    struct __attribute__ ((__packed__)) compact_board_state {
       uint64_t o:42, x:42;
    } board; // sizeof = 11
    uint8_t victory;
}; // sizeof = 12

struct semi_compact_game_state {
    struct __attribute__ ((__packed__)) semi_compact_board_state {
       uint64_t o:48, x:48;
    } board; // 96 bits = 12 bytes
    enum winlose victory; // another 4 bytes
};

这些实际上是用g ++编译的:see on godbolt

使用endian-agnostic code 执行您的I / O,因此它不会在大端机器上中断。它是一种文件格式,因此只要正确的字节位于正确的位置,它实际上并不重要。 Little-endian可能是文件格式的一个很好的选择,因此在little-endian机器上,加载/存储代码是无操作的。或者只是懒惰并在结构数组上执行二进制I / O,并且只在具有与训练数据集相同的字节顺序的机器上使用您的代码。

如果您不使用位板,最好使用2位字段数组。随机访问可能很慢,但将其转换为字节数组可能比两个单独的位域更快。屏蔽低2位,将其用作{ POS_EMPTY, POS_O|POS_OCCUPIED, POS_X|POS_OCCUPIED }查找表的索引。然后按位移2,使下一个字段进入低位。该板需要84位,因此可以使用单独的32位或64位块。不需要进行128位双移。赢/输/抽奖信息可以输入最终的2位数据块。

将其打包成三个uint32_t的12字节结构,或uint64_t和uint32_t等。或者只是一个uint8_t数组,但是让编译器做一个宽负载更难。您可以将内容打包成一个11字节的结构,但随后对齐更是一个问题。如果保存1/12内存大小对缓存有用,那就去吧。跨越缓存行的负载在x86 CPU上只需要几个额外的周期,而且您不经常加载。 (第一块的64位负载无论如何都不会与64b对齐,有12B结构,但它至少会在中间分开,这是一种比不均匀缓存更快的特殊情况 - 在某些CPU上拆分行。)

我认为将单独的位板解码为字节数组需要分别移动每个位板。它仍然可以是无分支的,但不是很好。

游戏数据库的内存存储

表示之间的转换需要CPU时间,因此如果它没用,只需从文件格式转到内部。

如果您保留文本文件格式并使用非紧凑快速格式,则可能只有一个单独的格式。在这种情况下,将文件解析为打包的2位位置(如该文件格式,见上文)。

如果您的文件格式是二进制文件,请保留它。 (甚至只是对文件进行内存映射。)

相关问题