【问题标题】:Resorting bits of ulong in a specific order按特定顺序排列乌龙
【发布时间】:2019-11-21 15:13:04
【问题描述】:

对于计算机游戏,我使用存储在 ulong 中的位掩码(我希望我正确使用了这个术语)。 位掩码与其他位掩码交互,当另一个位掩码重叠时,将位的 1 变为 0。当与其他位掩码的交互结束时,我想一一检查特定位是否为1。但是,按照不遵循ulong中的顺序的特定顺序。

由于交互阶段的对称性原因,它们最初的排序方式不同,这使得预排序位掩码的旋转成为可能。

我记下了原始位的顺序。如果需要该命令来帮助我,这里是:

31, 39, 34, 27, 43, 30, 38, 26, 42, 33, 29, 37, 23, 47, 25, 41, 22, 46, 32, 28, 36, 21, 45, 24, 40, 20, 44, 48, 16, 49, 17, 50, 18, 51, 19, 12, 52, 8, 56, 13, 53, 4, 60, 0, 14, 54, 9, 57, 15, 55, 5, 61, 1, 10, 58, 6, 62, 11, 59, 2, 7, 63, 3

(例如第一个数字是31。也就是说,预排序的ulong的第31位(从0开始计数)要进入排序后的ulong的第一位。那么第39位的值就变成了进入排序后的第二个 ulong 等等)

我的游戏代码的这一部分对性能最关键,因此我寻找最有效的方法来执行此操作。它与人群机制有关,这段代码的效率越高,我可以并且将包含的人群成员越多。

我也愿意使用低 MB 大小的查找表。我对此的一个想法是将 ulong 分成四个短裤,将它们放入一个查找表中,每个都返回一个排序的短裤。但由于需要在这四个块之间移动很多位,而不仅仅是在这四个块内移动,仅此一项是行不通的。

我的另一个想法是:

ulong unsorted = x;
ulong sorted = 0;
sorted += (unsorted & 0x0001000010000000) >> 18 //bits that are shifted the same manner
//go on

【问题讨论】:

  • 您可以将bitmask % 2 存储在一个数组中,并在一个循环中设置bitmask = bitmask / 2,然后使用您的索引访问该数组(如果需要,在您将最后一位存储在数组的第一个位置时进行还原)

标签: c# sorting bit-manipulation


【解决方案1】:

一些初步说明:

  • 您的示例数据只有 63 个值。出于以下代码示例的目的,我将35 的缺失值任意添加到列表末尾。
  • 您的问题中没有详细说明您最初是如何陷入这种混乱局面的。不言而喻,但我还是要提一下:最佳 解决方案是完全消除问题的解决方案。在您的确切情况下这是否可能,更不用说如何做到这一点,我不能说,因为问题中缺少细节。

说了这么多……

我的另一个想法是:

ulong unsorted = x;
ulong sorted = 0;
sorted += (unsorted & 0x0001000010000000) >> 18 //bits that are shifted the same manner
//go on

这个想法可能行得通。但是你没有在你的问题中提供足够的细节让其他人知道,因为我们没有任何关于为什么这些位被重新排序的信息。您似乎在考虑编写特殊情况代码以利用最初导致位排序的任何模式,但是如果没有关于该模式的具体细节,没有其他人可以充实这个想法。

我的一个想法是将 ulong 分成四个短裤,将它们放入一个查找表中,每个查找表返回一个排序的短裤。但由于需要在这四个块之间移动很多位,而不仅仅是在这四个块内移动,仅此一项是行不通的。

这个想法也会奏效。你是对的,你不能映射到 short 值(甚至是 ushort)。但您可以映射到ulong 值,然后组合结果。

当然,对于任何代码来说,首要的也是最重要的事情是让它工作。所以恕我直言,从一个可以验证的更简单的实现开始是有意义的。完成后,您就可以继续寻找替代方案了。

这是一种选择:

static readonly int[] bitOrder = // NOTE: 35 not in original example, added here at end arbitrarily
{                                // so that a valid implementation can be demonstrated
    31, 39, 34, 27, 43, 30, 38, 26, 42, 33, 29, 37, 23, 47, 25, 41,
    22, 46, 32, 28, 36, 21, 45, 24, 40, 20, 44, 48, 16, 49, 17, 50,
    18, 51, 19, 12, 52,  8, 56, 13, 53,  4, 60,  0, 14, 54,  9, 57,
    15, 55,  5, 61,  1, 10, 58,  6, 62, 11, 59,  2,  7, 63,  3, 35
};

private static ulong[] BuildBitwiseMapping()
{
    ulong[] bitwiseMapping = new ulong[bitOrder.Length];

    for (int i = 0; i < bitwiseMapping.Length; i++)
    {
        bitwiseMapping[i] = 1UL << bitOrder[i];
    }

    return bitwiseMapping;
}

private static ulong SortBits(ulong[] bitwiseMapping, ulong value)
{
    ulong sorted = 0, mask = 1;

    for (int i = 0; i < bitwiseMapping.Length; i++, mask <<= 1)
    {
        if ((value & bitwiseMapping[i]) != 0)
        {
            sorted |= mask;
        }
    }

    return sorted;
}

您将首先调用BuildBitwiseMapping(),然后将返回的数组用于每次调用SortBits()。当然,您可以将构建的数组合并为 static readonly 字段,以简化对 SortBits() 的调用。

这必须为每个位循环一次,这不一定是最有效的。但除了循环条件之外,它严格来说只是查找和按位或运算,因此已经相当快了。

如果您想使用查找表的想法来改进这一点,这很简单:

private static ulong[][] BuildWordwiseMapping(ulong[] bitwiseMapping)
{
    ulong[][] result = new ulong[4][];
    ulong increment = 1;

    for (int i = 0; i < result.Length; i++)
    {
        result[i] = new ulong[ushort.MaxValue + 1];
        ulong value = 0;

        for (int j = 0; j <= ushort.MaxValue; j++)
        {
            result[i][j] = SortBits(bitwiseMapping, value);
            value += increment;
        }

        increment = value;
    }

    return result;
}

private static ulong SortBits(ulong[][] wordwiseMapping, ulong value)
{
    ulong sorted = 0;

    for (int i = 0; i < wordwiseMapping.Length; i++, value >>= 16)
    {
        int ushortPart = (int)(value & 0xffff);

        sorted |= wordwiseMapping[i][ushortPart];
    }

    return sorted;
}

上面从第一个示例中的 64 元素逐位表开始,并使用它构建了四个更大的表,可用于逐字查找。

您需要四个不同的表,一个用于原始ulong 值的每个位子集,因为当然它们每个映射不同。它们可以分布在整个 ulong 结果中,因此表格元素大小仍然需要为 ulong

这为您提供了四个表,每个表有 65536 个元素,每个元素长 8 个字节,总内存占用为 2MB。另一方面,虽然更简单的逐位版本需要对每个值进行 64 次循环迭代,但逐字版本只需要 4 次。这可以大大提高您的吞吐量。或者它可能对整体影响不大;这实际上取决于这段代码到底有多少瓶颈,以及四表版本的速度有多快。

您可能会注意到,从技术上讲,值 0 实际上并不需要在每个表中处理,因为不会为这些值设置任何位,无论它们出现在原创ulong。但实际上比较0 并处理这种特殊情况,然后只查找 NOP“零映射到零”值可能会花费更多的性能。

【讨论】:

    【解决方案2】:

    执行此排列的另一种方法是:

    ulong permute(ulong x)
    {
        x = bit_permute_step(x, 0x00000000c539d22d, 32);
        x = bit_permute_step(x, 0x0000cbc10000749e, 16);
        x = bit_permute_step(x, 0x0039007e00a9007f, 8);
        x = bit_permute_step(x, 0x06010e04070e0507, 4);
        x = bit_permute_step(x, 0x2113320311022110, 2);
        x = bit_permute_step(x, 0x5401151151100141, 1);
        x = bit_permute_step(x, 0x0032323110011333, 2);
        x = bit_permute_step(x, 0x030b0a0b080e0d00, 4);
        x = bit_permute_step(x, 0x00a5009500220097, 8);
        x = bit_permute_step(x, 0x00005cd6000043cc, 16);
        x = bit_permute_step(x, 0x00000000b3f003f3, 32);
        return x;
    }
    

    其中bit_permute_step定义为:

    ulong bit_permute_step(ulong source, ulong mask, int shift)
    {
        ulong t = ((source >> shift) ^ source) & mask;
        return (source ^ t) ^ (t << shift);
    }
    

    该结构是一个完整的 Beneš 网络,有 11 个步骤(6 * 2 - 1,其中 6 是 64 的以 2 为底的对数)。这是一种非常通用的技术,可以处理任何排列,在许多情况下有更好的特殊情况技术,但我找不到适合这种情况的技术。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-03-28
      • 2014-08-17
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多