C言語実戦課-klibライブラリ学習

8403 ワード

PythonとRでは、既存のツールを使用して、通常はインストールしてから、ヘルプドキュメントを見て関数を呼び出す方法を学びます.では、C言語にとって、私はどのように既存の車輪を使うべきですか?
先日、私は李恒大神のseqtkコードについて紹介したブログを翻訳して、私にseqtkを切り口にして、どのように他の人で作った車輪を紹介するかを決めさせました.seqtkは主に2つのヘッダファイルを使用しています.hとkseq.h、同時に私はこの2つのファイルがklibの中にあることを発見して、klibは比較的に詳しい紹介ドキュメントを提供して、そのため私は直接klibを学びました.
klib名のkについて、以前英語の先生が言ったように、英語ではkとcの発音が似ていて、例えばkindleはcandleのように読むので、ここのkと関数名、構造体名のkは、cと考えられ、c言語ライブラリであることを示しています.
ステップ1:ドキュメントを読む
優秀なプロジェクトには必ず優秀なドキュメントがこのプロジェクトを紹介します.そうしないと、自分で楽しむしかなく、広く使われません.そのため、klibを学ぶ最初のステップは、そのドキュメントを読むことです.私は自分をもっとよく理解するために、翻訳を通じて自分を真剣に読むことができます.
モデルのコンテナを実現するために,klibは広くCマクロを用いた.これらのデータ構造を使用するには、マクロ内のインスタンスメソッドを拡張する必要があります.これにより、ソースコードは読みにくくなり、後続のデバッグに不利になります.C言語にはテンプレートの特性がないため、効率的なモデルプログラミングを実現するにはマクロしか使用できません.マクロの助けの下でこそ,モデルコンテナを書くことができ,初期化後に効率的にタイプ特異なコンテナと競争することができる.他のライブラリ、例えばGlibは、void *タイプのインプリメンテーションコンテナを使用している.しかし、この実装方法は効率が低く、klibよりも多くのメモリを使用する.(この評価結果を参考に)
klibを効果的に使用するためには、モデルプログラミングをどのように実現するかを理解する必要があります.ここでハッシュテーブルライブラリ(ハッシュテーブルはキー値対データベースであり、クエリー効率が高い)を例に挙げる
#include "khash.h"
KHASH_MAP_INIT_INT(32, char) //            
int main() {
    int ret, is_missing;
    khiter_t k;
    khash_t(32) *h = kh_init(32); //   
    k = kh_put(32, h, 5, &ret);   //  key
    kh_value(h, k) = 10;   //  key   
    k = kh_get(32, h, 10); //    key
    is_missing = (k == kh_end(h));
    k = kh_get(32, h, 5);
    kh_del(32, h, k); //  key
    for (k = kh_begin(h); k != kh_end(h); ++k) //     
        if (kh_exist(h, k)) kh_value(h, k) = 1;  //        
    kh_destroy(32, h); //     
    return 0;
}

ここでの例では、2行目は、unsignedのキータイプ、charの値タイプを有するハッシュテーブルを実例化し、m32はハッシュテーブルの名前である.この名前に関連するすべての関数とデータ型はマクロであり、後で説明します.マクロkh_init()はハッシュ・テーブルを初期化し、kh_destroy()はメモリの解放を担当する.kh_putはキーを挿入し、ハッシュテーブルの反復器(または位置)を返す.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は巨大な都マクロであり、彼はすべての都構造体方法を定義した.このマクロが呼び出されると、すべてのコードがCプリプロセッサを介してプログラムに挿入されて呼び出されます.1つのマクロが複数回呼び出されると、コードはプログラムに複数のコピーが表示されます.ハッシュ・テーブルで異なるキー値タイプによるネーミング競合を回避するために、このライブラリはtoken concatenation、すなわちプリプロセッサを使用してマクロ内のパラメータを使用してシンボルの一部の内容を置き換えることができます.最後にCプリプロセッサは次のコードを生成してコンパイラに渡す.(kh_exist(h,k)は複雑なので、ここでは紹介しません)
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プリプロセッサはklibにおいて非常に重要な部分であることが分かる.なぜklibがそんなに速いのか、一部の理由は、コンパイラがコンパイル中にキー値ペアのタイプを知っているため、クラス特異コードペアレベルにコードを最適化することができるからです.void *で書かれたモデルライブラリでは、このパフォーマンスは達成できません.
インスタンス化中に多くのコードが挿入されます.これは、C++のコンパイル速度が遅く、STL/boostを使用してコンパイルされたバイナリファイルが大きい理由を示しています.Klibはコード量が小さく,各コンポーネントが相対的に独立しているため,より優れている.100行以上のコードを挿入してもコンパイルが遅くなりません.
ステップ2:ケースの実行
ケースとしてハッシュ・テーブルを紹介し、テストを行うことができます.
git clone https://github.com/attractivechaos/klib

新しいhash_を作成test.c,下記の内容を貼り付ける
#include "klib/khash.h"
KHASH_MAP_INIT_INT(m32, char)        // instantiate structs and methods
int 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;
}

コンパイルと実行
gcc -o test hash_test.c
./test

このコードは実行時に出力されません.次にコードを改造して、コードが情報を出力できるようにしてみましょう.
ステップ3:コードの改造
コードを書くことを学ぶ最も速いパスは、マルチライトコードです.以前私が勉強していたときは、何度も見れば十分だと思っていたので、本当に書く必要はありませんでした.本当にコードを書き始めたとき、自分が何もできないことに気づいた.したがって、khashの使用を習得できるようにする.h、私たちは自分の考えに従ってコードをいくつか改造して、コードの表現が予想に合っているかどうかを見ます.次のコードでは、キー値ペアを設定するためのループを書きます.
#include 
#include "klib/khash.h"
KHASH_MAP_INIT_INT(m32, char)        // instantiate structs and methods
int main() {
    int ret, is_missing;
    khint_t k;
    khash_t(m32) *h = kh_init(m32);  // allocate a hash table

    int i = 0;
    for ( i = 0 ; i < 10 ; i++ ){
        k = kh_put(m32, h, i, &ret);     // insert a key to the hash table
        if (!ret) kh_del(m32, h, k);
        kh_value(h, k) = i * i;             // set the value
    }

    for (k = kh_begin(h); k != kh_end(h); ++k)  // traverse
        if (kh_exist(h, k))          // test if a bucket contains data
            printf("%d
", kh_value(h, k) ); kh_destroy(m32, h); // deallocate the hash table return 0; }

ファイルがあり、キー値ペアの関係でもある場合は、このファイルを読み取り、ファイルでハッシュテーブルにコンテンツを割り当てることができます.
ステップ4:プロジェクトに適用
klibを学ぶ目的は私の実際の問題を解決するためで、つまり共通のfastqを書いてfastaプログラムに変換して、このプログラムは自動的に行が長すぎる問題を解決することができます.最終コードは次のとおりです.
#include 
#include 
#include "klib/kseq.h"

KSEQ_INIT(gzFile, gzread)

int main(int argc, char *argv[])
{

        gzFile fp;
        kseq_t *seq;
        int l;
        if (argc == 1){
                fprintf(stderr, "Usage: %s 
", argv[0]); return 1; } fp = gzopen(argv[1], "r"); seq = kseq_init(fp); // seq while( (l = kseq_read(seq)) >= 0){ // seq printf(">%s
", seq->name.s); // printf("%s
", seq->seq.s); } kseq_destroy(seq); // gzclose(fp); return 0; }

今回のコードは特に簡潔で、読みやすいです.変数名定義とメモリ管理のコードが数行増えたほか、Pythonスクリプトを書くのと同じように、呼び出し関数にほかならない.kseq_readはデータ読み出し、kseq_tはシーケンスデータを格納するために使用され、その構造定義は以下の通りである.
typedef struct __kstring_t {
        size_t l, m;
        char *s;
} kstring_t;
typedef struct {
        kstring_t name, comment, seq, qual;
        int last_char;
        kstream_t *f;
} kseq_t

これは私が書いたklibライブラリについての最初のチュートリアルで、これからもこのライブラリを使い続け、使うときに何編か書いて紹介します.
おすすめ読書:C言語実戦授業-klibライブラリ学習