寻找算法在bitset中找到n个未设置的位

时间:2014-11-25 15:55:52

标签: c algorithm

背景: 我正在编写一个分配4KiB块的物理内存分配器。它使用bitset来标记已使用的4KiB内存块。我没有可用的标准C库。

问题: 我正在寻找一种算法,它会在最小的间隙中找到n个连续的未设置位,这样我就可以留下未设置位的最大间隙。

实施例: 让我们说一个bitset包含以下几位:

 0010 0000 0111 0001 1100 0011

如果我想设置4位,算法应该返回第18位。

4 个答案:

答案 0 :(得分:2)

我认为你可以在2次通过中做到这一点:

传递#1:
扫描数组,注意连续的零位数,以及这些位的开始。

从您的示例中,扫描会产生:

2 bits, starting at 0
6 bits, starting at 3
3 bits, starting at 12
4 bits, starting at 18

传递#2:
扫描传递#1中的数据,查找目标值(4)或大于目标的最小值。

用C语言编写这两个过程似乎都很简单,这应该是一个适用于所有情况的通用解决方案。

在你开始工作之后,我也会看到一些优化,以便在某些情况下你可能根本不需要运行#2#。

答案 1 :(得分:1)

正如我在评论中提到的那样,当你试图处理合并时,情况会完全不同。但要解决你现在要问的问题。使用Red Black Tree非常简单,我习惯称之为RBTree

有数千个RBTree实施,所以你可以选择一个适合你的语言。我只使用类似python的伪代码提供allocate内存的方式。 (正如我所说,如果你在释放记忆时试图解决问题,那就是一个不同的问题。)

RBTree:

key:0&#39的数量。

值:连续0的第一个位置。

所以在你的情况下,你的问题应该被初始化为:

rbt=new RBTree()
rbt.insert(2, 0)
rbt.insert(6, 3)
rbt.insert(3, 12)
rbt.insert(4, 18)
如果我错了,请原谅我。

你想分配一块内存的时间:

func alloc(num_of_chunks):
    # try to find the key-value-pair that is the min one that satisfy: chunk.key >= num_of_chunks
    chunk=rbt.find_ceil(num_of_chunks) 
    if chunk is Nil: raise NotFound
    ret=chunk.value
    # may locate some chunks that have bigger size than required.
    if chunk.key>num_of_chunks:
        rbt.insert(chunk.key-num_of_chunks, chunk.value+num_of_chunks)
    return ret

所以要维护树。

优势使用RBTree:

  1. 速度很快。我在评论中建议的线性搜索是O(n),但是使用RBTree,它缩小为O(lg n),这意味着它的可扩展性更高。

  2. 易于维护。有数以千计的良好实施的库可以满足您的不同需求。

  3. 如果您通过跟踪块的位置(即我的代码中的value字段)来使用RBTree,您将很好地跟踪您分配的内存,并在您尝试时获得良好的结果解决合并问题。


  4. <强>更新

    这个答案似乎涉及动态内存分配,这可能会引入鸡蛋和鸡蛋问题,实际上并非如此。

    如果您知道未分配块,则必须将其未使用。因此,RBTree的数据可以存储在未使用的块中,这意味着元数据实际上分布在存储空间上。所以在C中,这个问题中的一个节点可能是:

    struct node {
        int length; // key
        struct node *left, *right;
    }
    

    在块中的第一个字节中。

    所以你要做的就是记住root

    struct node *root;
    // your code should operate on rootp, since rotation on RBTree may have root changed.
    // all interfaces related should all receive struct node ** type.
    struct node **rootp = &root;
    

    &#34;那么价值呢?你没有定义一个字段来存储块的地址。&#34;

    是的,我已经定义了。由于数据本身存储在块中,因此struct node的地址是块的地址。

    所以以这种方式,你可以避免动态内存分配,而且,似乎我没有回答你如何找到合适的位序列......但我想这样,你可以管理你的内存分配更好。

答案 2 :(得分:0)

int find_free(int alloca_mem, int req)
{
    int min_len = INT_MAX;
    int start = 0;
    int min_len_index = -1;
    int i;
    for (i = 0; i < NUM_BITS; ++i)
        if (alloca_mem & (1 << i)) {
            int len = start - i;
            if (len > req && len < min_len) {
                min_len_index = i;
                min_len = len;
            }
            start = i + 1;
        }
    }
    return men_len_index;
 }

答案 3 :(得分:0)

调用机器字w(通常为w = 32或64)的大小,以及4KB内存页的总数m。如果已经分配的块之间存在g间隙,并且存在一些完全适合机器字的自由长度n块(并且您很乐意选择最小的这样的块,而不是跨越两个字之间的边界的块) ,然后通过利用进行整数加法&#34;涟漪&#34;: O(m / w + g)时间中找到它>

  1. 给定一个代表w 4KB页面的位图字,反转其所有位。现在,由k个连续的4KB页面组成的任何块都是k个连续1位的块。
  2. 如果添加一个只有1位是该块的LSB的整数,则进位将一直波动,将该块中的所有k位设置为0,将下一位设置为1.
  3. 使用这个并不太难找到在O(m / w + g)时间内包含n个未设置位的间隙而不使用昂贵的或者#34;奇怪的&#34;乘法,除法或位扫描等指令。但是为了找到最小这样的块,你似乎需要位扫描或分区。幸运的是,我们可以限制在整个位图中总共需要使用这些指令至多w-1次操作的次数,因为这是当前最佳间隙可以缩小的最大次数&#34;缩小&#34 ;到较小的一个。 (而不是使用分区,你可以使用类似于最后一段的代码,使用更简单的指令在O(log w)时间内找到MSB的索引;这可能更快,但这一点没有实际意义,因为需要很少的划分。)我们仍然需要O(g)乘法,这可能并不理想,但这些乘法在现代CPU上都很快。

    const int m = /* HOW MANY BLOCKS */;
    const int w = sizeof (unsigned) * CHAR_BIT;
    unsigned bits[m / w];
    
    unsigned findSmallestBlock(unsigned n) {
        if (n > w) return UINT_MAX;    // Can only find within-word blocks.
        unsigned bestI = UINT_MAX;    // Index with bits[]
        unsigned bestLsb = 0;  // Has a 1-bit in the LSB of the gap; 0 = "no solution"
        unsigned bestShifted = ~0;    // The gap's bits, "flush right".
        for (int i = 0; i < sizeof bits / sizeof bits[0]; ++i) {
            // Find the shortest block in bits[i], if one exists
            // First, handle an edge case
            if (bestLsb == 0 && bits[i] == 0) {
                // We don't handle this edge case
                bestI = i;
                bestLsb = 1;
                bestShifted = ~0;
                if (n == w) break;    // Perfectly snug
            }
            unsigned y = ~bits[i];
            unsigned probe = y & (y - 1);    // The LSB of the gap we will test
            while (probe << (n - 1)) {    // Left-shifting it too far => 0 => stop.
                y += probe;    // Ripple!
                unsigned edge = y & (y - 1);    // Extract new LSB.  Overflow to 0 OK.
                unsigned gap = edge - probe;    // Every bit in the gap is 1.
                // Is the gap big enough?
                if (probe << (n - 1) <= gap) {
                    // The gap is big enough.  Is it the tightest fit so far?
                    // Doing this with bit-scan operations is easy; without them we can
                    // use integer multiplication and division, but we want to keep the
                    // divisions to a minimum.  Needs an edge case to be handled above.
                    if (gap < bestShifted * probe) {
                        // Found a new, shorter gap.
                        // The division shifts the entire block right so that it starts
                        // at bit 0.  It's expensive, but happens at most w-1 times.
                        bestI = i;
                        bestLsb = probe;
                        bestShifted = gap / probe;
                        // Is it perfectly snug?  If so, we can stop now.
                        if (probe << (n - 1) > gap >> 1) goto done;    // Yes, "goto".
                    }
                }
                y -= edge;            // Clear the *new* LSB; ignore intervening bits
                probe = edge << 1;    // Again, skip over all intervening bits
            }
        }
    
    done:
        if (bestLsb == 0) return UINT_MAX;    // Didn't find anything
    
        // Find the index of the 1-bit in bestLsb in O(log w) time.
        unsigned pos = bestI * w;
        unsigned z = w >> 1;    // The number of bits we will try to shift right
        while (bestLsb > 1) {
            unsigned x = bestLsb >> z;
            if (x) {
                bestLsb = x;
                pos += z;
            }
            z >>= 1;
        }
    
        return pos;
    }
    

    关于时间复杂度的一个词:当g与m的大小相同时,O(m / w + g)与其他一些解决方案提出的O(m)时间相同,这可能发生:例如:如果每隔一个4KB页面被分配,那么g = m / 2。但是,如果g很小 - 无论是因为没有分配多少页面,还是因为几乎所有页面都已经分配,​​或者因为它们只是被分配和解除分配的模式导致出现的空白很少 - 那么时间复杂性就大了接近O(m / w)。

    还有一件事:在不改变时间复杂度的情况下,可以调整此代码以查找跨越字边界的块,方法是跟踪每个字中的最终间隙,并在每个字的开头进行一次性测试看看增长前一个词的最后差距是否有效。寻找大于w的块也是可能的(除此之外:任何满足n> kw页面请求的间隙必须至少包含位[]中的k个全字0值 - 使用此值可能会大大降低常数因子寻找大的空白)。这两个扩展只需要更多的簿记。