Klib - C语言通用库

来源:互联网 发布:办公软件网上培训 编辑:程序博客网 时间:2024/05/01 14:03

samtools,bwa等软件都会涉及到这个库,在项目中也比较方便加入这个库。
来自:https://github.com/attractivechaos/klib

简介

Klib是C语言写成的,独立,轻量级,并且遵循MIT/X11声明。其中的大部分只是用到了C标准库,相互之间也是独立的。如果使用这个库中的一部分,只需要将使用到的文件拷贝到你自己的项目中即可,而不用担心库的依赖关系。

Klib致力于高效率和比较小的内存使用。一些子库是在所有语言里的相似算法或数据结构中的最具有效能的实现,包括速度和内存,比如: khash.h, kbtree.h, ksort.h和 kvec.h

基本子库

  • khash.h : 基于多重散列(double hashing)的散列类
  • kbtree.h : 基于B树的搜寻树
  • ksort.h : 排序类,包括 introsort, merge sort, heap sort, comb sort, Knuth shuffle和k-small算法
  • kseq.h : 流缓存类及FASTA/FASTQ格式解析
  • kvec.h : 动态数组类
  • klist.h : 单链表和内存池
  • kstring.{h,c} : 基础字符库
  • kmath.{h,c} : 包括了伪随机数生成器,基本的非线性规划和一些特殊的数学函数

不常用的子库

  • ksa.c : 基于校正SAIS算法,实现的字符串后缀数组
  • knetfile.{h,c} : 远程文件(on HTTP or FTP)接口
  • kopen.c : 敏捷数据流打开器
  • khmm.{h,c} : 基本的HMM库
  • ksw.{h,c} : Striped Smith-Waterman algorithm
  • knhx.{h,c} : Newick树格式解析

使用方法

为了实现通用容器,klib广泛使用了C语言的宏定义。为了使用这些数据结构,我们需要扩展很长的宏来定义实现方法。这就使得源代码比较与众不同,甚至是丑陋,并且增加了困难性(不管是写还是读)和bug。不幸的是,在C语言中限制使用模板,为了高效率的通用容器编程只能使用C宏定义。使用宏定义后,可以很有效率的完成指定类型的容器。有一些C语言的泛型库,例如Glib,使用了void *类型去定义了容器。但是这通常会比klib慢并且会使用更多的内存。

为了高效的使用klib,理解如何实现通用编程很重要。下面是使用hash table库的一个例子:

#include "khash.h"KHASH_MAP_INIT_INT(m32, char)        // instantiate structs and methodsint main() {    int ret, is_missing;    khint_t k;    khash_t(m32) *h = kh_init(m32);  // allocate a hash table    k = kh_put(m32, h, 5, &ret);     // insert a key to the hash table    if (!ret) kh_del(m32, h, k);    kh_value(h, k) = 10;             // set the value    k = kh_get(m32, h, 10);          // query the hash table    is_missing = (k == kh_end(h));   // test if the key is present    k = kh_get(m32, h, 5);    kh_del(m32, h, k);               // remove a key-value pair    for (k = kh_begin(h); k != kh_end(h); ++k)  // traverse        if (kh_exist(h, k))          // test if a bucket contains data            kh_value(h, k) = 1;    kh_destroy(m32, h);              // deallocate the hash table    return 0;}

在这个例子中,第二行示例了一个键类型为unsigned,值类型为char的hash table。m32是这种类型hash table的名称。m32相关的所有类型和函数都是宏定义,后面会有解释。宏kh_init()创建一个hash table,宏kh_destroy()释放hash table。kh_put()插入一个键值并返回一个hash table的迭代器(或位置)。kh_get()和kh_del()得到一个键和删掉一个元素。kh_exist()测试一个迭代器(或位置)是否存在值。

首先遇到的问题就是这段代码看起来不像是可用的C代码(缺少分号或是未定义的m32)。为了去理解为什么这段代码是正确的,我们来看看khash.h的源码是怎么写的。

#define KHASH_INIT(name, SCOPE, key_t, val_t, is_map, _hashf, _hasheq) \  typedef struct { \    int n_buckets, size, n_occupied, upper_bound; \    unsigned *flags; \    key_t *keys; \    val_t *vals; \  } kh_##name##_t; \  SCOPE inline kh_##name##_t *init_##name() { \    return (kh_##name##_t*)calloc(1, sizeof(kh_##name##_t)); \  } \  SCOPE inline int get_##name(kh_##name##_t *h, key_t k) \  ... \  SCOPE inline void destroy_##name(kh_##name##_t *h) { \    if (h) { \      free(h->keys); free(h->flags); free(h->vals); free(h); \    } \  }#define _int_hf(key) (unsigned)(key)#define _int_heq(a, b) (a == b)#define khash_t(name) kh_##name##_t#define kh_value(h, k) ((h)->vals[k])#define kh_begin(h, k) 0#define kh_end(h) ((h)->n_buckets)#define kh_init(name) init_##name()#define kh_get(name, h, k) get_##name(h, k)#define kh_destroy(name, h) destroy_##name(h)...#define KHASH_MAP_INIT_INT(name, val_t) \    KHASH_INIT(name, static, unsigned, val_t, is_map, _int_hf, _int_heq)

KHASH_INIT()是一个很大的宏定义,其中定义了所有了结构和方法。用到这个宏的时候,所有的这些代码就会插入到使用他的地方。如果这个宏使用了多次,那么这段代码就有多个拷贝。为了避免不同key-value类型hash table的命名冲突,使用了##运算符来在宏定义中通过传入参数作为标记来定义不同的hash table名称。最后,在把相应带有标记的函数指代回来。

typedef struct {  int n_buckets, size, n_occupied, upper_bound;  unsigned *flags;  unsigned *keys;  char *vals;} kh_m32_t;static inline kh_m32_t *init_m32() {  return (kh_m32_t*)calloc(1, sizeof(kh_m32_t));}static inline int get_m32(kh_m32_t *h, unsigned k)...static inline void destroy_m32(kh_m32_t *h) {  if (h) {    free(h->keys); free(h->flags); free(h->vals); free(h);  }}int main() {    int ret, is_missing;    khint_t k;    kh_m32_t *h = init_m32();    k = put_m32(h, 5, &ret);    if (!ret) del_m32(h, k);    h->vals[k] = 10;    k = get_m32(h, 10);    is_missing = (k == h->n_buckets);    k = get_m32(h, 5);    del_m32(h, k);    for (k = 0; k != h->n_buckets; ++k)        if (kh_exist(h, k)) h->vals[k] = 1;    destroy_m32(h);    return 0;}

这就是我们平常熟知的C程序了。

从上面的例子我们可以看到,宏定义和C预编译器在klib中起到了主要作用。klib之所以快是因为在编译阶段就已经知道了key-value的类型可以优化到指定key-value类型的程度,而使用void *来构建通用库的方式是达不到这种效率的。

从上例来看,在程序中可能会插入很多代码,通过这个我们想到C++当使用STL/boost库是编译起来很慢,并且编译完成的文件很大。klib要好很多,比较小的代码量,并且彼此独立。即使插入几百行代码也不会使编译速度变慢。

0 0