为什么要给C语言写一个string库?

(来源于README of cstring

C语言没有原生的string类型,这使得string的管理非常麻烦。这个库主要解决的几个问题:

  • 对于短字符串(小于 32 字节),进行 string interning 。这可以在文本处理时节约不少内存。短 string 相当于 symbol 类型,对它做比较操作的代价可以减少到 O(1) 。
  • 对于临时字符串,如果长度不大(小于 128 字节),尽可能的放在 stack 上,避免动态内存分配。
  • 支持常量字符串,对于常量短字符串只做一次 string interning 操作。
  • 使用引用计数管理相同的字符串,减少字符串的拷贝。
  • 短字符串,常量字符串,以及引用次数非常多(大于 64K 次)的字符串可以不动态释放,简化生命期管理。
  • 惰性计算,以及缓存字符串的 hash 值,以方便实现 hashmap 。
  • 这个库是线程安全的。

cstring中的数据结构

struct cstring_data {
    char * cstr;
    uint32_t hash_size;
    uint16_t type;
    uint16_t ref;
};

记录字符串的数据结构,cstr指向的内存位置和相应的字符串种类有关:

struct string_node {
    struct cstring_data str;
    char buffer[CSTRING_INTERNING_SIZE];
    struct string_node * next;
};

struct string_pool {
    struct string_node node[INTERNING_POOL_SIZE];
};

struct string_interning {
    int lock;
    int size; // 哈希表的大小
    struct string_node ** hash;
    struct string_pool * pool;
    int index;
    int total;
};

在存储短字符串(小于32字节)时需要进行string interning操作,每个intering string节点由一个string_node表示,所有被intering的字符串记录存放在一个string pool中,并在哈希表hash中记录它们的信息。在string_intering结构中,采用链地址法来解决hash冲突。

cstring的实现

  • 临时字符串(小于128字节),直接在栈上申请分配空间,避免动态申请释放内存带来的开销;
  • 短字符串(小于32字节),使用string interning技术进行存储,相同的字符串只会存储一个实例,这样的好处在于对字符串进行比较操作时时间复杂度为O(1)(云风的博文中提到它们相当于一个symbol类型),缺点在于在进行string interning和hash表expand的时候会带来一些开销;
  • 常量字符串:如果它的长度小于32字节,把它当成短字符串来处理,否则当成permanent字符串来处理。

首先看看它是如何存储临时字符串(小于128字节)的:

#define CSTRING_BUFFER(var) \
    char var##_cstring [CSTRING_STACK_SIZE] = { '\0' }; \
    struct cstring_data var##_cstring_data = { var##_cstring , 0, CSTRING_ONSTACK, 0 }; \
    cstring_buffer var; \
    var->str = &var##_cstring_data;

从代码可以看出,用宏CSTRING_BUFFER来定义常量字符串时,字符串的所需内存直接在栈上分配。

然后看看string intering是怎么实现的:

static cstring
interning(struct string_interning * si, const char * cstr, size_t sz, uint32_t hash) {
    if (si->hash == NULL) {
        return NULL;
    }
    int index = (int)(hash & (si->size-1));
    struct string_node * n = si->hash[index];
    //先在hash表中查找,如果找到,直接返回,相同的cstr返回的cstring地址相同
    while(n) {
        if (n->str.hash_size == hash) {
            if (strcmp(n->str.cstr, cstr) == 0) {
                return &n->str;
            }
        }
        n = n->next;
    }
    // 添加cstr到pool和hash表中
    // 80% (4/5) threshold
    if (si->total * 5 >= si->size * 4) {
        return NULL;
    }
    if (si->pool == NULL) {
        // need not free pool
        // todo: check memory alloc error
        si->pool = malloc(sizeof(struct string_pool));
        assert(si->pool);
        si->index = 0;
    }
    n = &si->pool->node[si->index++];
    memcpy(n->buffer, cstr, sz);
    n->buffer[sz] = '\0';

    cstring cs = &n->str;
    cs->cstr = n->buffer;
    cs->hash_size = hash;
    cs->type = CSTRING_INTERNING;
    cs->ref = 0;

    n->next = si->hash[index];
    si->hash[index] = n;

    return cs;
}

对常量字符串的处理:

#define CSTRING_LITERAL(var, cstr)  \
    static cstring var = NULL;  \
    if (var) {} else {  \
        cstring tmp = cstring_persist(""cstr, (sizeof(cstr)/sizeof(char))-1);   \
        if (!__sync_bool_compare_and_swap(&var, NULL, tmp)) { \
            cstring_free_persist(tmp);  \
        }   \
    }

static cstring
cstring_clone(const char * cstr, size_t sz) {
    if (sz < CSTRING_INTERNING_SIZE) {
        return cstring_interning(cstr, sz, hash_blob(cstr,sz));
    }
    struct cstring_data * p = malloc(sizeof(struct cstring_data) + sz + 1);
    // todo: memory alloc error
    assert(p);
    void * ptr = (void *)(p + 1);
    p->cstr = ptr;
    p->type = 0;
    p->ref = 1;
    memcpy(ptr, cstr, sz);
    ((char *)ptr)[sz] = '\0';
    p->hash_size = 0;
    return p;
}

cstring 
cstring_persist(const char * cstr, size_t sz) {
    cstring s = cstring_clone(cstr, sz);
    if (s->type == 0) {
        s->type = CSTRING_PERMANENT;
        s->ref = 0;
    }
    return s;
}

思考

README中关于literal操作提到在宏CSTRING_LITERAL(var, literal)中literal必须是以引号包含的字符串常量,而不能使用指向字符串的指针?

这是由于在处理这个宏的时候作者使用了sizeof()来计算literal字符串的大小,而sizeof()是不能计算指针指向的内存的大小的。那为什么不用strlen()来代替sizeof()?sizeof()是运算符,在编译阶段可以得到结果,而strlen()是函数,需要运行阶段才能知道它的值。考虑到这个宏只用于处理常量字符串,这种要求比较合理。

参考资料

cstring的实现

一个简单的 C string 库

Comments

Powered by Pelican. Booler's Adventure © 不贰 2014-2016