kmem_cache_alloc_bulk()を試してみる

lwn.netの2015/09/10の記事Linuxの4.3にkmem_cache_alloc_bulk()とkmem_cache_free_bulk()のスラブキャッシュをがっつり確保するための関数が入ったと書かれていたので試してみました。

最初のパッチはslab: infrastructure for bulk object allocation and freeing · torvalds/linux@484748f · GitHubで、kmem_cache_alloc()を複数回呼ぶという形でしたが4.3.0-rc2だと変わっています。

変わるとなんなのでコピペしておくと4.3.0-rc2ではこうなってます。

/* Note that interrupts must be enabled when calling this function. */
bool kmem_cache_alloc_bulk(struct kmem_cache *s, gfp_t flags, size_t size,
               void **p)
{
    struct kmem_cache_cpu *c;
    int i;

    /*
    * Drain objects in the per cpu slab, while disabling local
    * IRQs, which protects against PREEMPT and interrupts
    * handlers invoking normal fastpath.
    */
    local_irq_disable();
    c = this_cpu_ptr(s->cpu_slab);

    for (i = 0; i < size; i++) {
        void *object = c->freelist;

        if (unlikely(!object)) {
            local_irq_enable();
            /*
            * Invoking slow path likely have side-effect
            * of re-populating per CPU c->freelist
            */
            p[i] = __slab_alloc(s, flags, NUMA_NO_NODE,
                        _RET_IP_, c);
            if (unlikely(!p[i])) {
                __kmem_cache_free_bulk(s, i, p);
                return false;
            }
            local_irq_disable();
            c = this_cpu_ptr(s->cpu_slab);
            continue; /* goto for-loop */
        }

        /* kmem_cache debug support */
        s = slab_pre_alloc_hook(s, flags);
        if (unlikely(!s)) {
            __kmem_cache_free_bulk(s, i, p);
            c->tid = next_tid(c->tid);
            local_irq_enable();
            return false;
        }

        c->freelist = get_freepointer(s, object);
        p[i] = object;

        /* kmem_cache debug support */
        slab_post_alloc_hook(s, flags, object);
    }
    c->tid = next_tid(c->tid);
    local_irq_enable();

    /* Clear memory outside IRQ disabled fastpath loop */
    if (unlikely(flags & __GFP_ZERO)) {
        int j;

        for (j = 0; j < i; j++)
            memset(p[j], 0, s->object_size);
    }

    return true;
}
EXPORT_SYMBOL(kmem_cache_alloc_bulk);

それで、ついでなのでkmem_cache_alloc_bulk()とkmem_cache_alloc()をループして同じ量のメモリを確保した時に実行時間に差があるかというのを見てみます。 テストコード、Makefile、テスト実行用のシェルスクリプトはこちらです。

kmem_cache_alloc_bulk() test

テストした環境はkvm上のArch Linux(カーネルは4.3.0-rc2)で、リブート直後に100回のinsmod(rmmodももちろん)を実行して結果をとりました。 あ、カーネルのconfigでメモリー関連のデバッグ機能は全部offにしました。 最初やたら遅いなとか思ったらCONFIG_DEBUG_VMをONにしていて、スラブ関連のデバッグ機能も有効にしていたというね。。

実行結果はこんな感じです。y軸の単位はクロック数です。赤い線でぴょこんと飛び出しているのは__slab_alloc()が呼ばれたんじゃないかなと。

f:id:masami256:20150922132327p:plain

これを最大5000クロック数まで表示するように変えて、棒グラフから折れ線グラフに変えてみたのがこちら。

f:id:masami256:20150922132438p:plain

中央値だとこうなりました。

関数名 中央値 平均
kmem_cache_alloc_bulk() 1858 1965.658
kmem_cache_alloc() 1990 2141.112

速度の差として出るだろう箇所としては、kmem_cache_alloc()のなかで実際にメモリを確保しているとこはslab_alloc_node()のこの部分でしょうか。スラブに空きがあってメモリ確保の要求に即座に答えられる場合はelse節に入ります。

 object = c->freelist;
    page = c->page;
    if (unlikely(!object || !node_match(page, node))) {
        object = __slab_alloc(s, gfpflags, node, addr, c);
        stat(s, ALLOC_SLOWPATH);
    } else {
        void *next_object = get_freepointer_safe(s, object);

        /*
        * The cmpxchg will only match if there was no additional
        * operation and if we are on the right processor.
        *
        * The cmpxchg does the following atomically (without lock
        * semantics!)
        * 1. Relocate first pointer to the current per cpu area.
        * 2. Verify that tid and freelist have not been changed
        * 3. If they were not changed replace tid and freelist
        *
        * Since this is without lock semantics the protection is only
        * against code executing on this cpu *not* from access by
        * other cpus.
        */
        if (unlikely(!this_cpu_cmpxchg_double(
                s->cpu_slab->freelist, s->cpu_slab->tid,
                object, tid,
                next_object, next_tid(tid)))) {

            note_cmpxchg_failure("slab_alloc", s, tid);
            goto redo;
        }
        prefetch_freepointer(s, next_object);
        stat(s, ALLOC_FASTPATH);
    }

    if (unlikely(gfpflags & __GFP_ZERO) && object)
        memset(object, 0, s->object_size);

    slab_post_alloc_hook(s, gfpflags, object);

仮に__slab_alloc()を全く呼ばずに済んだとすると、kmem_cache_alloc_bulk()との差分としてはthis_cpu_cmpxchg_double()とprefetch_freepointer()、それにstat()を呼んでいるというのがあります。その辺がクロック数の差としてでたのかな。あ、stat()はCONFIG_SLUB_STATSが設定されていないと空の関数なので最適化の過程で消えると思いますが。

暗号技術入門 第3版 秘密の国のアリス

暗号技術入門 第3版 秘密の国のアリス