现在是否值得实施slab分配器?

时间:2015-03-04 16:44:38

标签: c linux performance sockets memory

我正在服务器上,必须同时读取数千个连接的客户端。客户端请求由具有大约32字节的所有相同精确大小的消息构成。

我正在阅读有关slab allocator的内容,当我调用read从套接字中获取数据时,我想将此特定技术用于我的应用程序(read复制数据从内核缓冲区到我选择的缓冲区,我想使用一些动态分配的内存。)

在我阅读时,似乎Linux内核已经在使用这种技术了。如果这用于实现malloc or new,那么鉴于分配已经具有效率,我仍然值得这么做吗?

我在想,如果不使用SLAB算法在堆栈上使用分配,我可能会更好,但我不确定哪种方法最好。

1 个答案:

答案 0 :(得分:14)

如果你是C程序员,当然你应该弄脏内存管理!

但是,除非您真正接近机器的极限,否则您可能不会遇到任何问题,只需对每个请求进行malloc,这似乎不太可能。但我相信,了解你的替代方案要比采取别人的话更好。以下是一些需要考虑的想法。

静态数组

最简单的替代方法是使用单个全局请求槽阵列,并跟踪正在使用的请求槽。这意味着对多少请求的静态限制,但另一方面,没有开销,也没有碎片的真正问题。只需设置极限即可。

这是一个示例实现。如果您不熟悉按位操作,它可能看起来有点令人困惑,但要点是我们有一个额外的数组,每个请求槽包含一个位(打开或关闭),指定该插槽是否正在使用中。相反,你可以添加一个' is_used'变量到结构本身,但最终会将结构填充多于一个位,这违背了我们减少开销的目标。

头文件非常小(顺便说一下,这是C的真正之美!):

typedef struct request_s {
  /* your 32 bytes of information */
  unsigned char data[32];
} request_t;

request_t *alloc_request(void);
void free_request(request_t *req);

源文件:

#include the header file

/* note: this example is not written with multithreading in mind */

/* allow a million requests (total 32 MB + 128 KB of memory) */
#define MAX_REQUESTS (1*1024*1024)

static request_t g_requests[MAX_REQUESTS];

/* use one bit per request to store whether it's in use */
/* unsigned int is 32 bits. shifting right by 5 divides by 32 */
static unsigned int g_requests_used[MAX_REQUESTS >> 5];

request_t *alloc_request(void) {
  /* note: this is a very naive method. you really don't want to search
   * from the beginning every time, but i'll leave improving that as an
   * exercise for you. */
  unsigned int word_bits;
  unsigned int word, bit;

  /* look through the bit array one word (i.e., 32 bits) at a time */
  for (word = 0; word < (MAX_REQUESTS >> 5); word++) {
    word_bits = g_requests_used[word];

    /* we can tell right away whether the entire chunk of 32 requests is
     * in use, and avoid the inner loop */
    if (word_bits == 0xFFFFFFFFU)
      continue;

    /* now we know there is a gap somewhere in this chunk, so we loop
     * through the 32 bits to find it */
    for (bit = 0; bit < 32; bit++) {
      if (word_bits & (1U << bit))
        continue; /* bit is set, slot is in use */

      /* found a free slot */
      g_requests_used[word] |= 1U << bit;
      return &g_requests[(word << 5) + bit];
    }
  }

  /* we're all out of requests! */
  return NULL;
}

void free_request(request_t *req) {
  /* make sure the request is actually within the g_requests block of
   * memory */
  if (req >= g_requests && req < g_requests + MAX_REQUESTS) {
    /* find the overall index of this request. pointer arithmetic like this
     * is somewhat peculiar to c/c++, you may want to read up on it. */
    ptrdiff_t index = req - g_requests;

    /* reducing a ptrdiff_t to an unsigned int isn't something you should
     * do without thinking about it first. but in our case, we're fine as
     * long as we don't allow more than 2 billion requests, not that our
     * computer could handle that many anyway */
    unsigned int u_index = (unsigned int)index;

    /* do some arithmetic to figure out which bit of which word we need to
     * turn off */
    unsigned int word = u_index >> 5; /* index / 32 */
    unsigned int bit  = u_index & 31; /* index % 32 */

    g_requests_used[word] &= ~(1U << bit);
  }
}

(是的,是的,您可以写index / 32而不是index >> 5,依此类推,编译器会为您优化它。但它对我来说并不合适。 ..)

你可以深入研究并优化分配器搜索免费插槽。一个简单的想法是在最后一次分配的位置开始搜索,并在结束时回滚。

这种方法有很多好处,但有一个很大的缺点:限制。您可能希望将限制设置为高于您预期的最大请求数量。但是,如果你能做到这一点,那么你可能没有碰到你系统的极限,所以你为什么一开始就在这里?!

内存池(静态数组的链表)

如果您不喜欢静态限制,则可以批量分配。保留内存和#34;池的链接列表,每个池都包含一定数量的请求槽。每个单独的池基本上是如上所述的静态阵列。如果所有现有池都已满,我们将一个新池malloc并将其添加到链接列表中。游泳池基本上是这样的:

#define REQUESTS_PER_POOL 1024

typedef struct request_pool_s request_pool_t;

struct request_pool_s {
  request_t requests[REQUESTS_PER_POOL];

  unsigned int requests_used[REQUESTS_PER_POOL >> 5];

  request_pool_t *prev;
  request_pool_t *next;
};

当流量消失时,您希望能够释放游泳池,否则这几乎与静态限制不同!不幸的是,在这种情况下,您将处理碎片问题。您可能会发现自己有很多稀疏使用的池,特别是如果请求有时可能持续很长时间。整个池都无法释放,直到它上面的每个最后一个槽都为空。你仍然在节省开销(考虑到个别请求的小规模),但处理碎片可能会将这从一个小巧,优雅的解决方案转变为更多的工作而不是它的价值。

您可以减少每个池的请求数量,以减少碎片的影响,但此时我们将失去此方法的优势。

哪一个?

首先,您应该考虑单个malloc的替代方案的主要原因是:结构的小尺寸(32字节),大量的结构,以及创建和销毁它们的频率。

  • 静态数组大大减少了开销,但在当今时代很难证明其合理性。除非您的服务器在Arduino上运行。

  • 内存池是这类问题的明显方向,但它们可能需要相当多的工作才能顺利运行。如果这是你的胡同,那么我说去吧。

  • 平板分配器就像复杂的内存池,不限于某个单一的结构大小。它们对你来说太过分了,因为你只有32字节的请求,尽管你可能找到适合你的第三方库。

采取简单的路线并简单地按照每个请求进行操作是一个滑坡,最终可能会完全放弃C。 ;)