読者です 読者をやめる 読者になる 読者になる

meryngii.neta

今日も新たな"ネタ"を求めて。

Curiously Recurring Template Pattern (CRTP) と Policy-based design

この記事はEEIC Advent Calender 2016の12月22日分として書かれた.

CRTPとは

C++でのジェネリックプログラミングで広く知られたパターンとして, Curiously Recurring Template Pattern (CRTP) というものがある. この名前は実態から大分かけ離れているので, CRTPが何たるかを要約しておくと次のようになる. 「基底クラスに派生クラスの型を与えると, 基底クラスから派生クラスを触ることができる.」 基本的にはそれだけのことである. それだけのことなのだが, なかなかに興味深い性質を持っているのでわざわざ名前が付いている.

template <typename Derived>
class base {
    // ...
};

class derived
    : public base<derived>
{
    // ...
};

CRTPという名前に「なってしまった」理由は, 1995年にJames O. Coplienという人物が同名の論文として発表したことによる. Coplien自身はCRTPのテクニックを発明したわけではなく, 複数のプログラマが独自に再発明しているのを見て これがある種のパターンではないかと気付いた,ということらしい. わざわざ「私が名付けるのは避けたい」とまで言っていることから, まさかこんな名前で広まるとは思っていなかったのだろう.

Curiously recurring template pattern - Wikipedia

ジェネリックプログラミングを実践しているプログラマの多くは, CRTPが何となく便利だとは認識していても, 「なぜ便利なのか」を認識していないかもしれない. Coplienの論文には,「何のためにCRTPを使うのか」が一言で明確に示されている.

That is, we encode the circular dependency directly using inheritance in one direction and templates in the other.

C++では継承によってクラス間の依存関係を表すことができるが, それは単方向の依存関係に限定される. 双方向な依存関係,すなわち循環依存をクラス間で表現する手法がCRTPであり, 片方を継承,もう片方をテンプレート引数として表現することで, 循環依存が発生しているクラスをコンポーネントに分解することが可能になる, というわけである.

「CRTPは静的多態性をうんたら」とかそういう論調のサイトをよく見かけるのだが, 12年前にCryolite先生が書いているように, 私もその説明には今ひとつ納得がいっていない. どちらかというと,CRTPはもっと一般的な手法で, 多態性の表現として使うのは応用の一種に過ぎない,という風に思っている. 循環依存が生じるパターンだったらおよそ何でも役に立つはずなのだ. 一応,オブジェクト指向の理論的枠組みでは CRTPのことを"F-Bounded Polymorphism"と呼ぶらしいが, 元論文がパッと見た感じ意味不明だったので誰か解読したら教えて欲しい.

細かい理屈は置いておいて,実用例を見ると理解が早くてよい. CRTPを活用した有名なライブラリとして, Boostのiterator_facade というテンプレートクラスを見てみる.

template <
    class Derived
  , class Value
  , class CategoryOrTraversal
  , class Reference  = Value&
  , class Difference = ptrdiff_t
>
class iterator_facade;

iterator_facadeは,自作イテレータを実装する際に要求される 各種関数を自動生成してくれるライブラリである. イテレータを定義するには, 似たような演算子を大量に定義する必要があって, そういう演算子を一つ一つ手で実装するのは時間の無駄である. しかし,だからといってイテレータという抽象化がダメだというわけではない. イテレータとして本来必要な機能はせいぜい「移動する」「デリファレンス」「比較する」ぐらいしかないので, それらの実装だけはコンテナごとに別々にしてインターフェイス部分を共通化することでイテレータを簡単に作れるようになるはずだ, というアイディアに基づくのがiterator_facadeである.

class int_iterator
    : public boost::iterator_facade<
        int_iterator,
        int,
        std::output_iterator_tag,
        int
    >
{
public:
    explicit int_iterator(int x) noexcept : x_{x} { }
    
private:
    friend boost::iterator_core_access;
    
    // 最低限の機能だけ実装する
    void increment() noexcept {
        ++this->x_;
    }
    int dereference() const noexcept {
        return this->x_;
    }
    bool equal(const int_iterator& other) const noexcept {
        return this->x_ == other.x_;
    }
    
    int x_;
};

int main()
{
    std::for_each(int_iterator{1}, int_iterator{5},
        [] (int x) { std::cout << x << " "; });
    
    // Output: 1 2 3 4 
}

iterator_facadeのドキュメント には,「なぜポリシーオブジェクトではなくCRTPが選択されたのか」について書いてある. ポリシーオブジェクトがなんなのかがこの文書だけでは釈然としなかったが, CRTP以外のやり方という文脈から察して, おそらく継承関係を逆にしたりコンポジションにしたりしたラッパのことを指しているのだと思う.

template <typename Iterator, /*...*/>
class iterator_facade : public Iterator { /*...*/ };

こういうラッパではなくあえてiterator_facadeでCRTPが使われている理由には,次の3つが挙げられている.

  • ラッパの生成やコピーにオーバーヘッドがかかる.
    • C++の規格上,空の基底クラスにはEmpty Base Optimization (EBO) が効いて,空間コストが必ず除去されることが保証される.
    • 現実的には,メンバを追加しない空の派生クラスでもオブジェクトのレイアウトが変わらない状況がほとんどな気はする.
  • 利用者がコンストラクタをカスタマイズできない.
    • これはそこそこ本質的な問題であろう.C++11だとInheriting Constructorsでマシになるが,根本的には解決していない.
  • パラメタライズされたイテレータ(例えばfoo_iterator<T>のようにTでパラメタライズされたもの)を作る時, (Template AliasesのないC++03以前では)foo_iterator_generator<T>::typeのように型生成テンプレートを経由する必要がある.
    • イテレータにはoperator++などが返す型が必ず同一でなければならないという規約があるので,ラッパだったとしても必ずiterator_facade<...>という型として扱わなければならない.
    • C++11だとTemplate Aliasesがあるのでこの手間はなくなった.しかし,最終的にユーザに見える型がiterator_facade<...>になってしまうことに変わりはない.

そもそものところ, 新しいイテレータを作る時にfoo_iteratorではなくて iterator_facade<...>という型になり, ユーザに内部の実装詳細であるはずのiterator_facadeが見えているということがおかしい. あくまで「イテレータインターフェイスを自動実装するクラス」を利用した「新しいイテレータクラス」を提供するわけで, ラッパというやり方は自然な依存関係の表現とは言い難いわけだ. しかし,実際のところインターフェイスを定義するために 最終的な派生クラスの型が要るから,それをテンプレート引数として 注入するのがCRTPの役割である,といえる.

興味深いこととして, CRTPには実行時のオーバーヘッドが全くない(ゼロオーバーヘッド抽象化)ので, iterator_facadeを使うかどうかについて性能面で悩む必要が全くない. 逆に言えば,CRTPを使う動機には「性能が絶対要件である」という前提が欠かせない. 性能がどうでもよいのなら,実際のところ仮想関数で何とかなる場合が多いからだ. こういった考え方こそC++らしいもので, それ故にCRTPはC++のライブラリ設計で頻出の手法になっている.

ちなみに,iterator_facadeも現在C++標準に提案されているらしい(採用されたわけではない). Ranges TSを精力的に開発しているEric Nieblerが著者の一人のようである.

Iterator Facade Library Proposal for Ranges

テンプレート引数地獄

必ずしもCRTPに限らないが, テンプレートをフル活用したプログラムにありがちな問題として, 「テンプレート引数の数が増えまくる」というものがある. 再びiterator_facadeを例に考えると, 後ろ2個のテンプレート引数にはデフォルト値があるので, 利用者からは最低限3個の引数を指定すればよい,ということになっているが, テンプレート引数が5個であることに変わりはない. このことは, ライブラリを利用する立場だとコンパイル時・リンク時・デバッグ時に表面化する. 例えば,上のコードでoperator++の実体は

boost::iterators::detail::iterator_facade_base<int_iterator, int,
    std::__1::output_iterator_tag, int, long, false, false>::operator++()

という名前である. int_iteratorの利用者からみて, longだのfalseだのの情報は明らかにいらないし, デバッグ中に見たい情報ではない. 残念ながらこれでもまだ単純すぎる例で, さらに少し複雑なコードになると画面が埋め尽くされるほどの型情報が コマンドを叩く度にわらわらと出て来る. 特にデバッグ中に見たいのは型情報よりも変数の値であって, そういう冗長な型情報は端的にいってデバッグの邪魔である.

ライブラリを開発する側もつらい. 例えば,iterator_facadeのコードを少し引用すると,こんな感じのがあちらこちらにある.

#  define BOOST_ITERATOR_FACADE_INTEROP_HEAD_IMPL(prefix, op, result_type, enabler)       \
    template <                                                              \
        class Derived1, class V1, class TC1, class Reference1, class Difference1 \
      , class Derived2, class V2, class TC2, class Reference2, class Difference2 \
    >                                                                       \
    prefix typename mpl::apply2<result_type,Derived1,Derived2>::type \
    operator op(                                                            \
        iterator_facade<Derived1, V1, TC1, Reference1, Difference1> const& lhs   \
      , iterator_facade<Derived2, V2, TC2, Reference2, Difference2> const& rhs)

Boostのコードはプリプロセッサの闇魔術で色々圧縮されているものの, 「テンプレート引数が多すぎる」という本質的な問題は 残されたままである. 例えば,iterator_facadeのテンプレート引数にもう一つ新規に追加するとしたら?…… ヘッダ中のあちらこちらを直さなければいけないだろうし, 苦痛以外の何者でもない.

テンプレート引数の羅列は順序に依存している ということも問題である. 例えばiterator_facadeに与えるテンプレート引数のどれかを交換すれば, おびただしい数のコンパイルエラーになること間違い無しである. 『C++の設計と進化』には,C++の設計過程で順序依存をなくす工夫が なされていることがあちこちに書いてある. 順序依存は可能な限り避けるべきであり, そして推奨されるプログラミングパターンに含まれているべきでもない.

循環参照との戦い

そもそもなぜValueだのCategoryOrTraversal だのといった型を渡さなければならないのか? どうせDerivedを渡しているのだから,Derivedの中で型を定義してしまえば, 基底クラスはそのメンバを参照できるはずじゃないか, と思うのは自然な発想である.

template <typename Derived>
class iterator_facade
{
public:
    Derived& operator += (typename Derived::difference_type n); // エラー
    // ...
};

しかし,残念ながら,C++の言語仕様上このコードはコンパイルできない. Derivedがincomplete typeとかそういうエラーが出るだろう. なぜかというと,派生クラスのメンバにアクセスするためには, 基底クラスのメンバの型やシグネチャ*1が確定していないといけないからである. すなわち,基底クラスのメンバ関数シグネチャが 派生クラスのメンバに依存するようなコードは,そもそも書けない. そのため,CRTPが活躍できるのは,あくまで 「基底クラス内のメンバ関数の"実装"が派生クラスに依存している」 という状況に限られている.

よく知られた回避策は2つある.

  • 基底クラスのテンプレート引数を増やす.
  • メンバテンプレートや戻り値型推論を活用することで,型の依存関係を逆向きに変える.

1番目の解決策は先ほどまで述べた通りである. 2番目の解決策だと,例えばこんな感じになる.

template <typename Derived>
class iterator_facade
{
public:
    template <typename Difference>
    Derived& operator += (Difference n);
    // ...
};

この場合だとなんとかうまくいくのだが, 一般にはこれでは対処しきれない場合が存在する. 最も単純な例としては,

template <typename Derived>
class foo
{
    typename Derived::value_type val_;
};

というように,基底クラス内で依存型を実体化しなければならない場合に起きる. こういうコードは普通にあり得るので, 2番目の解決策は即座に限界を迎える. また,そもそもメンバテンプレートでなかった単純なメンバ関数が, 依存性解決のためだけにテンプレートになって複雑性が増大しているのも問題である. こうしてテンプレート引数が増えると, 同じようにデバッグ時などに困るだけである.

こういう状況に何度も直面した後,私は次のような考えに至った. シグネチャを確定させるための型情報は, CRTPを積極的に活用する上でやむを得ず必要になるので, それらは事前に与える以外の解決策がない. そして,そのための最も単純で使いやすい手法は, 「CRTPに使うためのDerivedもまとめて」丸ごと一つのテンプレート引数として与える, というものである.

template <typename P>
class iterator_facade
{
    using derived_type      = typename P::derived_type;
    using value_type        = typename P::value_type;
    using category_type     = typename P::category_type;
    using reference_type    = typename P::reference_type;
    using difference_type   = typename P::difference_type;
    
public:
    // ...
};

先ほどまでDerivedというテンプレート引数を与えていた代わりに, 派生クラス自身ではない型にderived_typeというメンバを持たせることで, テンプレート引数に陽に現れない形で間接的にCRTPを実現する. そして,シグネチャを確定させるための情報も一緒にその型に持たせてしまう. たったこれだけの変更だけで, テンプレート引数が増大する問題を回避しながら, CRTPも活用できるようになるのである.

ところで,Pとは何だろうか? これはまさしく次に解説する ポリシー (Policy) そのものであるといえる.

CRTPとPolicy-based design

ポリシーを使った設計それ自体は,何ら新しいことではない. 2001年に出版された『Modern C++ Design』(略称MC++D) *2 という本には, ポリシーを使ってスマートポインタを構成するといった, 当時としては相当先進的なジェネリックプログラミングのスタイルが示されている. しかし,そうはいっても,MC++Dが出版されて既に15年以上経っているわけで, ポリシーが新規の概念かというとそういうことはない.

template <
    typename T,
    template <class> class OwnershipPolicy,
    class CoversionPolicy,
    template <class> class CheckingPolicy,
    template <class> class StoragePolicy
>
class SmartPtr;

このスマートポインタは, ポリシーを切り替えるだけで参照カウント方式や 暗黙変換の方式などを変更することができる. 様々なスマートポインタに共通の処理だけがSmartPtrに実装されていて, 例えばアトミック変数で参照カウントを制御するといった詳細は ポリシーに分離されている.

MC++Dで解説された「ポリシー」と この記事で述べている「ポリシー」が 一つ決定的に違っているのは,ポリシーとして テンプレートクラスではなくただのクラスを用いていることである *3. テンプレートクラスを受け取る形式は, 上の例だと利用者がTを指定する必要が無いという利点があるが, しかしテンプレート引数が増える問題は解消できていない. 実際のところ,利用者は既にTの実体を知っているわけだから, ポリシーとして与える際にTを知っている前提で型を定義することに それほど手間がかかるわけではないのだ.

PolicyとTraits

C++11の標準ライブラリにType Traits (型特性)があるので, Traitsは比較的知られている概念だと思う. 型同士の相関を考えるという性質上, TraitsとPolicyはよく似ているので紛らわしいが, 違いについて下のように解説がある.

What is the difference between a trait and a policy?

Traitsとは要するに「既存の型から情報を引き出す」操作, Policyは逆に「情報をクラスに入れ込む」操作であるといえる. この解説の例ではstd::allocator<T> がPolicyの例として挙げられている. アロケータはコンテナに「入れ込む」形になっているわけで, よくよく考えてみるとSTLの中にもポリシーは使われている.

ポリシーの合成,入れ子

ポリシーを型として実装した場合, 直交したポリシーを合成するのは非常に簡単で, 多重継承を使えばよい. 以下の例だと,アロケーションとロガーを別々のポリシーとして定義し, それらを一つにまとめている.

template <typename Policy>
struct my_list_base
{
    using derived_type = typename Policy::derived_type;
    using value_type = typename Policy::value_type;
    
    struct elem {
        elem* next;
        value_type val;
    };
    
public:
    void add(const value_type& val) {
        const auto ptr =
            static_cast<elem*>(
                Policy::allocate(sizeof(elem))
            );
        
        ptr->next = head_;
        ptr->val = val;
        head_ = ptr;
        
        Policy::add_log("added element");
    }
    
    void traverse() {
        for (auto cur = head_; cur != nullptr; cur = cur->next) {
            this->derived().on_traverse(cur->val);
            Policy::add_log("traversing...");
        }
    }
    
protected:
    my_list_base() : head_{} {}
    
private:
    derived_type& derived() noexcept {
        return static_cast<derived_type&>(*this);
    }
    
    elem* head_;
};

struct my_allocation_policy {
    static void* allocate(std::size_t size);
    static void deallocate(void*);
};
struct my_logging_policy {
    static void add_log(const char*);
};

template <typename T>
class my_list;

template <typename T>
struct my_list_policy
    : my_allocation_policy
    , my_logging_policy
{
    using derived_type = my_list<T>;
    using value_type = T;
};

template <typename T>
class my_list
    : public my_list_base<my_list_policy<T>>
{
private:
    friend my_list_base<my_list_policy<T>>;
    
    void on_traverse(T val) {
        std::cout << ++count_ << " " << val << std::endl;
    }
    
    std::size_t count_ = 0;
};

このパターンをより複雑なものに一般化した場合, ポリシーを前提に階層的なクラス設計を行う際に, 基底クラスに近づくにつれて「ポリシーが大きくなっていくような」ものになる. 例として,侵入型参照カウンタintrusive_ptrの実装について考えてみる.

template <typename T>
class my_intrusive_ptr {
    // ...
};

template <typename Policy>
class ref_counted_base
{
private:
    using derived_type = typename Policy::derived_type;
    using ref_count_type = typename Policy::ref_count_type;
    
    friend my_intrusive_ptr<derived_type>;
    
    void acquire_ref() {
        Policy::increment(ref_count_);
    }
    void release_ref() {
        if (Policy::decrement(ref_count_) == 1) {
            delete &derived(); // thisをdeleteするアグレッシブさ
        }
    }
    
    derived_type& derived() noexcept {
        return static_cast<derived_type&>(*this);
    }
    
    ref_count_type ref_count_{1};
};

template <typename Policy>
struct atomic_ref_counted_policy
    : Policy
{
    using ref_count_type = std::atomic<std::size_t>;
    
    static void increment(ref_count_type& count) {
        count.fetch_add(1, std::memory_order_seq_cst);
    }
    static std::size_t decrement(ref_count_type& count) {
        return count.fetch_sub(1, std::memory_order_seq_cst);
    }
};

template <typename Policy>
class atomic_ref_counted_base
    : public ref_counted_base<atomic_ref_counted_policy<Policy>>
{
    // ...
};

class my_class;

struct my_class_policy {
    using derived_type = my_class;
};

class my_class
    : public atomic_ref_counted_base<my_class_policy>
{
    // ...
};

この例だと,継承の真ん中にある atomic_ref_counted_baseは,元あるポリシーを拡張して, ref_count_typeといった新しいメンバを導入している. 大したコード量でないのでありがたみが分かりにくいが, アトミック変数という実装詳細を後回しにして, ref_counted_baseには「侵入型参照カウントを実装するとしたらこうなる」というコード片を CRTPを駆使して括り出せている,ということが分かる. そして,atomic変数を使うことを確定させた時, 実装すべきはincrementといったより細分化された部品だけになっている.

循環依存の関係にあるわけだから, 継承が一段深くなる度にポリシーは逆方向に継承されていくのは自然であるといえよう. あるいは,継承関係が逆転することは, 「制御の反転」 の一形態と見ることもできる. ただし,あまりこういう複雑な継承関係を組み合わせすぎると意味不明になるので, やりすぎない程度にすることも大事である.

ここまでを俯瞰してみると, ポリシーというのは「そのクラス専用の名前空間のようなものを定義している」 と考えることもできる. 言い換えると,クラスが依存するコンテキストを限定することができ, 依存関係を最小化できる. そして,C++のクラスあるいは型というのは, ほとんど何でもできると言って良いぐらい色んな物を持てるから, 受け取る型が一つでも表現能力の面で困ることがない.

  • 普通の関数 (staticメンバ関数)
  • 普通の値(constexpr or 変数)
  • 変数メンバ
  • メンバ関数
  • 型(typedef または 入れ子クラス)
  • テンプレートクラス(メンバテンプレートクラス または テンプレートエイリアス

そんなわけで,型を一つポンと渡すことでポリシーを表現するやり方は, シンプルな割に表現力も問題がないので便利だと思う.

CRTP共通基底クラス

CRTPでは大抵の場合,derived()メンバを定義するのだが, ポリシーが必ずderived_typeという型を持つとしたら, この作業も共通化してしまえる. まあ実際のところ無くても大して問題ではないが,コードの重複は少し減らせる.

template <typename Policy>
class crtp_base
{
protected:
    using derived_type = typename Policy::derived_type;
    
    derived_type& derived() noexcept {
        return static_cast<derived_type&>(*this);
    }
    const derived_type& derived() const noexcept {
        return static_cast<const derived_type&>(*this);
    }
};

動的なダウンキャストについては, Boostにはpolymorphic_downcastという小粒なライブラリと同じ手法を使えばチェックできる. その動きを簡単に説明すると,デバッグ時にはdynamic_castを使って, リリース時にはstatic_castを使う,というものである. 但し,ポリモーフィックでない型(=仮想関数が無いクラス)の場合は コンパイル時に怒られるので,仮想デストラクタを強制的に付けてRTTIが動くようにする必要がある.

    #ifdef ENABLE_POLYMORPHIC_CAST
    virtual ~crtp_base() = default;
    #endif

    derived_type& derived() noexcept {
        #ifdef ENABLE_POLYMORPHIC_CAST
            auto p = dynamic_cast<derived_type*>(this);
            if (!p) {
                throw std::bad_cast{};
            }
            return *p;
        #else
            return static_cast<derived_type&>(*this);
        #endif
    }

例えばこんなミスを自動検出できる.

template <typename Policy>
struct printer_base : crtp_base<Policy> {
    void print() {
        std::cout << this->derived().x << std::endl;
    }
};

struct int_wrapper;
struct double_wrapper;

struct int_wrapper_policy {
    using derived_type = double_wrapper; // Broken
};
struct int_wrapper : printer_base<int_wrapper_policy> {
    int x = 123;
};
struct double_wrapper_policy {
    using derived_type = double_wrapper;
};
struct double_wrapper : printer_base<int_wrapper_policy> { // Broken, too
    double x = 123.456;
};

void f() {
    int_wrapper w;
    w.print();
    // int_wrapper -> printer_base<int_wrapper_policy> -> double_wrapper
}

デバッグ時とリリース時でオブジェクトのレイアウトが変わってしまうのは深刻な問題なので, 普遍的に役立つかというと微妙ではある. 上で示しているように必要なときだけ#ifdefで有効にするか, ポリモーフィックな型だけをメタプログラミングで判定して チェックしたりといった使い方になるだろうか.

ポリシーでプロトタイピング

ここまでを踏まえて,最後により汎用的なプログラミングスタイルについて述べる. ポリシーの利用用途は上のような特殊なライブラリに限られたものではなく, ほとんど何にでも使える. ポリシーを使うと,C++のような静的型付け言語であっても トップダウンに素早くプロトタイピングすることが可能である.

ものすごく単純な例だが, 入力が少しずつ入ってくるタイプのtokenizer (カンマなどで文字列を切断するやつ)について考えてみる. まず何も考えずにクラスの名前を決める.

template <typename Policy>
class tokenizer_base
    : public crtp_base<Policy>
{
    // ???
};

まず文字を受け取る関数が要るだろうから, addみたいな関数を追加する. となると,引数には文字を受け取るだろうから, character_typeみたいな型をポリシーに要求する.

template <typename Policy>
class tokenizer_base
    : public crtp_base<Policy>
{
    using character_type = typename Policy::character_type;
    
public:
    void add(character_type ch) {
        // ???
    }
};

新しい文字が来たら,デリミタ(切り分ける文字)かそうでないかのどっちかで, デリミタじゃなかったら単にバッファに溜めるみたいな処理になるはずである. デリミタかどうかの判断は派生クラスに丸投げして,バッファの型も追加する.

template <typename Policy>
class tokenizer_base
    : public crtp_base<Policy>
{
    using character_type = typename Policy::character_type;
    using token_buffer_type = typename Policy::token_buffer_type;
    
public:
    void add(character_type ch) {
        auto& self = this->derived();
        
        if (self.is_delimiter(ch)) {
            // ???
        } else {
            // ???
        }
    }
    
private:
    token_buffer_type buf_;
};

バッファは「文字を追加できる何か」だから とりあえずvectorみたいなやつと考えることにして, トークンができた後の処理は派生クラスに丸投げすることにする. あとバッファがたまったままだと困るので,finishみたいなやつも加える.

template <typename Policy>
class tokenizer_base
    : public crtp_base<Policy>
{
    using character_type = typename Policy::character_type;
    using token_buffer_type = typename Policy::token_buffer_type;
    
public:
    void add(character_type ch) {
        auto& self = this->derived();
        
        if (self.is_delimiter(ch)) {
            self.on_new_token(buf_);
            buf_.clear();
        } else {
            buf_.push_back(ch);
        }
    }
    
    void finish() {
        this->derived().on_new_token(buf_);
    }
    
private:
    token_buffer_type buf_;
};

ポイントは,「ここでコンパイルにかけてしまう」ことである. まだvectorを使うとか何も言っていないわけだが, プロトタイピングをしている時は最終的な型がどうなるのか分からないので, 型が確定していないのも当たり前だ. しかし,テンプレートのコードだから型を確定させなくてもこれだけでコンパイルはできる. C++コンパイルエラー地獄でよく知られている言語であるので, C++プログラマコンパイラと仲良くなる方法を知るべきであり, その一つがシンタクスエラーなど単純なエラーを早い段階で潰しておくことである.

このクラスを実際に使うとなったら,実際にポリシーの実体と派生クラスを作る. この時初めてテンプレートが実体化されて, シンタクスよりも具体的なエラーが出てくるようになる.

class char_tokenizer;

struct char_tokenizer_policy
{
    using derived_type = char_tokenizer;
    using character_type = char;
    using token_buffer_type = std::vector<char>;
};

class char_tokenizer
    : public tokenizer_base<char_tokenizer_policy>
{
public:
    void show_all() {
        for (const auto& token : tokens_) {
            std::cout << token << std::endl;
        }
    }
    void set_delimiter(char delim) {
        delim_ = delim;
    }
    
private:
    friend tokenizer_base<char_tokenizer_policy>;
    
    bool is_delimiter(char c) {
        return c == delim_;
    }
    
    void on_new_token(const std::vector<char>& v) {
        tokens_.emplace_back(std::begin(v), std::end(v));
    }
    
    char delim_ = ',';
    std::vector<std::string> tokens_;
};

このような順番で考えていくと, 自然とtokenizer_baseから実装詳細が抜き取られ, 逐次的にトークン処理をする際に最低限必要なものだけが書かれていることが分かる. 必要に応じて基底クラスと派生クラスの仕様をすり合わせていくと, 分離されたモジュールとしていい具合に整備されてくるはずである. そして何より大事なのが,こういう抽象化はオーバーヘッドなしに行える, ということである.

まとめ

  • CRTPはモジュール間の循環依存問題を解消する.
  • テンプレート引数が増えるテクニックは望ましくない.
  • CRTPとポリシーを組み合わせるパターンは極めて有用である.
  • テンプレートのコンパイルエラーに負けない強い心を持つ.

明日のEEIC Advent Calendarは@nobi_sannです.

*1:私としては経験的には何に依存しているのか予想できるのだが,規格上の厳密な依存の範囲はよく知らない.Coplienの論文だと"structure"と呼ばれていた.

*2:Modern C++ Designは現在ではあまりModernではないが,ジェネリックプログラミングの歴史において重要な一冊である.

*3:さらにもう一つの別のやり方として,private継承を用いるものもある.

Uniform initialization syntaxの括弧問題についてのぼやき

C++11で導入されたUniform initialization syntaxは、C++の変数宣言にまつわる諸問題 (組み込み型とクラスの違い、Most Vexing Parse、narrow conversion)を解決する機能である。 しかし、Effective Modern C++でも取り上げられているように、 波括弧 {}だけではstd::vectorのようなクラスで呼び出せないコンストラクタが存在するという新たな問題が発生している。

std::vector<int> a(3, 123); // 3要素のベクタ: {123, 123, 123}
std::vector<int> b{3, 123}; // 2要素のベクタ: {3, 123}
std::vector<int> c{{3, 123}}; // 2要素のベクタ: {3, 123}

原因は周知の通り、std::vectorが「整数引数2個を受け取るコンストラクタ」と 「std::initializer_listを受け取るコンストラクタ」の両方を持っているが、 オーバーロード解決時にUniform initialization syntaxはinitializer_listをやたらと好むので、 前者のコンストラクタに一致させられないというものである。 特にテンプレート内の変数宣言では構築する型が分からない場合もあるので、 どちらを使うべきかという悩ましい問題になる。

どうもこの問題は言語設計上のミスだったのではないかという疑いを持ってしまう。 どう考えても普通のプログラムで一番よく使うのは普通のコンストラクタの呼び出しで、 それらに比べればstd::initializer_listを使うことは比較的稀だ。 丸括弧 ()を置き換えるためにUniform initialization syntaxを使うと、 我々はそこで暗黙のうちに「できるだけinitializer_listにマッチしよう」と主張してしまうことになる。 私の記憶では、当初Uniform initialization syntaxはあくまでInitializer listsの提案のおまけのようなものだったから、 こういう仕様になってしまった流れになった理由は何となく推察はできる。

ちなみに上の例だとbcも同じ意味になる。 bは他のコンストラクタよりも優先してinitializer_listを探し、 cはbraced-init-listを1つ取るコンストラクタを探し、 結果的に同じものにマッチすることになる。 私が思うに、bのような配列の初期化を認めず、 常にcのような形でinitializer_listを使うことを強制すればよかったのではないかという気がする。 配列を初期化するときは常に波括弧を2つ使うことになるが、 配列の初期化なんてそう頻繁に表れるわけではないのだから2文字増えるぐらいよいではないか、と思う。

まあでも言語機能として導入されてしまった以上、それを考慮してコードにするしかない、 という結論しか出ないのだが・・・。

追記

数年前に議論されていたらしい.

vector/arrayとUniform initialization+Initializer list - yohhoyの日記

Non-type template parametersの威力

数年ぶりにC++関連のまとまった記事を書こうという気になった。 最近の私はというと研究で毎日C++を書いていたので記事にできそうなネタはたくさんあるのだが、 それを公開するのは若干面倒なので出し渋っていたところがある。 加えて、C++嫌いな人々の根も葉もない批判によって、 毎日強烈なストレスを受けてきたのも制約の一つだったかもしれない。 まあここで愚痴愚痴言う気もあまりないのだが、 プログラマが落ち着いて物事を考えるには周りの環境はとても大事ってわけだ。

この記事では、直近1年ぐらいで私が重要だと感じるようになった、 Non-type template parameters (型ではないテンプレート引数)を使ったイディオムをいくつか紹介しようと思う。 面白いことに、今回紹介する抽象化手法には実行時オーバーヘッドが全く無いため、 上手く組み合わせることで安全で生産的かつ高速なコードを記述することができる。

C++03までは、Non-type template parametersの重要な用途としてコンパイル時計算があったが、 そういった用途はC++11以降ではconstexprでほとんど置き換えられている。 今回紹介するのはコンパイル時計算とは異なった用途の例である。

アダプタ関数の生成

よくあるケースで、次のようなコールバック関数を要求するAPIがあるとしよう。

void set_callback(void (*func)(int, void*), void* data);

この例のfuncは引数にユーザ定義のvoid*を取ることができるようだ。 例えば、ユーザは以下のように任意の型を定義して、それに対応したコールバック関数を定義する。

// ユーザ定義型
struct my_data
{
    int val;
};

// コールバック関数
void my_handler(int info, void* ptr)
{
    my_data* data = static_cast<my_data*>(ptr); // キャストが要る
    data->val = info; // イベントのデータを自分で用意した領域にコピー
}

my_data g_data; // コールバックに書き込んでもらうデータ

void g()
{
    set_callback(my_handler, &g_data); // コールバックを設定
}

上のコードには特に問題はない。 しかし、このset_callbackという関数はvoid*を受け取っており、 タイプチェックが行われないので、以下のような危険なコードを簡単に書けてしまう。

double x;
set_callback(my_handler, &x); // ptrはmy_data*型だったはずだが・・・

本題と少し逸れるが、static_castというのは(Cの"包容力の高い"キャストよりは大分マシとはいえ) 実際のところ大して安全ではないということも分かる。

T* -> (暗黙のキャスト) -> void* -> (static_cast) -> U*

というように2段階にキャストすると、 reinterpret_cast相当のことが簡単にできてしまう。 (実際にこの罠に嵌っている人を見たことがある。)

T* t = /*...*/;
void* v = t; // 暗黙のキャスト
U* u = static_cast<U*>(v); // reinterpret_cast<U*>(t)と同じ

どうすればこのようなミスを防げるだろうか? コールバックを登録する際には、関数の型とデータの型を比較できるはずだから、 本来はset_callbackの呼び出し時点で静的タイプチェックができるはずなのである。 やりたいこととしては、テンプレートを使ってこんなラッパー関数を書くことである。

// タイプセーフなラッパー関数
template <typename T>
inline void set_callback_typesafe(void (*func)(int, T*), T* ptr)
{
     set_callback(func, ptr); // ←タイプエラー
}

void my_handler(int info, my_data* ptr);

void g()
{
    set_callback(my_handler, &g_data);
}

しかし、実際には矢印の場所でタイプエラーが起きる。 funcの型が、元の関数だとvoid (*)(int, void*)だったのに、 ラッパー関数ではvoid (*)(int, my_data*)になってしまっており、シグネチャが合っていない。

いい加減な解決方法としては、無理やりvoid (*)(int, void*)にキャストしてしまうという方法がある。

template <typename T>
inline void set_callback_typesafe(void (*func)(int, T*), T* ptr)
{
     set_callback(reinterpret_cast<void (*)(int, void*)>(func), ptr); // 危うい
}

型に無頓着なC言語プログラマがいかにも好きそうな方法だ。 ほとんど全てのアーキテクチャにおいて、ポインタはその指す先の型に関わらずただのアドレスだから、 実際に実行しても多分何もエラーは出ないだろう。 しかし、複雑なコードになればなるほど、こういうキャストをばらまいたコードは信頼出来ない品質になっていくだろう。

ランタイムの性能を犠牲にできるのなら解決は容易である。 例えばこんな風にする。

template <typename T>
struct Temp
{
    void (*func)();
    T* ptr;
};

template <typename T>
void transfer(int data, void* ptr)
{
    Temp<T>* temp = static_cast< Temp<T>* >(ptr);
    temp->func(temp->ptr);
}

template <typename T>
inline void set_callback_typesafe(void (*func)(int, T*), T* ptr)
{
    Temp<T> p = new Temp<T>;
    p->func = func;
    p->ptr = ptr;
    set_callback(&transfer<T>, p);
}

このコードは安全だし、実際正しく動く保証がある。 しかし、実行時性能は(無理やりキャストする案と比べて)ひどく悪くなった。 funcは関数ポインタとして一時的に格納されるから、インライン化は事実上不可能になり、 2段階の関数コールが走る。 関数ポインタを一時的に格納するために動的メモリ確保が必要だし、 しかもポインタ2個分のメモリを余計に食っていることになる。 実際には機械語として出力されないはずのキャストを省略するためのコストとしては、あまりに大きすぎる。

ではどうするか。我々は無念にもvoid*に屈し、キャストをばらまいた型安全でないコードを書くしかないのか? いや、そんなことはない。 Non-type template parametersを使えば、次のようなラッパ関数を書くだけでよいのだ。

template <typename T, void (*Func)(int, T*)>
void transfer(int data, void* ptr)
{
    Func(data, static_cast<T*>(ptr));
}

template <typename T, void (*Func)(int, T*)>
inline void set_callback_typesafe(T* ptr)
{
    void (*func)(int, void*) = &transfer<T, Func>;
    set_callback(func, ptr);
}

そしてこれをこんな風に使う。

void my_handler(int, my_data*);

my_data g_data;

void g()
{
    set_callback_typesafe<my_data, &my_handler>(&g_data);
}

Funcはテンプレートパラメータだから、コンパイル時に決定される。 そのため、transferにはコンパイル時に関数が渡る。 set_callback_typesafeの利用者は、 全くキャストを使わずに静的タイプチェックをしながら、 型のない世界にコールバックを渡せるのである。

関数をコンパイル時に渡すことで何が嬉しいかというと、 コンパイル時に呼び出す関数が決まるからコード中(要するにアセンブリ中のテキスト領域)に関数を埋め込めることである。 さらに、インライン化できる場合はそもそも関数呼び出しが消滅し、ラッパ関数のみが残る。 結果的に、インライン化が行われればオーバーヘッドがゼロ、ということが実現できる。 2個の関数呼び出しをコンパイル時に1個に融合させた、ともいえる。

「なんだ、キャストがなくなっただけじゃないか」と思う人もいるだろうが、 この手法は別にキャストに限った話ではなく、応用範囲は広い。 例えばこんな風に

// 指定したコールバックを10回呼び出す謎のアダプタ関数
template <typename T, void (*Func)(int, T*)>
void transfer_10(int data, void* ptr)
{
    for (int i = 0; i < 10; ++i)
        Func(data + i, static_cast<T*>(ptr));
}

template <typename T, void (*Func)(int, T*)>
void set_callback_10(T* ptr)
{
    void (*func)(int, void*) = &transfer_10<T, Func>;
    set_callback(func, ptr);
}

void h()
{
    set_callback_10<my_data, &my_handler>(&g_data);
}

関数の前処理/後処理を入れたり、複数の関数を合成したり、呼び出し回数や条件を変えたりなど、 要するに関数をラップする色んな転送関数がオーバーヘッドなしに作れる。 この特長は、コールバック関数に典型的なパターンを共通化して、 ジェネリックかつ高速なライブラリを作る際に特に役に立つだろう。

インライン化の促進

コールバック関数のインライン化に重点をおいた話として、Cryolite先生が興味深いコードを載せていた。

http://d.hatena.ne.jp/Cryolite/01000831

STLアルゴリズム等に述語として関数ポインタを渡した際に、 コンパイル時に呼び出す関数が決定されるにも関わらず、関数ポインタの指す関数のインライン化が行われないことがある。 最近のGCCやClangは賢いのでそのような問題はないが、古めのコンパイラを使った場合に、 関数ポインタが引き回された際の解析を途中で諦めてしまうのである。

そこで、関数ポインタをNon-type template parametersとした関数オブジェクトを作るという手法がある。 すると、関数ポインタが型にエンコードされるので、手で関数オブジェクトを書いた時と同様の状況となり、インライン化が促進される。

元の記事のコードは後方互換性のために若干複雑なので、C++11で簡略化したコードを載せておく。 C++03ではVariadic Templatesが使えないので引数を並べる必要があるが、依然として実装は至って単純である。

template <typename Sig, Sig Func>
struct inline_function;

template <typename R, typename... As, R (*Func)(As...)>
struct inline_function<R (*)(As...), Func>
{
    template <typename... Args>
    R operator () (Args&&... args) const {
        return Func(std::forward<Args>(args)...);
    }
};

// マクロにせずにfを2度書かなくて済む方法があったら教えてほしい
#define MAKE_INLINE_FUNCTION(f) (inline_function<decltype(f), (f)>())

ちなみに、今となっては過去のイディオムだが、 MAKE_INLINE_FUNCTIONは、decltypeのないC++03においても実装できる。 初めて見た時はなかなかマジカルな手法だと思った。

// C++03

template <typename Sig>
struct deducer {
    template <Sig Func>
    inline_function<Sig, Func> make() {
        return inline_function<Sig, Func>();
    }
};

template <typename Sig>
inline deducer<Sig> make_deducer(Sig /*ignored*/) {
    return deducer<Sig>(); // 実行時の関数ポインタを捨てるのがミソ
}

#define MAKE_INLINE_FUNCTION(f) (make_deducer(f).make<f>())

こうして定義したinline_functionは、こんな風に使う。

// 与えられた関数を10回呼び出すジェネリックな関数
template <typename Func>
void f(Func func)
{
    for (int i = 0; i < 10; ++i)
        func(i);
}

void g(int x)
{
    std::cout << x << std::endl;
}

// ...

f(&g); // 古いコンパイラだとインライン化されないかも
f(MAKE_INLINE_FUNCTION(&g)); // ほぼインライン化される

近年のコンパイラは賢いので、このテクニックによるインライン化の促進自体はさほど重要ではないだろう。 しかし、依然として静的解析の弱いコンパイラは存在するので覚えておいて損はない。

なお、最初のコールバックを登録する話題の中で、利用側で毎回シグネチャを渡していたが、この話と混ぜると少しだけ短くできる。 関数を型にエンコードする部分は分離したほうが分かりやすい気がする。

template <typename T, void (*Func)(int, T*)>
inline void set_callback_typesafe(inline_function<void (*)(int, T*), Func> /*ignored*/, T* ptr)
{
    void (*func)(int, void*) = &transfer<T, Func>;
    set_callback(func, ptr);
}

void f(int, my_data*);

my_data g_data;

void g()
{
    set_callback_typesafe(MAKE_INLINE_FUNCTION(&f), &g_data);
}

グローバル変数へのポインタ

グローバル変数は、スコープを飛び越えてありとあらゆる関数に副作用をもたらす邪悪な存在である。 一方で、C++プログラマはしばしば忘れがちであるが、 構造体メンバにポインタ経由でアクセスするよりも、 グローバル変数に直接アクセスする方が高速になる場合が多い。 グローバル変数だと、構造体メンバのアドレスをコンパイル時に決定できるので、 ポインタの値をメモリからロードしたり、オフセットを足しこんだりする命令が不要になるためである。

単純化した例として、次のようにクラスfooをクラスbarに集約して、 barに転送関数を記述したコードを考える。

class foo
{
public:
    void set(int val) { val_ = val; }

private:
    int val_;
};

class bar
{
public:
    bar(foo* f) : f_(f) { }
    
    void set(int val) const {
        f_->set(val);
    }

private:
    foo* f_;
};

foo g_foo;
bar g_bar(&g_foo);

void set_value_1(int val) {
    g_foo.set(val);
}
void set_value_2(int val) {
    g_bar.set(val);
}

これをClangでコンパイルすると、次のようなアセンブリが吐かれる。

__Z11set_value_1i:
        # ...
        movl    %edi, _g_foo(%rip)  # 直接g_foo.val_に代入
        # ...

__Z11set_value_2i:
        # ...
        movq    _g_bar(%rip), %rax  # g_bar.f_の値をロード
        movl    %edi, (%rax)        # 間接的にg_foo.val_に代入
        # ...

g_bar.f_の値は実行中に書き換わるかもしれないから、 コンパイラとしては必ずf_を一度ロードして、 その結果得られたアドレスを使わなければいけない。 しかし、f_の指す先が特定のグローバル変数だと決まっているのなら、 このような間接参照は無意味であるし、コンパイル時に取り除くべきである。

この例だと、g_bar.f_が定数であることを明示すれば、 コンパイラは冗長なポインタのロードを除去できる。

const bar g_bar(&g_foo); // g_bar.f_をconstにした

void set_value_2(int val) {
    g_bar.set(val);
}
__Z11set_value_2i:
        # ...
        movl    %edi, _g_foo(%rip)
        # ...

というわけでconstをたくさん付けましょうで話が済めばいいのだが、実際のところそう簡単ではない。 少なくともClangでは、デストラクタを加えて少し複雑にしただけで、 再び不要なポインタのロードが発生してしまうのである。

class bar
{
public:
    bar(foo* f) : f_(f) { }
    
    // デストラクタを加える (実際のところ空でも同様)
    ~bar() { std::cout << "~bar()" << std::endl; }
    
    void set(int val) const {
        f_->set(val);
    }

private:
    foo* f_;
};

foo g_foo;
const bar g_bar(&g_foo);

void set_value_2(int val) {
    g_bar.set(val); // g_bar.f_をロードしてしまう
}

デストラクタを加えたところで、f_コンパイル時に決定されることに変わりはないから、 コンパイラは依然としてg_bar.f_のロードを取り除けるはずなのだが、そうはならない。 この現象については私も正確な理由が分からないのだが、可能性としては次のように考えられる。 プログラム終了時にグローバル変数のデストラクタを呼ぶためには、事前に__cxa_atexitを使ってデストラクタを登録する必要がある。 デストラクタの登録作業もコンストラクタの仕事の一部と考えれば、 barは定数で初期化できないということになる。 しかし、この理屈だと、デストラクタが空でも同じ理由が説明できていない。

いずれにせよ、我々がコンパイラに伝えたいことは、 グローバル変数を指して初期化したので間接参照を省いてほしいということである。 クラスの利用側の状況によって、コンパイル時にポインタの値が決定できる場合もあるし、 実行時にしかわからない場合もありうるが、 コンパイル時に決定できるなら必ず間接参照を省いてほしいということになる。

一つの解決策として紹介したいのが、やはりNon-type template parametersを使った手法である。 まず、グローバル変数を表すテンプレートを作り、 それにポインタ的なインターフェイスを付ける。

template <typename T, T* Obj>
struct global_object
{
    T& operator * () const { return *Obj; }
    
    T* operator -> () const { return Obj; }
};

そして、これの利用を意図したクラスを次のようにテンプレートで定義する。

template <typename Foo>
class bar
{
public:
    bar(Foo f) : f_(f) { }
    
    ~bar() { std::cout << "~bar()" << std::endl; }
    
    void set(int val) const {
        f_->set(val);
    }

private:
    Foo f_;
};

テンプレート引数であるFooには、 グローバル変数を紐付けたい場合global_objectを入れ、 実行時にアドレスを変更したいならポインタ型を入れればよい。

// グローバル変数なら型として埋め込む
const bar< global_object<foo, &g_foo> > g_bar( (global_object<foo, &g_foo>()) );
    // Most vexing parseの一例になってしまっている

void set_value_2(int val) {
    g_bar.set(val);
}

void set_to(foo* f)
{
    bar<foo*> b(f); // 実行時に決定することもできる
    b.set(123);
}

ポインタを持った全てのクラスをこのように定義するのは若干面倒だが、 グローバル変数を直接触るコードよりは遥かに柔軟性が高いし、 その一方でコンパイラの最適化能力に関わらずオーバーヘッドは確実にゼロにできるので、 手段の一つとしてありなのではないかと思う。

まとめ

Non-type template parametersを使うことで、関数や変数、定数などを型にエンコードし、 コンパイル時の色んな最適化を促進することができる。 性能要件の厳しい状況下において、 C++が備えているゼロオーバーヘッドの抽象化能力は特に役立つ。

エンコーディング・マジック・アワー

この記事はeeic Advent Calendar 2015の22日目の記事として書かれた.

最近動画エンコードについてよく聞かれるので,録画PCネタの続編ということで記事にすることにした. 私は動画処理に別段詳しいわけでもなく,専門分野もマルチメディアとはまるで関係ないのだが, x264やFFmpegといった動画変換ソフトウェアの使い方については日頃から色々試してはいるので, とても適当に今まで得た知見をまとめようと思う. おそらくこの記事には間違いも多々含まれているだろうから,コメントは随時募集している.

前置き

去年の夏,気が狂ったように深夜アニメを片っ端から録画してHDDに貯め始めた結果, 我が家の録画PCのHDDはあっという間に満杯になった. 放送波のデータはMPEG-2 TS (Transport Stream, しばしば単にTSと呼ばれる)で送られてくるが, その圧縮率は決して高くない *1 ので,1クールに放送される全てのアニメを圧縮せずに全部録り貯めると体感7TB (これはビッグデータだろうか?)ぐらいになる. 富豪的に解決するにはHDDを買い足せばいいわけだが, 今最も単位容量あたり価格の安い3TBのHDD(現在の価格は大体1万円弱)を増やすとしても, 1年に10台は買い足す必要がある. 貧乏な院生が10万円/年を定常的に注ぎこむのはなかなか厳しいし, 仮に資金調達が上手くいったとしても,HDDを交換する手間などを考えるととても現実的な選択肢ではない.

そのような事情から,多くの録画PC持ちの人々はTSをそのまま保存するのではなく, MPEG-4のような圧縮率の高いフォーマットに圧縮(エンコード*2し直すことで, 録画動画の容量削減を行っている. TS→MPEG-4の変換については非常に多種多様なやり方があるが, 映像に関してはH.264,音声はAACエンコードするのが最も一般的であろう. 最近は新しい映像コーデックとしてH.265も登場しているが, まだ対応ソフトが非常に限定されていて,エンコーダも発展途上であるから,今回は取り扱わない.

ソフト vs. ハード

映像のエンコード手法は, ざっくり分けてソフトウェアエンコードとハードウェアエンコードがある. ここでいうハードウェアエンコードとは, GPUなどに備わっているエンコード専用回路を利用して映像を圧縮することである. 一方でソフトウェアエンコードとは, CPUの機能だけを使うソフトウェアによるエンコードであり, H.264の場合はx264というオープンソースのソフトウェアが有名である. FFmpegやAviutlといった著名な動画変換ソフトはフロントエンド*3であり, 内部ではx264に代表されるコーデックを呼び出している.

今回取り上げるのはソフトウェアエンコードの話題である. 一見すると,ハードウェアエンコードの方が専用回路を使っている分だけ優れていそうなのだが, ソフトウェアに比べるとチューニングできるパラメータが限定的であるが故に,現状ではその使用用途も限定的である. また,x264のようなソフトは圧縮効率を高めるために日進月歩で非常に細かい工夫がなされているため,ハードではそれに追い付けていないのが現状である*4. もちろん,ハードウェアエンコードには省電力,高速といった利点があるため, そのような利点が活かされる場面 (例えばストリーミング配信)では採用するべきだろう. ソフトウェアエンコードは「エネルギー効率や変換時間はどうでもいいが,とにかく画質と圧縮効率を向上させたい」という要件を満たすために適切な選択肢であり,録画データを保存するなら普通はそういう要件になると思う.

要件定義

動画エンコードを行うにあたって,我々が吟味する主なパラメータは次のようになる.

  • 画質
    • 大雑把にいって,変換前後で各画像フレームの差分が小さい方が画質が高い.画質には人間の主観も入るので定量的に比較しづらいが,映像でよく使われるのはStructual similarity (SSIM)という指標で,これはx264でも計算することができる.
    • x264には,人間が見た際の心理的な判断として画質が変わらないとみなせる範囲で画質を上げ下げする仕組み (psy-rd)もある.
    • 画像と異なった動画ならではのポイントとして,動きの少ない/激しい部分のどちらを優先するかで画質を上げ下げすることができる (qcomp).
  • ファイルサイズ
    • 元動画が1つのファイルの場合,最終的なファイルサイズは比べやすい指標である.
    • 一般には動画中の各時刻でビットレートは刻々と変動するため,当然のことながら単純にビットレート×秒数と計算してもファイルサイズは決まらない.
  • エンコード時間
    • エンコード時間も計測しやすい指標である.最終的に影響を与えるのは,単位時間あたりに捌ききれる動画の本数と,PCの電気代(CPU100%で動いている時間が長いほど電力を使う)である.
    • オプションをいじくることで,元動画の時間に対して数十分の1でエンコードが済んだり,何十倍もの時間がかかるような極端な設定を作ることができる.
    • ある程度のところまではエンコード時間をかければ他のパラメータが改善するが,それ以降は伸び悩むので,手間ひまかけて煮込みまくればそれだけ素晴らしい出来栄えになるかというとそういうことはない.
  • 再生負荷
    • 再生側で最終的にMPEG-4をデコードしないと動画として見られないから,再生時の処理も一応考慮する必要がある.
    • 再生負荷を重視するのは主に古い環境やモバイル機器を使っている人たちで,将来的にはこれらも改善されていくと考えるのが私は妥当だと思う.
    • 再生時の“シークのなめらかさ”も忘れがちであるが重要である.関係してくるのはkeyintなどである.

x264のようなコーデックに与えるオプションによって,これらのパラメータは大きく変動する. 基本的には,あるオプションを調整するとこれらのパラメータのうちのどれかが改善する代わりに, どれかが悪化するというトレードオフの関係になっている. すなわち,全てを改善するような魔法のオプションというのは存在せず, どれかのオプションをいじるとどこかしらしわ寄せがいくことになる. そのため,エンコードを行うにあたって, 出力結果の要件をきちんと定めて, それらを全てクリアできるようなオプションを導き出すという手順が必要になる.

代表的なトレードオフは次のように挙げられる.

  • 画質 vs. サイズ
    • crf, qcomp
  • サイズ vs. (エンコード時間, 再生負荷)
    • ref, bframes, me, subme, trellis, keyint, rc-lookahead, etc.

オプション決定に際しての基本的な方針としては次のようになる.まず,大抵の場合その人が許容できる画質があるだろうから,それをcrfqcompを試しながら決定する.その上で,refbframesといったファイルサイズに影響のあるパラメータを調整して,許容できるファイルサイズ(とエンコード時間)になるまで試す.その段階でファイルサイズを見て愕然とすることもあるだろうから,その際は適度に画質を落としながら妥協点を探る.後は直接これらのパラメータに関係しないその他のオプションを調整すれば完成である.以上のような基本的な流れが,FFmpegの公式サイトにきちんとまとめられていたことにこの記事を書いている最中に気付いた.

Encode/H.264 – FFmpeg

コンテナの話

動画エンコードについて触れる前に,MPEG-4コンテナに関する話や,デインターレースといった前処理の話をまとめておく.

音ズレの恐怖

MPEG-2やMPEG-4は,映像と音声を多重化 (mux)して保存することができる, いわゆる動画コンテナと呼ばれる形式である. 特に元々の放送波はストリーミングなので, 映像と音声のタイミングを適切に同期させるためのデータが含まれている.

割と多くのウェブサイトでは,「MPEG-2 TSをエンコードする前に,最初に映像と音声を分離 (demux)して, 別々にエンコードしたのち,結合 (mux)しよう」という解説をしているところが多い. 理屈としてはこれで最も柔軟な運用ができるのだが, 私の体感として,一度でも映像と音声を分離してしまうと,かなり注意していても音ズレが起きるMPEGコンテナには音声再生を開始するオフセット値 (名前は忘れた)があり, この値をきちんと出力のコンテナフォーマットに引き継がなければいけない. 分離や結合の際にオフセット調整を行ってくれるソフトを使えば上手くいくはずであるが,それにはあまり期待しないほうがいい. しかも,音ズレが起きているかを確かめる作業というのはとにかくつらくて, 複数の動画を再生して映像と音声のタイミングのずれ (数百ミリ秒)を検証するのは決して楽ではない.

私も当初はdemuxを行って映像・音声に別々のエンコードソフトを使うなどしていたが, 後になって音ズレ動画を量産していることに気付いた. ネット上にあふれる音ズレ動画はこうして作られているのだなぁと納得した瞬間である. そして結局,MPEG-2を映像+音声のままFFmpegに突っ込んで, 映像+音声としてMPEG-4を出力させる,というのが音ズレが起こらないし簡単だという結論に達した. FFmpeg以外のソフトでも同じことがいえるだろう.

ちなみに,TSに含まれている音声もAACだから,FFmpegなら-vcodec:a copyとやってしまえば 別に再エンコードをかけずにそのままMPEG-4にmuxできるのではないか, ということを私は当初考えていたが,色々試してみてもこれはうまくいかなかった. TSのストリーミング用AACは途中でぶつ切れになっていることがあるらしく, 出力ファイルが再生できないといった問題が発生したので, 今のところは諦めて再エンコードするという方針で落ち着いている.

解像度

解像度とは要するに画面上のピクセル数のことで, エンコードの前処理で縮小処理をかけて解像度を下げれば, 当然ながらファイルは大幅に縮む. しかし,私としては,録画保存用途なら解像度は下げないことを強く推奨したい. 最近ではディスプレイの高解像度化が進んでいて, フルHD画質 (1920×1080)ぐらいなら普通に表示できてしまうので, そういうディスプレイで低解像度の動画を見た時には虚しい気分になること間違いなしである. もちろん,モバイル用途等で必要に応じて解像度を下げることはありだと思う.

インターレース時の60p化

テレビの放送波は約30fps (正確には29.97fps)の映像が送信されてくるが, その映像はアナログ時代の名残でインターレースと呼ばれる方式に沿っている. 雑に解説すると,画像を縞々の線 ("走査線")に2分割して, 更新する走査線を入れ替えながら動画を再生する仕組みになっている. こんな謎な方式は,地デジ化の際に駆逐してしまえばよかったのではないかと思うのだが, 私は別に仕様策定の事情について知らないのでこの辺にしておこう.

 ****** 偶
 ****** 奇
 ****** 偶   ←全然そうはみえないかもしれないが
 ****** 奇     6ピクセル ×6ピクセル の画面だと思ってほしい
 ****** 偶
 ****** 奇


インターレース (60i) ← 画面半分が1秒に60個

     1/30 秒
 _______________
|               |
 ++++++            ++++++          
          ------            ------ 
 ++++++            ++++++          
          ------            ------    ...
 ++++++            ++++++          
          ------            ------
   偶       奇       偶       奇
                                     ---> 時間t


プログレッシブ (30p) ← 画面全体が1秒に30個

     1/30 秒
 _______________
|               |
 ++++++            ++++++
 ++++++            ++++++
 ++++++            ++++++
 ++++++            ++++++             ...
 ++++++            ++++++
 ++++++            ++++++
                                     ---> 時間t

インターレース方式の映像を,フレーム毎に全ての走査線が更新されるように変換し, プログレッシブ方式の映像を得るのがデインターレースである. FFmpeg始め多くの動画変換ソフトには,yadifという有名なデインターレースアルゴリズムが実装されている. デインターレースも実は奥深いことがWikipediaを読むとわかる.

インターレース解除 - Wikipedia

インターレースで一般的によく使われているのがyadif=0として30pの動画に変換する方法なのだが, 上で示しているように偶奇のコマのタイミングが微妙に違うので,これらのコマを混合するような処理を行っている. そのため,例えば上下に移動するテロップのような連続したコマがブレて見えるようになる. ネット上には,これを防ぐためにフィルタをかけるといった手法も散見されるが,下手に動画を加工することはやめておいたほうがいい. 「アニメは元々24fpsで作られているから24pに変換しよう」などとフレームレート変換を試している人もいるが同じことである.

私としては,yadif=1としてフレーム数を倍にし,60p化すること (Bobデインターレースともよばれる)をオススメしたい. 60fpsになったからといって動画サイズが2倍になるわけではなく, フレーム間の差分が小さくなることから,Bフレームなどの仕組みが働いて上手く圧縮されるためである. 30fpsの動画と比べても1.1倍程度のサイズで収まるはずである.

(ちなみに60p化というのは,YouTube等にネタとして上げられている,映像がぬるぬるになるフィルタとは全く異なる.)

60p化の問題点は,フレーム数が単純に2倍になるためにエンコード時間も約2倍になってしまうことである. その点からみると,画質を優先してエンコード時間を犠牲にする設定であるといえる. また,ほとんどの再生機器は60pの動画を再生できるが,再生機器がヘボいと結局テロップはカクカクしてしまうかもしれない.

60pにしたからといって,データを補間している都合上はデインターレースによるノイズ (アーティファクト)が完全に消えるわけではない. 普通のテレビ受像機はその辺うまいことやっているように見えるので,詳しい方がいたら教えてほしい.

x264のオプション

私が最も重要だと思うポイントは,「自分で意味が分からないオプションは付けない」というものである. x264の開発者は我々よりもずっと動画エンコードに詳しいのだから, 可能な限りx264のデフォルトの設定をそのまま活かすべきだろう. オプションの意味を理解して,利点と欠点がある程度明確になったら,その段階で初めて調整するのが賢明である.

とはいうものの,私も未だによく分からないオプションが多くあって,ネットの情報を元に微調整したものがいくつかある. それらのオプションは,奇をてらわずにデフォルトのパラメータからわずかに違う値に変更するようにしている.

もう一つ,上で言ったことと既に相反するのだが, 「x264のチューニング *5は使わない」という点も重要である. 例えば,チューニングにanimation (--tune animation)というものがあって,いかにもアニメエンコードに有用そうに見えるわけだが, 実際には極端な設定値になっていることで有名である. これはx264を使い始めたエンコ職人見習い用の罠であると思ってよい. オプションをきちんと理解した上で設定するなら構わないが, そのレベルに達した頃にはチューニングのようなデフォルトの設定集を使う必要はなくなっているはずである.

CRFとqcomp

x264での基本は,Constant Rate Factor (CRF)という値に基づいて,画質を一定に保ちながらエンコードするシングルパスの手法である. x264には他にも2段階エンコードというものもあるが, これは1段目のエンコードでファイルサイズを予測するための仕組みであり, 容量に特段の制約がある場合のみに使用すべきである. 2段階でエンコードしたからといって,CRFでエンコードした時よりも画質が良くなるわけではない.

CRFの値は完全に好みの問題で,各々がどの程度の画質で妥協できるかに依っている. 私は画質とファイルサイズを比較対照しながら, アニメならcrf=23 (FFmpegのデフォルト値!)ぐらいが一番コスパがよいのではないかと思って今はそう設定している.

CRF比較用の画像を用意したので参考にして欲しい. 甘城ブリリアントパークのOP (1分30秒)を切り出した後にエンコードをかけている.

time ffmpeg -i input.ts -crf 18 -preset slow -filter:v yadif=1 -x264opts keyint=600:no-psy -ssim 1 crf=18.mp4

左の絵がオリジナルで,右の絵がエンコード後の画像である.上の方のCRFが小さいものほど画質がよい. 静止画をパッと見ただけでは全然違いが分からないのだが,動画で見るともっと歴然とした違いがある. 私の目でも,拡大してみるとcrf=25ぐらいまで下がると明らかに線がガタガタしているようにみえる. 個人的にはcrf=23〜24あたりが人間が知覚できるかどうかの微妙なラインなのではないかと思っている. ところでラティファ様はかわいい.

f:id:meryngii:20151223014943p:plain

元画像が大きすぎて記事編集中に既にブラウザがクラッシュしかけたので,元画像は下記リンクを参照のこと.

画質に影響を与えるもう一つのパラメータはqcompである. qcompは,映像の変動率が大きいところにどの程度ビットレートを割り振るかを決めている値 (0.0〜1.0)である. qcompが高いほど,動きの激しい箇所を優先して大きくビットレートを割り振るので, 画質が向上する代わりにファイルサイズが増加する. qcompの値はデフォルトが0.6で,ネット上では少し高めにしている設定例 (0.7とか)が多く見られるのだが, 私の目ではqcomp=0.7にしてもファイルがブクブク太る割に目立った効果が感じられなかったので, qcomp=0.6のままにしている.

crfとqcompの値に関しては,私は色々妥協を重ねた結果こうしているのだが, HDDの空き容量が余りまくっていて満足できないという人は画質を上げるべきである. 私も昔はcrf=20ぐらいに上げた設定を使っていたのだが, 本当に好みの番組は再エンコードせずにTSのまま保存したくなってしまうということに気付き, エンコード後の超高画質を目指すことはあまり意味が無いのではないかと思うようになった. かといって,後でから見返す気にならないほどの低画質は耐え難いので, 妥協点は常に探っていく必要がある.

プロファイルとレベル

H.264でハイビジョン画質をエンコードするには,基本的にハイプロファイルを使用する. 「メインプロファイルの方がよい」と力説しているサイトも散見されるが, SD解像度のエンコードについて解説しているようで, 解像度そのままでフルHD解像度でエンコードするなら素直にハイプロファイルにすることをオススメする.

H.264にはレベルという概念もあり, 再生機器を限定している必要条件になっている. そのため,レベルを無意味に上げ過ぎると再生可能な機種が限定されるが, 別に画質が向上するわけでもないので何の利点も無い. レベルを決める場合は,下記のWikipediaの表を参考にして, 入力映像に対応した最低のレベルにする.

https://ja.wikipedia.org/wiki/H.264#.E3.83.AC.E3.83.99.E3.83.AB

ディジタルテレビのビットレートをカバーしているのはHigh Profile Level 4.0ぐらいからだから, これぐらいに設定しておけば問題はないだろう. *6 このレベルを再生できないような太古のモバイル機器を使っている人は…… 新しい端末に買い換えたほうがいいかもしれない.

ref, bframes

MPEGの基本的な仕組みとして,近い時刻のフレーム群は似ている場合が多いので, 近くのフレームからの差分のみを記録することでデータを圧縮している. どこからの差分を記録するかでフレームの種類が分かれる.

  • Iフレーム: 画面全体を保持しているフレーム.キーフレームとも.
    • H.264の場合,シークなどの切れ目になっているGroup Of Pictures (GOP)という単位の先頭のキーフレームはIDRと呼ばれて特別扱いされる.IDRじゃないIフレームからはシークができない.
  • Pフレーム: 以前のフレームからの差分を保持しているフレーム.
  • Bフレーム: 以前・以後からの差分を保持しているフレーム.

ref (FFmpegだとrefs)は「参照フレーム数」のことで,エンコード時にIフレームやBフレームが参照できる元フレームがどれだけ離れていてもよいかを示す. refを増やせば増やすほど最適なフレームを選択できるのでビットレートを削減できるが, 試行するフレーム数が増える分だけエンコード時間はどんどん伸びていく. 再生時には各フレームをメモリに保持していなければいけないので, refを上げれば上げるほど再生負荷も増大する.

bframes (FFmpegだとbf)はBフレームの最大連続数」を表していて,その性質上ref以下である必要がある. bframesも増加させるとビットレートを削減できる一方, エンコード時間と再生負荷が増大する.

私が色々試した限りでは,--ref 4, --bframes 4ぐらいが妥当なようである. ちなみにこの文章を書いていて初めて気付いたが,Level 4.0で1080pの動画をエンコードする場合のrefの最大値は4と決まっているらしい. あと1増やしていたら規格外の動画を生産していたようだ.

keyint

keyint「IDRフレーム間の最大フレーム数」である. keyintを上げるとIDRフレームが少なくなるため,ファイルサイズが削減できる. その代わり,再生時にはIDRフレームからしかデコードが開始できないため, keyintを上げるほどシークが遅くなっていく.

一般的に,keyintの値は「フレームレート×10」(10秒間隔でならシークが高速に終わる)ぐらいにするのが無難であるという説が有力である. yadif=1で60pにデインターレースした映像をエンコードする場合は,--keyint 600ということになる *7yadif=0ならkeyint=300で,この数値のほうがネットではよく見かける. デフォルトのkeyintは250となっているが,これはおそらく海外で使われている25pの規格に合わせているのだと思う.

高画質のためのオプション色々

x264にはプリセット (チューニングとは異なる)という予め用意された設定集があって, サイズと負荷のトレードオフを決めることができる. 画質とファイルサイズ重視なら,使うのはだいたいslow 〜 veryslowぐらいになるはずで, 細かいことを気にしないならプリセットを使ってもいいかもしれない. プリセットを参考にしながら私が調整した値を以下に示す.

  • --b-adapt 2
  • --me umh
    • Integer pixel motion estimation (整数ピクセル動き予測)という,アルゴリズムを決めるオプション.
    • umhはバランスの取れたオプションで,これ以上のオプション (esa, tesa)にしてもほとんど縮まないと開発者が明言している.
  • --subme 8
    • Subpixel motion estimation (サブピクセル動き予測)というオプションで,整数値はモードを表す.
    • 9以上に上げてもファイルサイズがほとんど変わらない割にエンコード時間が伸びるだけだったので,8にしている.
  • --trellis 2
    • 2だとエンコード時間と再生負荷が激増する,と解説するサイトが多いが,高画質にするなら2が無難.
  • --partitions all
    • 画像のブロック分割の手法らしい.細かく指定できるのだが,とりあえず全部オン.
  • --direct auto
    • Direct MV prediction modeというもので,モーションベクトルの予測のモードらしい.auto以外にしているのはあまり見たことがない.
  • --rc-lookahead 50
    • 先読みに関係しているらしいがあまり良く知らない.
    • slowだと50,slower以上だと60になっている値.微妙すぎて違いがよく分からず.60p用に変えたほうがいいのかもしれない.

psy-rd

psy-rdは,心理的に複雑に見える箇所の圧縮率を下げることで,画質が上がったかのように見せるためのオプション. アニメのような平坦な映像に対しては低めの値にしたほうがいいらしいので, 私はデフォルトの1.0から少し下げた--psy-rd 0.5:0.0にしている (比べてみても違いはよくわからない).

psy-rdに関しては以下のサイトが詳しい.あくまで心理的な効果を狙ったものなので, 再現性を追求するならオフにするのが望ましいらしい. そのうち気が変わったら--no-psyにするかもしれない.

Linux で H.264/AAC エンコード

ちなみにpsy-trellisという実験的な機能もあるのだが,こちらはオフ (psy-rdの0.0の部分)にしている.

aq-strength

aq-strengthはAdaptive Quantization (AQ)という画面上のビット再配分をする機能の強度を変える値で,デフォルトは1.0. 下げたほうが輪郭が綺麗に残りやすいらしくアニメ向きらしいので,私は少し下げて--aq-strength 0.8にしている. 個人的にはこれも変化が分かりづらいパラメータの一つだと思う.

AQのモードを変えるaq-modeというオプションもあるが,これは試したことはない.

独断と偏見によるプリセット解釈 その1:因幡製作所業務マニュアル - ブロマガ

その他微妙なオプション

以下はさらによく分からないオプション集で,どれかに決めなければいけないので適当に決めたものである. そのうち微調整するかもしれない.

  • --min-keyint 4
    • keyintは「IDRフレーム間の最大間隔」だったのに対し,min-keyintは「IDRフレーム間の最小間隔」.
    • 小さめの値に設定しておくとシークが楽になるが,1にしてしまうとGOPが切れまくってビットが無駄遣いされるらしい.そんなわけで4にしたはずだが,それ以上明確な根拠は多分ない.
  • --scenecut 60
    • どのぐらい積極的にIDRフレームを挿入するかを示す値で,デフォルトは40.
    • 多めにIDRを挿入したほうが後々編集など扱いやすいという理由でこうしたような気がする.デフォルトでいい気がする.
  • --deblock
    • デフォルトのまま.デブロックフィルタについては詳しく検証していない.
  • --merange
    • これもデフォルト (16)のままにしている.プリセットだとslowerでも16で,veryslowからようやく24に上がる.

FFmpegでの例

最後に,これまでの設定をまとめてFFmpegに与える例を示す. x264とFFmpegで微妙にオプションの名前が異なることに注意.

-threads 6となっているのはスレッド数で,コア数×1.5にするのが無難と言われている. -movflags +faststartは,ストリーミング用にメタデータを先頭に移動しておくオプションである.

ffmpeg                     \
-i "input.ts"               \
-codec:v libx264           \
-profile:v high -level 4.0 \
-filter:v yadif=1:-1:0     \
-crf 23                    \
-qcomp 0.6                 \
-refs 4                    \
-bf 4                      \
-aq-strength 0.8           \
-psy-rd 0.5:               \
-x264opts \
keyint=600\
:min-keyint=4\
:scenecut=40\
:b-adapt=2\
:direct=auto\
:me=umh\
:subme=8\
:trellis=2\
:rc-lookahead=50\
:partitions=all \
-codec:a libfdk_aac        \
-b:a 192k                  \
-threads 6                 \
-movflags +faststart       \
"output.mp4"

雑感

  • 散々調整した割には結局FFmpegのデフォルト値になったオプションが多い.
  • MPEG-4の規格について知らないことが多すぎる.
  • エンコ職人への道のりは険しい…….

*1:古めの規格なのでそもそも圧縮効率があまりよくなく,ストリーミング用なので圧縮率も低めに設定してある,はず.

*2:本当は再エンコードというのはTSをデコード→MPEG-4エンコードという2段のフェーズを経ているから, 放送局がTSにエンコード→変換ソフトウェアのフロントエンドがデコードというフェーズを経る時点で本来の画質よりも落ちている.

*3:しかしただのガワといえるほど単純なシステムではない.

*4:x264がハードウェアの機能を一部借りれないかというアイディアは昔からあるらしく,例えばQSVに関してはIntelが仕様を公開してくれないとかでうまくいっていないという話をどこかで見た.

*5:紛らわしいのだがチューニングとプリセットは異なる.

*6:と断言したはいいものの,本当にこのレベルで規格内なのかこの文章を書いている間に不安になってきた.

*7:インターレース後のフレームレートを使うのが正しい,はず.きちんと検証したことはない.

録画用サーバPCを組んだ時の話

1年前に深夜アニメを大量に録画しようと思い立ち、初めての自作PCとして録画用サーバを構築したのだが、いつか暇になったらその構成をWebに公開しようと思いながらもう1年も経ってしまった。(現代人には暇な時間などないのだ。)

PC業界は移り変わりが激しいので、1年経つだけで情報が陳腐化してしまってもおかしくはない。とはいえ、このまま書かないで放置するのも色々と都合がよくないので、この機会にまとめておこうと思う。

今回は録画PCのハードウェアの話にとどめておくが、そのうちエンコード周り等ソフトウェアの話も書けたら書きたい。

組む前に

自作PCを組むにあたって最重要視すべきことは「何に注力するか?」であるように思う。これが明確でないと、パーツの種類がたくさんありすぎて、選ぶ基準が無くなって困る。しかし、PC全体でのコンセプトを決めて、その上で各パーツを選んでいくと、案外すんなり決まるように思う。

PCパーツを差別化する要素は、以下のように色々ある。それぞれは必ずしも相反するものではないことに注意。

  • 価格(とにかく安いやつ)
  • 性能(とにかく速いやつ)
  • 省電力(ピーク電力、アイドル電力の2種類ある)
  • 静音性(ファンがうるさいのをどこまで許容するか)
  • 多機能(最新規格に対応してるかどうかとか)
  • 大きさ、見た目(部屋で邪魔にならないか)

僕はこのうち、静音性+省電力を重視して、価格と性能は二の次ってことにした。というのも、サーバとして24時間運用する以上、寝ている時にうるさくなくて、月々の電気代に響かないことが結構重要だからだ。性能に関しては、お金をかけた分の利益が得られそうなときだけ上げた。この辺の話は、別に縛りプレイではないので、困ったときに従う程度にざっくり決めておけばよいと思う。

構成

ということで僕の録画サーバの構成を。

パーツ名 型番 税込価格(当時)
ケース Define Mini ¥9,561
電源 SST-ST30SF ¥6,880
マザーボード B85M-G ¥5,458
CPU Core i5 4460 ¥19,440
CPUクーラー SCKTT-1000 ¥2,880
メモリ CFD W3U1600HQ-4G ¥9,279
SSD CT256MX100SSD1 ¥11,980
HDD (1) WD30EZRX-1TBP ¥10,559
HDD (2) MD04ACA300 ¥9,200
チューナー PT3 ¥11,680

しめて10万強くらい。この他にディスプレイ、キーボード、マウスも買っているから、実際にはもっとかかっている。色々調べた割には、価格.comで上位から適当に引っ張ってきたようなやる気のない構成になってしまった。まあ自作PCは初めてだし、変態構成を目指しているわけでもなし、別にいいのだけれども。

Linuxで運用するのでOS代無し。元々仮想化して使う予定だったのだが、PT3のPCIパススルーが上手く行かなくて断念。

PCゲーム等はやらないので、ビデオカードはなし。録画データをハードウェアエンコードしたいなら要るのだけれど、圧縮効率はx264の方が良い(はず)なのと、ビデオカードは挿すだけでアイドル電力が上がるという点を考慮して使わない。というかそもそも、Linuxではハードウェアエンコードが出来ないのだが……。

ケース

ケースは結構悩んだ。見た目はほとんどケースで決まるし、好みも分かれるポイントだし、何よりケースは中のパーツより寿命が長い可能性が高い。デカいから捨てるのも面倒くさそう。というわけで割と慎重に。

僕の中で静音ケースを探して、当時候補に上がったのが以下の製品。

  • Fractal Design Define Mini (買ったやつ)
  • Fractal Design Define R4
  • SilverStone SST-PS07
  • Antec P100
  • Antec P280

ケースは実際に店舗に行って見るのが一番良いと思った。最初P100を買うつもりで何度か見に行ったのだが、前面ドアの安っぽさ感がどうしても受け付けられなくて却下した。Define Miniを選んだ理由は、実際に見てみて大きさがちょうどよかったから。Miniとかいう名前の割にやたら大きいケースなのだが、microATX専用ケースということもあってR4とかのATXケースよりは少し小さくて、意外と重要な差だと感じた。microATXだけで拡張性は心配ないかは今のところ断言できないが、現時点では困っていることはない。

ケースに関しては、今だったらDefine R5を買うのが良さそう。ただし実物を見たわけではない。R5は3.5インチベイ8個と、HDDを大量に積む必要がある録画サーバに持ってこいだと思う。

電源

SilverStone SFX電源 300W SST-ST30SF

SilverStone SFX電源 300W SST-ST30SF

電源はSST-ST30SFをチョイス。電源容量300Wと小さめ。この電源はセミファンレスってやつで、一定以上の温度まで高くならないとファンが回らないという仕組みになっている。実際、今までこの電源のファンが回っているところは見たことない。

電源は低負荷時での効率が低いという特性を持っている。下手に大容量の電源を買って、実際には低負荷で運用すると、低効率状態でずっと使うことになってしまう。僕の作ったPCはというと、アイドル電力が30Wくらいで、CPU高負荷時で80Wくらいを消費している。このぐらいの消費電力だったら小型の電源にしておいた方がいい。

マザーボード

マザーボードはB85M-Gで、わざとローエンド的な製品を買った。これでもSATAポートは8本あるようだし、機能的には十分だと判断した。

省電力を追求するという考え方からすると、マザーボードの機能は出来るだけ少ないほうがよい。マザーボードは常に通電しているので、特にアイドル電力に影響を与えるからだ。この辺の話は、2ch自作PC板にある「低消費電力 自作PC」スレを眺めると参考になる。世の中にはアイドル電力を削ることに執念を燃やしている人もいるらしい。

CPU

CPUは、あまり迷わずにi5 4460に決めた。i3にしてコア数を減らすのはさすがに性能的に許容できなそうだと思ったのと、i7にしてハイパースレッディングで受けられる恩恵はごく一部だと思ったのでi5に。i5内の上位モデルにした時の周波数向上も同様に必要なさそうだと感じたので、一番安い4460で。

よく言われていることとして、高性能CPUが欲しいからK付きのシリーズを買えば良いというわけではなくて、K付きはオーバークロックすること前提の一部の人々向け。でも最近はKだけマイナーアップデートしている(?)ようなので、一般人が買ってもいいかもしれない。あと、T付きやS付きはTDP値が低いので省電力だと思われていることがあるが、これらは最大消費電力を抑えるという意味なので、アイドル電力が小さいわけではない。メーカーPCにおいて筐体の設計上熱の制約が厳しい場合に使われる。

CPUクーラー

サイズ 【HASWELL対応】 虎徹 12cmサイドフロー SCKTT-1000

サイズ 【HASWELL対応】 虎徹 12cmサイドフロー SCKTT-1000

CPUクーラーは後でも買えるので、CPU付属のリテールクーラーで十分という人はそれでもいいかもしれない。僕も後でから追加購入した。

僕のようにCPUでソフトウェアエンコードするような人は、CPUが高負荷で回り続けるので必須だと思う。高負荷時のCPU温度が60℃台だったのが、CPUクーラーを交換してから40℃台に収まった。あまり高温で運用し続けるとCPUの寿命が縮まりそうな気がする。

メモリ

メモリはぶっちゃけ何でもいいと思う。最近は相性問題もほとんど起きないらしい。メモリの価格は2012年以降上昇するというとんでもないことが起きていて、大容量をチョイスする気になれなかったので4GB×2で。お金がある人は8GB×2でいいと思う。

SSD

Crucial MX100 2.5インチ内蔵 SSD 256GB SATAIII CT256MX100SSD1

Crucial MX100 2.5インチ内蔵 SSD 256GB SATAIII CT256MX100SSD1

SSDはシステムを置いておく用。256GBはあった方がよいと思う。

HDD

HDDは安いやつで。WD30EZRX-1TBP(通称WD緑)は単価が最も安いHDDとして有名。今のところRAIDは組んでなくて、吹っ飛ぶとヤバいので何とかしないといけない。

MD04ACA300も一時期WD緑に迫るぐらい安かったのだけど、最近は少し値段差が開いた。これはサブで使っている。

チューナー

PT3 Rev.A

PT3 Rev.A

PT3一択だと思う。ノートPCで録画したいとかの事情がなければ。

PT3は地上波×2とBS×2を同時録画出来るが、地上波のチャンネルはそれでも足りない場合があるので、増設するかはちょっと検討中。

録画用途だとPT3にPCIeのスロットを最低1個持っていかれるので、ビデオカード等々積むかもしれない人は注意したほうがいいかもしれない。

終わりに

自作PCといっても部品を買って組むだけだから、やる気とお金さえあれば誰でも出来ると思う。実際、組み始めてから起動させるまで、僕でも6時間くらいしかかからなかった。

全てにおいてベストな決断が出来ているわけはないのだが、まあまあ無難なものを作れたとは思う。電力に関しては、24時間稼働させることで実際に月々の電力が1000円くらい上がったのだが、ランニングコストとしては問題ないと思っている。静音性については、PCを稼働し始めた後、ワンルームだと冷蔵庫や空気清浄機や換気扇の方がずっとうるさいということが分かってしまった。そうはいっても騒音源は少ないほうがいいのは間違いないし、最近HDDが時々ゴンゴンうるさくて困っている。

録画用サーバPCといいつつも、それ以外の用途に使っている時もあるので、その時はエンコードでCPUが100%で回り続けているのは割とうっとうしい。僕は作業用にノートPCを併用するのでそれほど気にならないのだが、デスクトップPCとしてもきちんと使いたい人は、ハードウェアエンコードするなりCPUやメモリを強化するなりした方がいいかもしれない。

CygwinのGitでエラー

何だろうこのエラーは…。

% git submodule init
/usr/lib/git-core/git.exe: error while loading shared libraries: ?: cannot open shared object file: No such file or directory

/usr/lib/git-core/git.exe: error while loading shared libraries: ?: cannot open shared object file: No such file or directory

調べてみたらGitにはgettextが要るらしいけど、依存関係が足りてなかったらしい。
https://www.mail-archive.com/cygwin@cygwin.com/msg131833.html

apt-cyg install gettext-devel

罠でしかない…

Java向けにC++クラスをラップする方法

JNI (Java Native Interface)ネタは、この記事で一応最後です。
前々回は、Javaから渡された配列や文字列を簡単に受け取る方法について述べました。配列の受け渡しに関しては、最悪だと呼び出し毎にネイティブヒープへのコピーが発生する可能性があるので、ネイティブ側とVM側のどちらにデータを配置するのが正しいのかを考慮して設計する必要があります。
前回は、C++をJNI経由で呼び出す際の例外処理について述べました。STLに例外が必要となるわけですし、Android等でも基本的には-fexceptionsを有効にしてビルドした方がいいと思います。
さて、ここまでの知識だと、JavaからC言語インターフェイス経由で関数を呼び出すということは出来ますが、C++のクラスをJavaから管理するとなるともう少し工夫が必要です。今回は、オブジェクト指向なやり方でC++のクラスを利用するにはどうすればいいのかについて述べていきます。

ポインタなんて所詮整数だ

Javaには、CやC++のポインタを認識する仕組みはありません。*1Java仮想マシン上の"綺麗な世界"には、ポインタなどという穢らわしきモノは存在しないわけです。
しかし、結局のところ、ポインタ≒アドレスなんて所詮整数なので、Javaのクラスに整数メンバを用意しておけばアドレスを保存しておくことができます。そいつをJNI経由でC++側から読み出せば、C++側でインスタンスを特定できるという話です。今回の記事の根幹はここだけです。
この辺りの話題は、以下の記事を参考にしています。
http://thebreakfastpost.com/2012/01/21/wrapping-a-c-library-with-jni-introduction/
例として、以下の様なテストクラスをC++によって実装してみることにします。

public class WrapperTest
{
    private long mHandle = 0; // <=== こっそりアドレスを保存する

    public WrapperTest() {
        init();
    }
    
    private native void init();   
    public native void put(int x);
    public native void show();
    public native void destroy();
}

ほとんどネイティブ関数となっていますが、mHandleなる整数メンバを入れていることに注目です。*2こいつをC++から読み書きすることで、Javaオブジェクトにぶら下がったC++インスタンスのアドレスを特定します。
ちなみに、Android標準ライブラリの多くが裏でネイティブを呼び出す形になっているのですが、使われているテクニックはやはり同じのようです。

ラッパークラス用テンプレート

さて、ここからはC++での実装に入っていきます。理屈は極めて単純なものの、毎回新しいラッパークラスを作る度に整数<->ポインタ間キャストについて考えるのは面倒でしょう。というわけで、ラッパークラスを作るための補助テンプレートについて紹介します。参考記事よりはもう少し賢い実装になっていると思います。
ほぼ同様のコードはGitHubにも上げてあります: https://github.com/meryngii/jni_util

template <typename T>
class Wrapper
{
public:
    explicit Wrapper(JNIEnv* env, jobject objj)
        : env_(env), objj_(objj) { }

    Wrapper(const Wrapper&) = default;
    Wrapper& operator = (const Wrapper&) = default;
    
    T& get() const {
        T* ptr = getPointer();
        if (!ptr) {
            throw std::runtime_error("Uninitialized native object was used!");
        }
        
        return *ptr;
    }
    
    template <typename... Args>
    void create(Args&&... args) const {
        destroy(); // delete if an object exists
        setPointer(new T(std::forward(args)...));
    }
    
    void destroy() const {
        delete getPointer();
        setPointer(nullptr);
    }
    
    // Pointer Operations
    T* getPointer() const {
        return reinterpret_cast<T*>(env_->GetLongField(objj_, getFieldID(objj_)));
    }
    void setPointer(T* ptr) const { 
        env_->SetLongField(objj_, getFieldID(objj_), reinterpret_cast<jlong>(ptr));
    }
    
private:
    jfieldID getFieldID(jobject objj) const {
        if (!classj_) {
            classj_ = env_->GetObjectClass(objj_);
            checkException(env_);
        }
        
        if (!fieldID_) {
            fieldID_ = env_->GetFieldID(classj_, "mHandle", "J");
            checkException(env_);
        }
        
        return fieldID_;
    }

private:
    JNIEnv* env_;
    jobject objj_;
    static jclass classj_;
    static jfieldID fieldID_;
};

ポイントとなる関数はgetFieldID, set/getPointerの3つで、後はオマケです。Javaのメンバ変数を読み書きするにはjfieldIDが必要となるので、ネイティブ側でstatic変数にキャッシュしておきます。
jclassやjfieldIDといった値は、実行時に一度手に入れば(確か)プロセス終了時まで有効なので、必ずキャッシュしましょう。ちなみに、jclassやjobjectはクラスやオブジェクトが同じでも異なるポインタ値となる可能性があるので、env->IsSameObjectといった関数がわざわざ定義されています。jfieldIDやjmethodIDは一意性が保たれているのでキーとして使うことも可能ですが、個人的にあまり危なそうなことはオススメしません。
上のWrapperテンプレートを使えば、後はラッパークラスを作るのは簡単です。C++で普通にクラスを作って、それにJNIのインターフェイスを加えます。

class WrapperTest
{
public:
    void put(int x) { data_.push_back(x); }
    
    void show() {
        for (int x : data_)
            std::cout << x << " ";

        std::cout << std::endl;
    }
    
private:
    std::vector<int> data_;
};

extern "C"
{

JNIEXPORT void JNICALL Java_WrapperTest_init
  (JNIEnv* env, jobject thisj)
{
    convertException(env, [=] () {
        Wrapper<WrapperTest>(env, thisj).create();
    });
}

JNIEXPORT void JNICALL Java_WrapperTest_put
  (JNIEnv* env, jobject thisj, jint x)
{
    convertException(env, [=] () {
        auto& self = Wrapper<WrapperTest>(env, thisj).get();
        self.put(x);
    });
}

// ...

}

ネイティブオブジェクトの破棄

基本的な話としては以上ですが、まだオブジェクトの破棄について言及していませんでした。
JavaのオブジェクトはGCによって不定期に開放されます。finalizeメソッドをオーバーライドすることによって、GCが走った時にリソースの最終処理を行うことが可能になっているので、普通に考えればC++のオブジェクトもその際に開放するのが自然な発想でしょう。ところが、finalizeメソッドはいつ呼び出されるか予測が出来ず、しかもアプリケーションの終了タイミングによっては呼び出されない可能性があります。
C++プログラマは「デストラクタは必ず呼び出される」という考えでクラスを設計するので、finalizeにネイティブリソースの破棄を一任するのは良い考えとはいえないでしょう。結局のところ、少々面倒ではありますが素直に破棄用メソッドを作って、Java側のクラス利用者がfinallyなりで責任を持って呼び出すという手法が一番マシと考えられます。上のクラスにはdestroyメソッドを定義してあります。

extern "C"
JNIEXPORT void JNICALL Java_WrapperTest_destroy
  (JNIEnv* env, jobject thisj, jstring strj)
{
    convertException(env, [=] () {
       Wrapper<WrapperTest>(env, thisj).destroy();
    });
}

finalizeは当てになりませんが、一応保険として最後に破棄するようにしておくことは悪くないと思います。そのためにもdestroyは何度呼び出してもOKな作りにしておく必要があるでしょう。

@Override
protected void finalize() throws Throwable
{
    destroy();
    super.finalize();
}

おわりに

最後に、JNIを使う際の注意点について簡単にまとめておきます。

  • Java Native Interface を使用する上でのベスト・プラクティスをじっくり読む。
  • (上にも書いてあるが)JNIの返り値は全力でグローバルにキャッシュする。
  • C++の例外が漏れ出さないように気を付ける。逆に、Java側の例外が発生したまま継続しない。
  • finalizeが呼び出されない可能性に気を付ける。
  • UnsatistiedLinkErrorで泣かない。
    • extern "C"を付けたか?
    • メソッド名・クラス名は正しいか?
    • javahを試したか?
    • System.loadLibraryを忘れていないか?
  • ぶっちゃけJNIは生産性が低いので、出来ることなら使わないほうがいい。(今日のオチ)

お粗末さまでした。

*1:C#にはunsafeなる魔術があるらしいですね…。

*2:Javaのlong型は64bitです。可搬性を考慮して、ポインタを保持する型は大きめに取ったほうがいいと思います。2038年問題を教訓に…。