将数据存储在长数字或 class 实例中以获得更好的性能

Store data inside long number or class instance for better performance

我正在为我的益智游戏编写 AI,我面临以下情况: 目前,我有一个 class、Move,代表我游戏中的一个动作,它具有与国际象棋类似的逻辑。 在 Move class 中,我存储了以下数据:

此外,我得到了一些描述move的方法,如IsResignedUndo

我的 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 位)——因此它甚至可能比我当前的实现具有最差的性能。

您对这种替代方法有何看法?

这里有几点要说:

  1. 你真的有性能问题吗(你确定内存使用是原因)? .net 中新实例的内存分配非常便宜,通常您不会注意到垃圾回收。所以你可能找错了树。
  2. 当你传递一个引用类型的实例时,你只是在传递一个引用;当您存储引用类型时(例如在数组中),您将只存储引用。因此,除非您创建大量不同的实例或将数据复制到新实例中,否则传递引用不会增加堆大小。所以传递引用可能是最有效的方法。
  3. 如果你创建了很多副本并很快丢弃它们并且你害怕内存影响(同样,你是否面临实际问题?),你可以创建值类型(struct而不是class).但是您必须了解值类型语义(您总是 处理副本)。
  4. 您不能依赖 32 位的引用。在 64 位系统上,它将是 64 位。
  5. 我强烈建议不要将数据存储在整型变量中。它使您的代码更难维护,并且在大多数情况下不值得进行性能权衡。除非你遇到严重麻烦,否则不要这样做。
  6. 如果您不想放弃使用数值的想法,请至少使用一个 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 一样)只有在您创建大量临时值时才值得,这些临时值很快就会被丢弃。然后你仍然需要通过个人资料确认你的表现是否确实得到了改善。