我在为我的拼图游戏编写AI时遇到了以下情况:目前,我有一个Move
类,用于表示游戏中的一个移动,其逻辑类似于国际象棋。在Move
类中,我存储了以下数据:
- 移动玩家的颜色。
- 移动的棋子。
- 棋盘上的起始位置。
- 棋盘上的目标位置。
- 通过此移动被击杀的棋子(如果有)。
- 移动的分数。
此外,我还有一些描述移动的方法,例如IsResigned
、Undo
等。
这个移动实例在我的AI中被传递,AI基于Alpha Beta算法。因此,移动实例被多次传递,并且在AI实现过程中构造了许多Move
类实例。因此,我担心这可能会对性能产生重大影响。
为了减少性能影响,我考虑了以下解决方案:不使用Move
类的实例,而是将整个移动数据存储在一个长数字中(使用位操作),然后根据需要提取信息。
例如:- 玩家颜色将从第1位到第2位(1位)。- 起始位置将从第2位到第12位(10位)。依此类推。
请看这个例子:
public long GenerateMove(PlayerColor color, int origin, int destination) { return ((int)color) | (origin << 10) | (destination << 20);}public PlayerColor GetColor(long move) { return move & 0x1;}public int GetOrigin(long move) { return (int)((move >> 10) & 0x3f);}public int GetDestination(long move) { return (int)((move >> 20) & 0x3f);}
使用这种方法,我可以只传递长数字,而不是类实例。然而,我有一些疑问:撇开程序增加的复杂性不谈,类实例在C#中是按引用传递的(即发送到该地址的指针)。那么我的替代方法是否有意义?情况甚至更糟,因为我在这里使用的是长数字(64位),但指针地址可能表示为整数(32位) – 所以它甚至可能比我当前的实现性能更差。
您对这种替代方法有何看法?
回答:
这里有几点需要说明:
- 您是否真的遇到了性能问题(并且确定内存使用是原因)?在.net中,为新实例分配内存非常便宜,通常您不会注意到垃圾回收。因此,您可能是在错误的方向上努力。
- 当您传递引用类型的实例时,您只是传递一个引用;当您存储一个引用类型(例如在数组中)时,您只会存储引用。因此,除非您创建了许多不同的实例或将数据复制到新实例中,否则传递引用不会增加堆大小。因此,传递引用可能是最有效的方法。
- 如果您创建了许多副本并很快丢弃它们,并且您担心内存影响(再次,您是否面临实际问题?),您可以创建值类型(使用
struct
而不是class
)。但您必须注意值类型语义(您总是在处理副本)。 - 您不能依赖引用是32位的。在64位系统上,它将是64位的。
- 我强烈建议不要将数据存储在整数变量中。这会使您的代码难以维护,在大多数情况下,这不值得性能上的权衡。除非您遇到了严重的问题,否则不要这样做。
- 如果您不想放弃使用数值的想法,至少使用一个
struct
,它由两个System.Collections.Specialized.BitVector32
实例组成。这是一个内置的.NET类型,它会为您执行掩码和移位操作。在该结构中,您还可以将访问值封装在属性中,这样您就可以将这种不寻常的存储值的方式与其他代码隔离开来。
更新:
我建议您使用性能分析器来查看性能问题所在。使用猜测来进行性能优化几乎是不可能的(而且肯定不是您时间的有效利用)。一旦您看到性能分析器的结果,您可能会对问题的真正原因感到惊讶。我打赌,内存使用或内存分配不是问题所在。
如果您最终得出结论,您的Move
实例的内存消耗是原因,并且使用小值类型可以解决问题(我会感到惊讶),不要使用Int64
,使用一个自定义结构(如第6点所述),它将与Int64
大小相同:
[System.Runtime.InteropServices.StructLayout( System.Runtime.InteropServices.LayoutKind.Sequential, Pack = 4 )]public struct Move { private static readonly BitVector32.Section SEC_COLOR = BitVector32.CreateSection( 1 ); private static readonly BitVector32.Section SEC_ORIGIN = BitVector32.CreateSection( 63, SEC_COLOR ); private static readonly BitVector32.Section SEC_DESTINATION = BitVector32.CreateSection( 63, SEC_ORIGIN ); private BitVector32 low; private BitVector32 high; public PlayerColor Color { get { return (PlayerColor)low[ SEC_COLOR ]; } set { low[ SEC_COLOR ] = (int)value; } } public int Origin { get { return low[ SEC_ORIGIN ]; } set { low[ SEC_ORIGIN ] = value; } } public int Destination { get { return low[ SEC_DESTINATION ]; } set { low[ SEC_DESTINATION ] = value; } }}
但请注意,您现在使用的是值类型,因此您必须相应地使用它。这意味着赋值会创建原始的副本(即更改目标值不会影响源),如果您希望子程序持久化更改,则使用ref参数,并避免任何代价的装箱以防止性能更差(有些操作可能意味着装箱,即使您不会立即注意到,例如将实现接口的struct
作为接口类型的参数传递)。使用结构(就像使用Int64
一样)只有在您创建了许多临时值并很快丢弃它们时才值得。然后,您仍然需要通过性能分析器确认您的性能实际上得到了改善。