将数据存储在长数字或 class 实例中以获得更好的性能
Store data inside long number or class instance for better performance
我正在为我的益智游戏编写 AI,我面临以下情况:
目前,我有一个 class、Move
,代表我游戏中的一个动作,它具有与国际象棋类似的逻辑。
在 Move
class 中,我存储了以下数据:
- 移动玩家颜色。
- 动片
- 棋盘上的原点位置
- 棋盘上的目标位置。
- 执行此步棋被杀死的棋子(如果有的话)。
- 移动得分。
此外,我得到了一些描述move的方法,如IsResigned
、Undo
等
我的 AI 正在传递此着法实例,它基于 Alpha Beta 算法。因此,移动实例被传递了很多次,我在 AI 实现中构建了许多 Move
class 实例。因此,我担心它可能会对性能产生重大影响。
为了降低性能影响,我想到了以下解决方案:
我不会使用 Move
class 的实例,而是将我的整个移动数据存储在一个长数字中(使用按位运算),然后根据需要提取信息。
例如:
- 玩家颜色将来自位 1 - 2(1 位)。
- Oirign 位置将从位 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);
}
使用这种方法,我可以将长数字传递给 AI,而不是 class 个实例。
然而,我得到了一些奇迹:撇开程序增加的复杂性不谈,class 实例在 C# 中通过引用 传递(即通过发送指向该地址的指针)。那么我的替代方法是否有意义?情况更糟,因为我在这里使用长数字 (64bis),但指针地址可能表示为整数(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
一样)只有在您创建大量临时值时才值得,这些临时值很快就会被丢弃。然后你仍然需要通过个人资料确认你的表现是否确实得到了改善。
我正在为我的益智游戏编写 AI,我面临以下情况:
目前,我有一个 class、Move
,代表我游戏中的一个动作,它具有与国际象棋类似的逻辑。
在 Move
class 中,我存储了以下数据:
- 移动玩家颜色。
- 动片
- 棋盘上的原点位置
- 棋盘上的目标位置。
- 执行此步棋被杀死的棋子(如果有的话)。
- 移动得分。
此外,我得到了一些描述move的方法,如IsResigned
、Undo
等
我的 AI 正在传递此着法实例,它基于 Alpha Beta 算法。因此,移动实例被传递了很多次,我在 AI 实现中构建了许多 Move
class 实例。因此,我担心它可能会对性能产生重大影响。
为了降低性能影响,我想到了以下解决方案:
我不会使用 Move
class 的实例,而是将我的整个移动数据存储在一个长数字中(使用按位运算),然后根据需要提取信息。
例如: - 玩家颜色将来自位 1 - 2(1 位)。 - Oirign 位置将从位 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);
}
使用这种方法,我可以将长数字传递给 AI,而不是 class 个实例。 然而,我得到了一些奇迹:撇开程序增加的复杂性不谈,class 实例在 C# 中通过引用 传递(即通过发送指向该地址的指针)。那么我的替代方法是否有意义?情况更糟,因为我在这里使用长数字 (64bis),但指针地址可能表示为整数(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
一样)只有在您创建大量临时值时才值得,这些临时值很快就会被丢弃。然后你仍然需要通过个人资料确认你的表现是否确实得到了改善。