浅析uthash系列之User Guide(翻译)

uthash用户指引

 

作者:Troy D. Hanson, Arthur O'Dwyer

翻译:jafon.tian

转载请注明出处

 

下载uthash,请点击此链接至GitHub项目页面

 

C语言的hash(A hash in C)

这份文档是写给C程序员们的。既然你来到了这里,相信你应该知道当需要使用key在集合中查找元素时可以使用hash。在脚本语言中,到处都在使用hash和dictionary。在C语言中,语言本身没有提供对hash的支持。uthash则为C语言结构体提供了一种hash table实现方法。

uthash能做什么?(What can it do?)

uthash支持对hash table中元素的下列这些操作:

  1. 添加/替换 (add/replace)
  2. 查找(find)
  3. 删除 (delete)
  4. 统计个数 (count)
  5. 迭代 (iterate)
  6. 排序(sort)

uthash速度快吗?(Is it fast?)

添加(add),查找(find)和删除(delete)操作时间为常量。此常量受key取值域和hash计算函数的选区影响。

uthash的目标是小型和高效。总共大概1000行C代码。由于全部采用了宏定义来实现,代码会在编译阶段由编译器自动进行嵌入处理。如果选用的hash函数适合对你提供的key进行计算,uthash将是很快的。你可以选择使用缺省的hash函数,也可以进行比较之后选择的其他hash函数(在buiit-in hash fucntions一节描述)。

uthash是库吗?(Is it a library?)

不是,它只是一个头文件:uthash.h。所有需要你做的只是把这个头文件拷贝到工程里,并且:

#include “uthash.h”

因为uthash只是一个头文件,所以使用它并不需要进行任何链接库的操作。

C/C++和平台(C/C++ and platforms)

uthash可以用在C和C++的程序中。已经在下列平台上进行了测试:

  • Linux
  • Windows using Visual Studio 2008 and 2010
  • Solaris
  • OpenBSD
  • FreeBSD
  • Android

测试案例(Test suites)

想要运行测试案例,请进入tests目录。然后,

  • 在UNIX平台,运行make
  • 在Windows平台,运行“do_test_win32.cmd”批处理文件。(如果你的Visual Studio未安装在缺省目录,你需要对批处理文件进行修改)

BSD licensed

This software is made available under the revised BSD license. It is free and open source.

下载uthash(Download uthash)

通过点击链接 https://github.com/troydhanson/uthash 克隆或下载本软件的zip版本。

获得帮助(Getting help)

请使用 uthash Google Group 提出问题。你也可以将问题发送到邮箱[email protected]

如何贡献代码(Contributing)

你可以通过GitHub提交合并请求。尽管如此,uthash的维护人员不会合并入非必要代码。

附加的头文件(Extras included)

uthash有三个附件的头文件。这些文件提供了对链表、动态数组和字符串的支持:

  • utlist.h 提供C结构体链表宏定义操作。
  • utarray.h 通过宏定义实现了动态数组。
  • utstring.h 实现了一个基础的动态字符串。

历史(History)

出于个人目的,我在2004-2006写了uthash。一开始,它是在SourceForge上。从2006年到2013年,uthash大概被下载了30,000次,之后uthash迁移到了GitHub上。uthash在商用软件,学术研究,其他开源软件中都有过应用。它也被添加进Unix-y发行版仓库中。

比起今天,当最开始写uthash的时候,在C语言中进行hash表操作的选择要少得多。现在有了更快,内存效率更高,具有不同API的hash表。但是,就像是驾驶小型货车,uthash方便实用,在很多时候完全够用。

2016年7月,uthash交由Arthur O’Dwyer维护。

你的结构体(Your structure)

在uthash中,一个hash表由复合的结构体组成。每一个结构体代表一个key-value组合。key由结构体中的一个或多个成员组成。结构体指针自身就是value。

定义一个可以被hash的结构体

#include "uthash.h"

struct my_struct {
    int id;                    /* key */
    char name[10];
    UT_hash_handle hh;         /* makes this structure hashable */
};

请注意,使用uthash,当添加结构体变量到hash表时,被添加的结构体变量不会被移动,也不会被重新拷贝到其他地方。这就意味着,在整个软件运行周期,无论是将结构体变量添加到hash表或从hash表删除,你都可以在其他结构体安全得保持着对这个结构体变量的引用。

Key(The key)

对用于key计算的结构体成员的数据类型和取名没有任何限制。Key可由物理上连续得多个结构体成员组成,可以具有任何成员名称和数据据类型。

任何数据类型...真的?

是的,你的key和结构体包含任何数据类型。不像函数调用需要使用固定的类型,uthash由宏定义组成——参数是无类型的——因此可以应用于任意类型的结构体或key。

key的唯一性(Unique keys)

在任何一种hash中,每一个元素都必须具有唯一的key。你的应用必须保证key的唯一性。在你将元素添加到hash表之前,你必须确保该key还没有被使用(如果怀疑已经用过,那就检查一下)。你可以通过宏HASH_FIND来检查key是否已存在。

Hash Handle(The hash handle)

UT_hash_handle成员必须在你的结构体中出现。它是被用来组织hash表内部结构的。它不需要初始化,可以使用任意名称,但是简单起见可以使用hh这个名称。这样的话,你就可以使用简单些的“便捷”宏定义来添加,查找和删除元素了。

关于内存(A word about memory)

内存占用(Overhead)

一个hash handle占用大约32字节(32位系统)或56字节(64位系统)。相比与hash handle,其他buckets和table的内存占用可以忽略不记。你可以使用宏HASH_OVERHEAD来获取以字节为单位的内存占用。

清理是如何发生的(How clean up occurs)

有人会问,uthash是如何清理其内部占用内存的。答案很简单:当你删除hash表中的最后一个元素时,uthash将与该hash表相关的所有内部占用内存,并将其指针设为NULL。

Hash操作(Hash opereations)

这一节将通过例子来介绍uthash的宏定义使用。具体参见Macro Reference章节。

快捷宏  vs 一般宏

uthash宏分为两类。快捷宏可以操作key数据类型是整数,指针或字符串的hash表(需要将UT_hash_handle名称取为hh)。快捷宏比一般宏使用更少的参数,对那些采用一般数据类型做key的情形使用起来会简单一些。

一般宏定义可以被用在key不是一般数据类型或者是多个成员组合或者UT_hash_handle名称不是hh的情形。这些宏需要更多的参数,因而也提供了更大的灵活性。但是如果快捷宏已经可以满足你的需要,那就使用快捷宏,那样会使你的代码更具可读性。

定义hash表(Declare the hash)

hash表必须定义为一个初始值为NULL指向你自定义结构的指针。

struct my_struct *users = NULL;    /* important! initialize to NULL */

添加元素(Add item)

为你的结构体申请内存并初始化。uthash唯一关心的一点是你的key必须初始化为具有唯一性的值。接着使用HASH_ADD(这里我们使用为key数据类型为int时设计的快捷宏HASH_ADD_INT)

  • 添加一个元素到hash表
void add_user(int user_id, char *name) {
    struct my_struct *s;

    s = malloc(sizeof(struct my_struct));
    s->id = user_id;
    strcpy(s->name, name);
    HASH_ADD_INT( users, id, s );  /* id: name of key field */
}

HASH_ADD_INT的第一参数是hash表,第二个参数是key成员的名称。在这里,key成员的名称是id。最后一个参数是要添加进hash表的结构体变量指针。

等一下...参数成员名称?

如果你觉得把结构体成员名称作为参数奇怪,那么欢迎你来到宏定义的世界。不用担心,C预处理器会把宏展开成C代码。

  • 在使用期间不能改变Key的值(Key must not be modified while in-use)

一旦将结构体变量添加进hash表,不要再改变它的key值。如果需要改变,先将其从hash表中删除,然后调整key值,再加入回hash表。

  • 检查唯一性(Checking uniqueness)

在上面的例子中,我们没有检查看user_id是不是已经存在于hash表中。如果在你的的程序中有可能产生重复的key值,再向hash表添加元素之前你必须进行唯一性检查。如果key值已经存在,你可以简单得修改已经存在于hash表中的元素,而不是要添加一个新的元素。添加两个具有相同key值的元素到hash表将是一个错误。

让我们来重写add_user函数来检查id是否已经存在。只有id不存在时,我们才创建新元素并将其添加进hash表。否则我们只是修改已经存在的元素。

void add_user(int user_id, char *name) {
    struct my_struct *s;
    HASH_FIND_INT(users, &user_id, s);  /* id already in the hash? */
    if (s==NULL) {
      s = (struct my_struct *)malloc(sizeof *s);
      s->id = user_id;
      HASH_ADD_INT( users, id, s );  /* id: name of key field */
    }
    strcpy(s->name, name);
}

为什么uthash不替你检查key的唯一性呢?这为那些可以保证key的唯一性,而不需要再检查的程序节省了查找时间。举个例子,那些key通过递增操作生成,永不重复的程序。

尽管如此,如果替换是个很平常的操作,可以使用HASH_REPLACE。这个宏会首先查找具有相同key值的元素并删除,然后再添加新元素。这个宏也会返回被替换元素的指针,因此用户有机会可以释放为该元素分配的内存。

  • 传递指针到函数(Passing the hash pointer into functions

在上面的例子里,users是全局变量,但是如果函数调用者想把hash指针传递给add_user函数呢?第一眼的感觉是你可以将users作为参数直接传递给han函数,但是这样做是不对的。

/* bad */
void add_user(struct my_struct *users, int user_id, char *name) {
  ...
  HASH_ADD_INT( users, id, s );
}

你真正需要做的是把指向hash指针的指针传递过去:

/* good */
void add_user(struct my_struct **users, int user_id, char *name) { ...
  ...
  HASH_ADD_INT( *users, id, s );
}

请注意,我们在HASH_ADD中也间接引用了这个指针。

需要传递这个指针的原因很简单:宏定义会修改它(换句话说,宏定义修改了指针本身,而不仅仅是指针指向的对象)

替换元素(Replace item)

HASH_REPLACE等同于 HASH_ADD,除了它会先去试着找到和删除元素,它也会将指针作为输出参数返回。

查找元素(Find item)

想要在hash表中查找一个元素,你首先需要它的key。然后调用HASH_FIND(这里我们使用为key数据类型为int时设计的快捷宏HASH_FIND_INT)

  • 使用key查找一个结构
struct my_struct *find_user(int user_id) {
    struct my_struct *s;

    HASH_FIND_INT( users, &user_id, s );  /* s: output pointer */
    return s;
}

这里,hash表是users,&user_id指向key(一个整型)。最后,s是HASH_FIND_INT的输出变量。最后的结果是s指向拥有给定key的结构体变量,如果没有在hash表中找到,那么s将是NULL。

注意:中间那个参数是指向key的指针。你不能传递字面量key值到HASH_FIND。可以将字面量赋值到一个变量,然后传递指向这个变量的指针进去。

删除元素(Delete item)

从hash表中删除一个结构体变量,你必须拥有这个结构体变量的指针。(如果你只是有key,那么先用HASH_FIND得到结构体指针)。

  • 从hash表中删除一个元素
void delete_user(struct my_struct *user) {
    HASH_DEL(users, user);  /* user: pointer to deletee */
    free(user);             /* optional; it's up to you! */
}

这里也是一样,user是hash表,user是指向我们要从hash表中删除的结构体变量的指针。

  • uthash绝不会释放你的结构体

删除一个结构体变量只是将其从hash表中移除——它并没有被释放。何时释放你的结构体完全掌握在你的手里;uthash绝不会释放你的结构体。比如,当使用HASH_REPLACE的时候,被替换的结构体指针作为输出参数返回,这是为了给用户去释放它。

  • 删除能改变指针

hash表指针(被初始化为指向第一个添加到hash表的元素)在HASH_DEL之后可能被改变(比如,删除了hash表中的第一个元素)

  • 迭代删除

HASH_ITER是对删除安全的for循环扩展。

  • 删除hash表中所有的元素
void delete_all() {
  struct my_struct *current_user, *tmp;

  HASH_ITER(hh, users, current_user, tmp) {
    HASH_DEL(users,current_user);  /* delete; users advances to next */
    free(current_user);            /* optional- if you want to free  */
  }
}
  • 一次性删除

如果你只是想删除所有的元素,但并不像释放他们或者不想进行逐个释放,你可以使用这个宏以更高效得做到这一点:

HASH_CLEAR(hh,users);

链表头(在这个例子中是users)将会被设置为NULL

获取元素个数(Count items)

通过HASH_COUNT可以获得hash表中元素的个数

  • 获取hash表元素个数
unsigned int num_users;
num_users = HASH_COUNT(users);
printf("there are %u users\n", num_users);

当链表头(在这个例子中是users)为NULL时,个数将是0.

迭代和排序(Iterating and sorting)

你可以通过从链表头,然后使用hh.next指针来遍历hash表中的所有元素。

  • 迭代hash表中的所有元素
void print_users() {
    struct my_struct *s;

    for(s=users; s != NULL; s=s->hh.next) {
        printf("user id %d: name %s\n", s->id, s->name);
    }
}

你可以通过hh.prev指针从你知道的任何一个元素逆向遍历所有元素。

  • 安全迭代删除

在上面的例子中,在for循环中删除和释放s时不安全的(因为s会在每一次循环中被引用)。重写正确很容易(通过在释放s之前,将s->hh.next指针保存在临时变量中),但是删除安全宏定义HASH_ITER就够用了。它扩展了for循环头。下面是重写的上面的例子,说明如何使用HASH_ITER。

struct my_struct *s, *tmp;
HASH_ITER(hh, users, s, tmp) {
    printf("user id %d: name %s\n", s->id, s->name);
    /* ... it is safe to delete and free s here */
}

hash表也是一个双向链表

由于存在hh.prev和hh.next成员,因此使得在hash表中前向和后向迭代元素成为可能。使用这些指针可以遍历hash表中的所有元素,因此hash表也是双向链表。

如果你是在C++程序中使用uthash,你还需要在for循环中进行x显式类型转换,比如,s=(struct my_struct*)s->hh.next。

排序(sorting)

当你使用hh.next指针来遍历hash表元素时,元素是以它们被插入hash表时的顺序排列的。你可以通过HASH_SORT来重新排列元素。

HASH_SORT( users, name_sort );

第二个参数是指向排序函数的指针。它必须能接受两个指针类型的参数(也就是要被比较的那两个元素),而且返回一个整型值。当第一个参数位置靠前于第二个参数时,返回值小于0;当第一个参数位置等同于第二个参数时,返回值等于0;当第一个参数位置落后于第二个参数时,返回值大于0(这与C库中strcmp或qsort的逻辑是相同的)。

int sort_function(void *a, void *b) {
  /* compare a to b (cast a and b appropriately)
   * return (int) -1 if (a < b)
   * return (int)  0 if (a == b)
   * return (int)  1 if (a > b)
   */
}

下面的name_sort和id_sort是排序函数的示例。

排序hash表中的元素(Sorting the items in the hash)

int name_sort(struct my_struct *a, struct my_struct *b) {
    return strcmp(a->name,b->name);
}

int id_sort(struct my_struct *a, struct my_struct *b) {
    return (a->id - b->id);
}

void sort_by_name() {
    HASH_SORT(users, name_sort);
}

void sort_by_id() {
    HASH_SORT(users, id_sort);
}

当对hash表中的元素进行重新排序之后,首位置的元素可能发生变化。在上面的例子中,调用HASH_SORT之后users可能会指向新的首元素。

一个完整的例子(A complete example)

我们将整合例子的代码,并在main()函数中调用相应的功能,从而构成一个完整的可以工作的例子。

我们命名存放这段代码的文件为example.c,并将它放到和uthash.h相同的目录下,按照下面的方法编译和运行程序:

cc -o example example.c
./example

根据提示试一试这个程序

完整的代码如下

#include <stdio.h>   /* gets */
#include <stdlib.h>  /* atoi, malloc */
#include <string.h>  /* strcpy */
#include "uthash.h"

struct my_struct {
    int id;                    /* key */
    char name[10];
    UT_hash_handle hh;         /* makes this structure hashable */
};

struct my_struct *users = NULL;

void add_user(int user_id, char *name) {
    struct my_struct *s;

    HASH_FIND_INT(users, &user_id, s);  /* id already in the hash? */
    if (s==NULL) {
      s = (struct my_struct *)malloc(sizeof *s);
      s->id = user_id;
      HASH_ADD_INT( users, id, s );  /* id: name of key field */
    }
    strcpy(s->name, name);
}

struct my_struct *find_user(int user_id) {
    struct my_struct *s;

    HASH_FIND_INT( users, &user_id, s );  /* s: output pointer */
    return s;
}

void delete_user(struct my_struct *user) {
    HASH_DEL(users, user);  /* user: pointer to deletee */
    free(user);
}

void delete_all() {
  struct my_struct *current_user, *tmp;

  HASH_ITER(hh, users, current_user, tmp) {
    HASH_DEL(users, current_user);  /* delete it (users advances to next) */
    free(current_user);             /* free it */
  }
}

void print_users() {
    struct my_struct *s;

    for(s=users; s != NULL; s=(struct my_struct*)(s->hh.next)) {
        printf("user id %d: name %s\n", s->id, s->name);
    }
}

int name_sort(struct my_struct *a, struct my_struct *b) {
    return strcmp(a->name,b->name);
}

int id_sort(struct my_struct *a, struct my_struct *b) {
    return (a->id - b->id);
}

void sort_by_name() {
    HASH_SORT(users, name_sort);
}

void sort_by_id() {
    HASH_SORT(users, id_sort);
}

int main(int argc, char *argv[]) {
    char in[10];
    int id=1, running=1;
    struct my_struct *s;
    unsigned num_users;

    while (running) {
        printf(" 1. add user\n");
        printf(" 2. add/rename user by id\n");
        printf(" 3. find user\n");
        printf(" 4. delete user\n");
        printf(" 5. delete all users\n");
        printf(" 6. sort items by name\n");
        printf(" 7. sort items by id\n");
        printf(" 8. print users\n");
        printf(" 9. count users\n");
        printf("10. quit\n");
        gets(in);
        switch(atoi(in)) {
            case 1:
                printf("name?\n");
                add_user(id++, gets(in));
                break;
            case 2:
                printf("id?\n");
                gets(in); id = atoi(in);
                printf("name?\n");
                add_user(id, gets(in));
                break;
            case 3:
                printf("id?\n");
                s = find_user(atoi(gets(in)));
                printf("user: %s\n", s ? s->name : "unknown");
                break;
            case 4:
                printf("id?\n");
                s = find_user(atoi(gets(in)));
                if (s) delete_user(s);
                else printf("id unknown\n");
                break;
            case 5:
                delete_all();
                break;
            case 6:
                sort_by_name();
                break;
            case 7:
                sort_by_id();
                break;
            case 8:
                print_users();
                break;
            case 9:
                num_users=HASH_COUNT(users);
                printf("there are %u users\n", num_users);
                break;
            case 10:
                running=0;
                break;
        }
    }

    delete_all();  /* free any structures */
    return 0;
}

这个程序包含在发行版的tests/example.c的位置。在tests目录下,通过运行make example进行编译。

标准key类型(Standard key types)

这一节将介绍如何使用不同类型的key。你几乎可以使用任何类型作为key-整形,字符串,指针,结构体等等。

注意:关于浮点数

你可以使用浮点类型作为key。对于所有程序都要注意的是判定浮点类型相等的操作。换句话讲,对于两个浮点数来说,即使是非常微小的不同,它们也会被认为是两个不同的key。

整型key(Integer keys)

前面的例子已经演示了整型key的使用。使用快捷宏定义HASH_ADD_INT 和 HASH_FIND_INT操作使用整型key的hash表(其他的操作

比如HASH_DELETEHASH_SORT对于采用不同数据类型作为key值的hash表都是相同的)

字符串型key(String keys)

如果你的结构体使用字符串类型key,那么有一点非常重要,就是字符串key是通过你结构里中的字符指针指向(char*),还是作为字符数组的方式存放在你的结构体里(char a[10])。接下来我们将看到,当使用的是字符指针,你需要使用HASH_ADD_KEYPTR,当使用存放在结构体内部字符数组的方式时使用HASH_ADD_STR。

注意:char[ ] vs char*

下面的第一个例子说明了字符串在结构体中情况——char[10] name。在第二个例子中,字符串在结构外——char* name。因此在第一个例子中使用HASH_ADD_STR,而在第二个例子中使用HASH_ADD_KEYPTR。

  • 字符串在结构体内

采用字符串型key的hash(字符串在结构体内)

#include <string.h>  /* strcpy */
#include <stdlib.h>  /* malloc */
#include <stdio.h>   /* printf */
#include "uthash.h"

struct my_struct {
    char name[10];             /* key (string is WITHIN the structure) */
    int id;
    UT_hash_handle hh;         /* makes this structure hashable */
};


int main(int argc, char *argv[]) {
    const char *names[] = { "joe", "bob", "betty", NULL };
    struct my_struct *s, *tmp, *users = NULL;

    for (int i = 0; names[i]; ++i) {
        s = (struct my_struct *)malloc(sizeof *s);
        strcpy(s->name, names[i]);
        s->id = i;
        HASH_ADD_STR( users, name, s );
    }

    HASH_FIND_STR( users, "betty", s);
    if (s) printf("betty's id is %d\n", s->id);

    /* free the hash table contents */
    HASH_ITER(hh, users, s, tmp) {
      HASH_DEL(users, s);
      free(s);
    }
    return 0;
}

这个例子包含在发行包中tests/test15.c,它会打印出:

betty's id is 2
  • 在结构体中使用字符串指针

这里有相同功能的例子,只不过用char*代替了char[ ]。

采用字符串型key的hash(字符串在结构体外)

#include <string.h>  /* strcpy */
#include <stdlib.h>  /* malloc */
#include <stdio.h>   /* printf */
#include "uthash.h"

struct my_struct {
    const char *name;          /* key */
    int id;
    UT_hash_handle hh;         /* makes this structure hashable */
};


int main(int argc, char *argv[]) {
    const char *names[] = { "joe", "bob", "betty", NULL };
    struct my_struct *s, *tmp, *users = NULL;

    for (int i = 0; names[i]; ++i) {
        s = (struct my_struct *)malloc(sizeof *s);
        s->name = names[i];
        s->id = i;
        HASH_ADD_KEYPTR( hh, users, s->name, strlen(s->name), s );
    }

    HASH_FIND_STR( users, "betty", s);
    if (s) printf("betty's id is %d\n", s->id);

    /* free the hash table contents */
    HASH_ITER(hh, users, s, tmp) {
      HASH_DEL(users, s);
      free(s);
    }
    return 0;
}

这个例子在tests/test40.c。

指针类型key(Pointer keys)

key可是是指针类型。明确一下,这里说的是指针本身可以作为key(与此不同的是,如果想要使用指针指向的内容作为key,需要使用HASH_ADD_KEYPTR)。

下面是个简单的例子,例子中的结构体有个名称是key的指针类型成员变量。

一个指针类型key

#include <stdio.h>
#include <stdlib.h>
#include "uthash.h"

typedef struct {
  void *key;
  int i;
  UT_hash_handle hh;
} el_t;

el_t *hash = NULL;
char *someaddr = NULL;

int main() {
  el_t *d;
  el_t *e = (el_t *)malloc(sizeof *e);
  if (!e) return -1;
  e->key = (void*)someaddr;
  e->i = 1;
  HASH_ADD_PTR(hash,key,e);
  HASH_FIND_PTR(hash, &someaddr, d);
  if (d) printf("found\n");

  /* release memory */
  HASH_DEL(hash,e);
  free(e);
  return 0;
}

这个例子包含在tests/test57.c中。请注意程序在最后将元素从hash表中删除(因此hash表中将没有任何元素),uthash将释放它内部的内存占用。

结构体类型key(Structure keys)

你可以采用任意数据类型的key。对于uthash而言,那只是一串字节。因此,就算是内部嵌套的结构体成员变量,也能被用来当作key。我们将使用一般宏HASH_ADD和HASH_FIND来做个演示。

注意:

结构体可能存在补位字节(为了满足计算机对齐要求而额外分配的字节)。这些补位字节必须先被设置为0,才能进行hash表的元素添加和查找操作。因此在设置你感兴趣的成员变量之前,应该总是将整个结构体先清零。下面的例子就是这么做的——看一下其中的两次memset调用。

  • key是结构体类型
#include <stdlib.h>
#include <stdio.h>
#include "uthash.h"

typedef struct {
  char a;
  int b;
} record_key_t;

typedef struct {
    record_key_t key;
    /* ... other data ... */
    UT_hash_handle hh;
} record_t;

int main(int argc, char *argv[]) {
    record_t l, *p, *r, *tmp, *records = NULL;

    r = (record_t *)malloc(sizeof *r);
    memset(r, 0, sizeof *r);
    r->key.a = 'a';
    r->key.b = 1;
    HASH_ADD(hh, records, key, sizeof(record_key_t), r);

    memset(&l, 0, sizeof(record_t));
    l.key.a = 'a';
    l.key.b = 1;
    HASH_FIND(hh, records, &l.key, sizeof(record_key_t), p);

    if (p) printf("found %c %d\n", p->key.a, p->key.b);

    HASH_ITER(hh, records, p, tmp) {
      HASH_DEL(records, p);
      free(p);
    }
    return 0;
}

使用方法几乎和接下来要介绍的复合key类型相同。

注意一般宏需要将结构体中UT_hash_handle类型变量的名称作为第一个参数传入(在这个例子中就是hh)。

高级话题(Advanced Topics)

复合数据类型key(Compound keys)

你的key甚至可以包含多个连续成员变量。

  • 多成员变量key
#include <stdlib.h>    /* malloc       */
#include <stddef.h>    /* offsetof     */
#include <stdio.h>     /* printf       */
#include <string.h>    /* memset       */
#include "uthash.h"

#define UTF32 1

typedef struct {
  UT_hash_handle hh;
  int len;
  char encoding;      /* these two fields */
  int text[];         /* comprise the key */
} msg_t;

typedef struct {
    char encoding;
    int text[];
} lookup_key_t;

int main(int argc, char *argv[]) {
    unsigned keylen;
    msg_t *msg, *tmp, *msgs = NULL;
    lookup_key_t *lookup_key;

    int beijing[] = {0x5317, 0x4eac};   /* UTF-32LE for 北京 */

    /* allocate and initialize our structure */
    msg = (msg_t *)malloc( sizeof(msg_t) + sizeof(beijing) );
    memset(msg, 0, sizeof(msg_t)+sizeof(beijing)); /* zero fill */
    msg->len = sizeof(beijing);
    msg->encoding = UTF32;
    memcpy(msg->text, beijing, sizeof(beijing));

    /* calculate the key length including padding, using formula */
    keylen =   offsetof(msg_t, text)       /* offset of last key field */
             + sizeof(beijing)             /* size of last key field */
             - offsetof(msg_t, encoding);  /* offset of first key field */

    /* add our structure to the hash table */
    HASH_ADD( hh, msgs, encoding, keylen, msg);

    /* look it up to prove that it worked :-) */
    msg=NULL;

    lookup_key = (lookup_key_t *)malloc(sizeof(*lookup_key) + sizeof(beijing));
    memset(lookup_key, 0, sizeof(*lookup_key) + sizeof(beijing));
    lookup_key->encoding = UTF32;
    memcpy(lookup_key->text, beijing, sizeof(beijing));
    HASH_FIND( hh, msgs, &lookup_key->encoding, keylen, msg );
    if (msg) printf("found \n");
    free(lookup_key);

    HASH_ITER(hh, msgs, msg, tmp) {
      HASH_DEL(msgs, msg);
      free(msg);
    }
    return 0;
}

这个例子包含在发行包的tests/test22.c中。

当使用复合类型key时,你需要知道编译器会在相邻的成员之间填充字节以满足字节对齐要求。比如说结构体包含一个char类型成员,紧接着又有一个int类型成员,那么在char成员之后就会有3个字节的填充,以满足int类型成员是从4字节对齐的地址开始的。

计算复合key的长度

要计算复合key的长度,必须包含编译器为了对齐而在成员间添加的填充字节。

一个简便的计算方法是使用offsetof,它包含在 <stddef.h>头文件中。公式如下

key length =   offsetof(last_key_field)
             + sizeof(last_key_field)
             - offsetof(first_key_field)

在上面的例子中,keylen就是用这个公式计算出来的。

在使用复合key时,首先必须将你的结构全部用0填充,才能使用HASH_ADD将其添加到hash表或使用复合key进行HASH_FIND查找。

前面的例子是通过memset来完成对结构的0填充的,包括成员间的填充字节。如果我们不对整个结构进行0填充,成员间的填充字节将会是随机的内容。这些随机的内容会导致HASH_FIND失败。因为看起来相同的两个key,会因为成员间填充字节的内容不同而不会被认为是相同的key。

多层hash表(Multi-level hash tables)

当hash表元素又包含它自己的hash表式,就出现了多层hash表。这可以包括任意多的层次。在脚本类型语言中,你可能会看到:

$items{bob}{age}=37

下面的例子使用C语言通过uthash来构建这个结构。hash表名字是items。它包含一个叫bob的元素,同时bob又包含一个自己的hash表,在这个表中有个叫age的元素,其值为37。构建多层次hash表并不需要特殊的函数。

在这个例子中两层hash表都使用了相同的结构体,但使用不同的结构体也是可以的。当然hash表也可以是3层甚至更多层。

多层hash表

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "uthash.h"

/* hash of hashes */
typedef struct item {
  char name[10];
  struct item *sub;
  int val;
  UT_hash_handle hh;
} item_t;

item_t *items=NULL;

int main(int argc, char *argvp[]) {
  item_t *item1, *item2, *tmp1, *tmp2;

  /* make initial element */
  item_t *i = malloc(sizeof(*i));
  strcpy(i->name, "bob");
  i->sub = NULL;
  i->val = 0;
  HASH_ADD_STR(items, name, i);

  /* add a sub hash table off this element */
  item_t *s = malloc(sizeof(*s));
  strcpy(s->name, "age");
  s->sub = NULL;
  s->val = 37;
  HASH_ADD_STR(i->sub, name, s);

  /* iterate over hash elements  */
  HASH_ITER(hh, items, item1, tmp1) {
    HASH_ITER(hh, item1->sub, item2, tmp2) {
      printf("$items{%s}{%s} = %d\n", item1->name, item2->name, item2->val);
    }
  }

  /* clean up both hash tables */
  HASH_ITER(hh, items, item1, tmp1) {
    HASH_ITER(hh, item1->sub, item2, tmp2) {
      HASH_DEL(item1->sub, item2);
      free(item2);
    }
    HASH_DEL(items, item1);
    free(item1);
  }

  return 0;
}

这个例子包含在tests/test59.c中。

存在于多个hash表中的元素

一个结构体变量可以被添加到多个hash表中。你要这么做的理由也许是:

  • 每个hash表有自己的key
  • 每个hash表有自己的排序
  • 或者有可能你是使用多个hash表对元素进行分组的。比如,你可能有users元素既包含在admin_users表内又包含在users表内。

你的结构体需要为每个hash表创建各自的UT_hash_handle类型成员bian变量。你可以随意命名,比如

UT_hash_handle hh1, hh2;

拥有多个key的元素(Items with alternative keys)

你可能创建了一个使用ID作为key的hash表,同时又创建了一个使用username为key的hash表(当然qian前提是username具有唯一性)。你可能将同一个结构体变量添加进了这两个hash表(当然是同一个结构体变量),从而可以通过username和ID来查找user。想要做到这一点,需要在你的结构体中为每一个hash表添加一个UT_hash_handle类型成员。

  • 一个拥有两个key的结构体
struct my_struct {
    int id;                    /* usual key */
    char username[10];         /* alternative key */
    UT_hash_handle hh1;        /* handle for first hash table */
    UT_hash_handle hh2;        /* handle for second hash table */
};

上面的这个结构体就可以添加到两个单独的hash表中。在其中一个hash表,id是key;在另外一个hash表,username是key。(当然并没有强制要求两个hash表必须使用不同的key,它们可以采用同样的key值,比如id)。

注意上面的结构有两个hash句柄(hh1和hh2)。在下面的代码中,每个句柄对应其指定的hash表。(users_by_id表使用hh1,users_by_name表使用hh2).

  • 结构体中有两个key
    struct my_struct *users_by_id = NULL, *users_by_name = NULL, *s;
    int i;
    char *name;

    s = malloc(sizeof(struct my_struct));
    s->id = 1;
    strcpy(s->username, "thanson");

    /* add the structure to both hash tables */
    HASH_ADD(hh1, users_by_id, id, sizeof(int), s);
    HASH_ADD(hh2, users_by_name, username, strlen(s->username), s);

    /* lookup user by ID in the "users_by_id" hash table */
    i=1;
    HASH_FIND(hh1, users_by_id, &i, sizeof(int), s);
    if (s) printf("found id %d: %s\n", i, s->username);

    /* lookup user by username in the "users_by_name" hash table */
    name = "thanson";
    HASH_FIND(hh2, users_by_name, name, strlen(name), s);
    if (s) printf("found user %s: %d\n", name, s->id);

新元素的排序插入

如果想维护一个排好序的hash表,你可以有两种选择。第一种是使用HASH_SRT() ,复杂度为n log(n)).当你刚刚建立其一个元素是随机排序的hash表时,最好的选择就是建立hash表之后调用一次HASH_SRT(),所有的元素就都排好了。显然,当你想每次插入数据的时候,hash表都保持顺序状态,这样是不行的。你可以在每次插入操作之后都调用一次HASH_SRT(),但这样的话复杂度就变成了O(n^2 log n)。

另外一种方法就是使用顺序插入和替换宏。HASH_ADD_INORDER*宏会有额外的比较函数参数,其他和HASH_ADD*宏一样。

int name_sort(struct my_struct *a, struct my_struct *b) {
  return strcmp(a->name,b->name);
}
HASH_ADD_KEYPTR_INORDER(hh, items, &item->name, strlen(item->name), item, name_sort);

新元素插入复杂度为O(n),因此建立整个顺序hash表的复杂度为O(n^2),在调用顺序插入之前,hash表必须是顺序排列状态。

多个排列顺序(Severral sort orders)

两个hash表拥有不同的排列顺序并没什么可惊奇的,不过这个特性给按不同方法排列同一个元素集合提供了一种方法。这种方法是基于将同一个元素集合添加到不同的hash表中。

扩展一下之前的例子,假设我们拥有很多的用户。我们将每个用户分别添加到users_by_id表和users_by_name表中。(这里不需要有两份用户结构的拷贝)。现在我们定义两个排序函数,接着使用HASH_SRT。

int sort_by_id(struct my_struct *a, struct my_struct *b) {
  if (a->id == b->id) return 0;
  return (a->id < b->id) ? -1 : 1;
}
int sort_by_name(struct my_struct *a, struct my_struct *b) {
  return strcmp(a->username,b->username);
}
HASH_SRT(hh1, users_by_id, sort_by_id);
HASH_SRT(hh2, users_by_name, sort_by_name);

现在通过users_by_id来遍历元素,元素将按id顺序出现。通过users_by_name来遍历元素,元素则会按照name顺序出现。所有的元素都是双向链接在一起。于是即使是对同一个元素集合,我们也可以通过将它存储于两个hash表中,从而实现对集合元素的两种排序访问。

布隆过滤器(Bloom filter (faster misses))

那些会产生比较平均不存在几率的程序会从内置的布隆过滤器收益。这个特性缺省情况下没有使能,因为布隆过滤器会对xing性能有一些影响。还有就是,那些需要对hash表中元素进行删除操作的程序也不要使用布隆过滤器。当然程序还是可以正常工作,但是删除操作减少了过滤器带来的好处。使能布隆过滤器,只需要在编译的时候加入DHASH_BLOOM=n就像这样:

-DHASH_BLOOM=27

取值可以是不超过32的任意值,这个值决定了过滤器占用的内存空间,具体见下表所示。使用更多的内存可以使过滤器更精确,如果可以更快做出不存在判断,也会使你的chec程序运行更快。

表1. 布隆过滤器占用内存大小和n的选择之间的关系

n Bloom filter size (per hash table)

16

8 kilobytes

20

128 kilobytes

24

2 megabytes

28

32 megabytes

32

512 megabytes

布隆过滤器只是一个性能特性;它并不影响hash表操作的结果。判断是否需要使用布隆过滤器唯一的方法就是进行测试。比较合理布隆过滤器大小事16~32bits。

Select

这里介绍一下还处于试验阶段的select操作,这个操作可以将源hash表中复合条件的元素添加到目标hash表中。这个插入操作会比HASH_ADD更高效,因为在添加到目标hash表时,不需要重新计算key值。select操作并没有删除源hash表中的元素,而是将符合条件的元素同时置于源和目标hash表中。目标hash表可以在操作之前就拥有元素,被选中的元素会被添加到其中。HASH_SELECT操作的结构体必须要有两个或多个hash句柄。(一个结构可以存在于多个hash表中,对每个hash表,结构都必须有一个单独的hash句柄)

user_t *users=NULL, *admins=NULL; /* two hash tables */
typedef struct {
    int id;
    UT_hash_handle hh;  /* handle for users hash */
    UT_hash_handle ah;  /* handle for admins hash */
} user_t;

现在假设我们已经添加了一些用户,我们想选择id号小于1024的管理员用户。

#define is_admin(x) (((user_t*)x)->id < 1024)
HASH_SELECT(ah,admins,hh,users,is_admin);

前面两个参数是目标hash句柄和hash表,接着的两个参数是源hash句柄和hash表,最后一个参数是select条件。这里我们使用了宏定义is_admin(),当然我们也可以使用函数。

int is_admin(void *userv) {
  user_t *user = (user_t*)userv;
  return (user->id < 1024) ? 1 : 0;
}

如果select条件总是返回true,那么这个操作就是将源hash表合并到目标hash表中。当然HASH_SELECT操作之后源hash表不会发生任何变化,此操作只是有选择得将元素添加到目标hash表。

源hash表句柄和目标hash表句柄不能是同一个。HASH_SELECT 的示例在 tests/test36.c中。

内置的hash函数(Built-in hash functions)

在uthash内部,通过hash函数将key映射到bucket number。目前缺省的hash函数是Jenkin。

有些程序使用其他的hash函数也许会更好。uthash提供了个简单的分析工具,用来测试哪一种hash函数会有更高的性能。

你可以通过在编译参数加上-DHASH_FUNCTION=HASH_xyz来选择不同的hash函数。这里的xyz可以取下表中的值。比如:

cc -DHASH_FUNCTION=HASH_BER -o program program.c

表2. 内置hash函数

Symbol Name

JEN

Jenkins (default)

BER

Bernstein

SAX

Shift-Add-Xor

OAT

One-at-a-time

FNV

Fowler/Noll/Vo

SFH

Paul Hsieh

MUR

MurmurHash v3 (see note)

注意:MurmurHash

如果你准备使用MurmurHash,需要额外定义特殊标记。添加-DHASH_USING_NO_STRICT_ALIASING到CFLAGS。如果你使用带优化功能的gcc的话,还需要添加-fno-strict-aliasing到CFLAGS。

  • 哪一个hash函数是最好的选择?

可以很容易为你的key域确定最合适的hash函数。想要做到这一点,需要你的程序进行一次data-collection过程,然后将收集到的数据丢给分析工具处理。

首先你需要编译处分析工具。在顶层目录执行

cd tests/
make

我们将使用test14.c来演示data-collection和分析过程(这里使用sh重定向,将3号文件描述符定向到一个文件)

  • 使用keystats
% cc -DHASH_EMIT_KEYS=3 -I../src -o test14 test14.c
% ./test14 3>test14.keys
% ./keystats test14.keys
fcn  ideal%     #items   #buckets  dup%  fl   add_usec  find_usec  del-all usec
---  ------ ---------- ---------- -----  -- ---------- ----------  ------------
SFH   91.6%       1219        256    0%  ok         92        131            25
FNV   90.3%       1219        512    0%  ok        107         97            31
SAX   88.7%       1219        512    0%  ok        111        109            32
OAT   87.2%       1219        256    0%  ok         99        138            26
JEN   86.7%       1219        256    0%  ok         87        130            27
BER   86.2%       1219        256    0%  ok        121        129            27

注意:在 -DHASH_EMIT_KEYS=3中的数字3是一个文件描述符。任意一个程序中未使用的描述符都可以,未必非要是3.data-collection模式是通过-DHASH_EMIT_KEYS=x打开的,在生产版本上不能使用data-collection模式。

通常情况下,你只要选择列表中最靠上的那个hash函数就可以了。在上面这个例子中就是SFH。这就是针对你的key有最平均的分发效果的hash函数。如果有多个函数有同样的ideal%,那就通过find_usec列找最快的那个。

  • keystats工具结果表格列说明

fcn

hash函数符号名称

ideal%

理想元素个数占比(详细见接下来的说明)

#items

keyfile中key的个数

#buckets

添加完keyfile中所有的key之后,uthash中bucket的个数

dup%

keyfile中重复key的比例

flags

ok或者nx,不建议使用nx的hash函数

add_usec

添加所有keyfile中所有key使用的微秒数

find_usec

查找每个key需要的微秒数

del-all usec

删除所有元素需要的微秒数

  • ideal%

什么是 ideal%?

n个元素将会被分发到k个bucket中。理想情况下,每个bucket中元素的个数应该等于(n/k)。换句话说,如果每个bucket都被充分的使用,那么bucket中最大的线性距离就是n/k 。如果有些bucket被过度使用,而另外一些bucket确使用不充分,那么过度使用的bucket中将包含一些线性距离大于n/k的元素,这些元素就被称谓non-ideal元素。

你可能猜到了,ideal%就是ideal元素个数在总元素个数中的占比。这些元素在bucket链中处于理想的位置。当ideal%接近100%时,查找操作将具有恒定的时间。

hashscan

注意:这个工具只能在linux和FreeBSD(8.1及以上版本使用)

在tests/目录下有一个hashscan工具。在此目录执行make就会自动生成这个工具。这个工具将对正在执行的程序进行检查并生成uthash表shi使用报告。它也可以将key值输出并保存成keystats可以使用的格式。

接下来是一个使用的例子。首先确保它被编译出来

cd tests/
make

由于hashscan需要指定一个需要检查的正在运行的程序,我们先启动一个程序生成hash表,然后这个程序会睡眠作为检查的目标程序。

./test_sleep &
pid: 9711

既然已经有了目标程序,我们开始运行hashscan对其进行检查:

./hashscan 9711
Address            ideal    items  buckets mc fl bloom/sat fcn keys saved to
------------------ ----- -------- -------- -- -- --------- --- -------------
0x862e038            81%    10000     4096 11 ok 16    14% JEN

如果我们想要将key全部拷贝到外部,以便于使用keystats进行分析,需要加上-k标志。

./hashscan -k 9711
Address            ideal    items  buckets mc fl bloom/sat fcn keys saved to
------------------ ----- -------- -------- -- -- --------- --- -------------
0x862e038            81%    10000     4096 11 ok 16    14% JEN /tmp/9711-0.key

现在我们可以运行./keystats /tmp/9711-0.key去分析哪一种算法是最好的选择。

  • hashscan工具结果表格列说明

Address

hash表虚地址

ideal

处于理想位置元素占比

number of items in the hash table

buckets

bucket数目

mc

最大链长度(uthash通常努力将单个bucket中存储的元素个数保持在10个以下,在某些情况下是10的倍数)

fl

标志位 (ok或者 NX)

bloom/sat

bloom统计数据

fcn

hash函数符号名称

keys saved to

导出key的文件路径

Expansion内部原理(Expansion internals)

uthash控制着bucket的数目,从而使得每个bucket只包含较少数量的元素。
 

为什么bucket的数目会有影响?

当进行查找操作的时候,先计算出元素的key值,找到对应的bucket,然后再bucket中进行线性的查找。为了是线性查找保持恒定的时间,每个bucket中的元素个数必须有范围限定。这是通过增加bucket的数目来实现的

  • 普通扩展

Uthash尝试着将每个bucket中元素的个数保持在10个以内。当添加一个元素到bucket导致超过这个限制的时候,bucket的数目就翻倍了,而且元素会被重新分配到新的bucket中。理想情况下,每个bucket中元素的个数将是之前的一半。

当需要时bucket扩展会自动发生,而且是不可见的。应用程序不需要知道bucket发生了扩展。

  • bucket扩展阈值

通常情况下所有的bucket拥有相同的扩展触发阈值(10).在进行bucket扩展的时候,uthash将根据每个bucket的情况调整bucket扩展阈值,尤其是当某些bucket被过度使用的时候。

当扩展阈值发生调整的时候,阈值会从10变成10的某个倍数。乘数是基于bucket中实际元素个数和理想元素个数的比值决定的。这样做考虑到要减少bucket扩展次数,有时候hash函数使得某些bucket被过度使用,但是总体上还算是平均分布。尽管如此,如果整体分布情况恶化的时候,uthash将改变策略。

  • 禁止扩展

通常情况下你不需要知道或担心这一点,尤其是如果在开发的时候已经通过keystats工具选择了最好的hash函数。

一个hash函数可能会在所有bucket中产出不均衡的分布。一定程度上讲,这不是个问题。当bucket元素链变长时,会触发普通扩展。但是当显著的不平衡发生时(由于hash函数不适合key域的原因),bucket扩展无法有效减少元素链长度。

想象一下,一个非常不好的hash函数,无论经过多少次扩展,它总是将所有的元素都分配到0号bucket,因此0号bucket元素链长度永远保持不变。在这种情形下,最好的方式就是停止扩展,接受 O(n) 的查询效率。Uthash就是这么做的,如果hash函数对于key域不合适,将平稳退化。

如果两个次连续的bucket扩展ideal%的值都低于50%,uthash将禁止扩展。一旦设置bucket expansion inhibited标志,当hash表中还存在元素就一直有效。禁止扩展将导致HASH_FIND的性能变差。

Hooks

你不需要使用这些hook——hook是留给你改变uthash的行为模式用的。这些hook是用来替换标准的库函数用得,有些儿平台上并没有这些函数的实现,比如说分配内存,或者对内部事件进行处理。

在uthash.h中除非已经被定义过,这些hook都会被赋为缺省值。你可以在包含uthash.h头文件之后先#undef然后再重新定义hook的值,或者也可以在包含uthash.h之前进行定义。比如说,可以通过将参数-Duthash_malloc=my_malloc传给编译器来修改hook的值。

  • 定义内存管理函数(Specifying alternate memory management functions)

缺省情况下,uthash是使用malloc和free进行内存管理的。如果你的应用使用的是定制的内存管理函数,uthash也可以使用这些定制函数。

#include "uthash.h"

/* undefine the defaults */
#undef uthash_malloc
#undef uthash_free

/* re-define, specifying alternate functions */
#define uthash_malloc(sz) my_malloc(sz)
#define uthash_free(ptr,sz) my_free(ptr)

...

注意一下,uthash_free有两个参数,参数sz是为那些自己管理内存的嵌入式平台准备的。

  • 定义标准库函数(Specifying alternate standard library functions)

uthash也使用strlen(比如在 HASH_FIND_STR 中),memcmp(用于key的比较),和memset(用于将内存置0).在那些没有提供这些库函数的平台上,你可以将它们替换成替换成自己的函数。

#undef uthash_bzero
#define uthash_bzero(a,len) my_bzero(a,len)

#undef uthash_memcmp
#define uthash_memcmp(a,b,len) my_memcmp(a,b,len)

#undef uthash_strlen
#define uthash_strlen(s) my_strlen(s)
  • 超出内存限制(Out of memory)

如果内存分配失败(比如说,uthash_malloc返回NULL),缺省的行为是调用exit(-1)终止当前进程。修改这个行为可以通过重新定义uthash_fatal来实现。

#undef uthash_fatal
#define uthash_fatal(msg) my_fatal_function(msg)

致命错误处理需要终止当前进程或者longjmp到安全的位置。需要注意的是分配失败会使得已分配的内存处于无法恢复的状态。在uthash_fatal被调用之后,hash表已经不可用。当处于这种状态的时候,即使HASH_CLEAR调用也会被认为是不安全的操作。

在内存分配失败的情况下,如果有能从失败中恢复的操作,可以在包含uthash.h头文件之前通过定义HASH_NONFATAL_OOM来实现。在这种情况下,uthash_fatal将不会被使用,相反,每次内存分配失败都会进行一次uthash_nonfatal_oom(elt)调用,这里的elt是导致分配失败的那个节点的地址。uthash_nonfatal_oom的缺省行为是没有任何操作。

#undef uthash_nonfatal_oom
#define uthash_nonfatal_oom(elt) perhaps_recover((element_t *) elt)

在调用uthash_nonfatal_oom之前,hash表会滚回到出现问题插入之前的状态,没有内存泄漏。在uthash_nonfatal_oom中进行throw或longjmp是安全的。

elt参数会保持正确的指针类型,除非是在HASH_SELECT调用了uthash_nonfatal_oom,如果是这种情况elt会是void*类型,在使用之前需要强制转型。在任何情况下,elt->hh.tbl会是NULL。

内存分配失败只会发生在将元素添加到hash表的时候(包括ADD,REPLACE和SELECT操作),在uthash_free中不会发生。

  • 内部事件

应用没有必要来设置这些hook,或者在事件发生时进行回应。设置这些hook主要是为了诊断程序的目的。

这两个hook是通知类hook,当bucket扩展时,或者bucket expansion inhibited被设置时会被调用。一般情况下不会定义这些hook。

  • 扩展

bucket扩展事件hook

#undef uthash_expand_fyi
#define uthash_expand_fyi(tbl) printf("expanded to %d buckets\n", tbl->num_buckets)
  • 禁止扩展

禁止扩展hook

#undef uthash_noexpand_fyi
#define uthash_noexpand_fyi printf("warning: bucket expansion inhibited\n")

调试模式

当在编译使用uthash的程序时使用了-DHASH_DEBUG=1,将会使能内部一致性检查。在这种情况下,hash表进行add或者delete操作时都会进行一致性检查。这只是为了调试uthash本身设计的,不在生产代码中使用。

在tests/目录中,运行make debug就会在这种模式下运行所有的的测试。

在这种模式下,任何hash表的内部错误都会打印一条消息到stderr并终止程序。

UT_hash_handle 结构包括 next, prev, hh_next 和 hh_prev成员。前面两个是应用顺序(也就是应用插入元素的顺序)。后面两个是bucket顺序。这些指针以双向链表的形式将元素链接起来组成。

  • -DHASH_DEBUG=1模式进行的检查:
  1. 遍历hash表两遍:一次bucket顺序,一次应用顺序。
  2. 两次遍历的元素总数都和hash表存储总数进行一致性检查。
  3. bucket顺序遍历时,检查每个元素的hh_prev指针是否指向先前的元素。
  4. 应用顺序遍历时,检查每个元素的prev指针是否指向先前元素。

宏调试

在编译有宏嵌套的程序时,有时候比较难定位编译器警告究竟对应的是哪一行。在uthash,一个宏定义可以被扩展为很多行。在这种情况下,先进行扩展再进行编译是很有帮助的。这样做可以将编译器警告定位到具体的一行。

举个例子说明如何进行这个操作。例子使用了tests/目录下的test1.c。

gcc -E -I../src test1.c > /tmp/a.c
egrep -v '^#' /tmp/a.c > /tmp/b.c
indent /tmp/b.c
gcc -o /tmp/b /tmp/b.c

最后一行编译了扩展后的test1.c,如果有编译器警告,警告中的line number将指向/tmp/b.c中的一行。

线程安全

阅读英文原文

猜你喜欢

转载自blog.csdn.net/JT_Notes/article/details/81201038