36

可変長配列 (VLA) を多用する C99 コードを C++ に移植しています。

VLA (スタック割り当て) を、ヒープにメモリを割り当てる配列クラスに置き換えました。パフォーマンスへの影響は大きく、3.2 倍の速度低下でした (以下のベンチマークを参照)。 C++ で使用できる高速な VLA の置き換えは何ですか? 私の目標は、C++ のコードを書き直すときにパフォーマンスへの影響を最小限に抑えることです。

私に提案された 1 つのアイデアは、クラス内に固定サイズのストレージを含む (つまり、スタック割り当て可能) 配列クラスを作成し、それを小さな配列に使用し、大きな配列のヒープ割り当てに自動的に切り替えることでした。これの私の実装は、投稿の最後にあります。これはかなりうまく機能しますが、元の C99 コードのパフォーマンスにはまだ達していません。それに近づくには、この固定サイズのストレージ (MSL以下) を、私が慣れていないサイズに増やす必要があります。スタックオーバーフローを引き起こすのではないかと心配しているため、それを必要としない多くの小さな配列に対しても、スタックに大きすぎる配列を割り当てたくありません。C99 VLA は、必要以上のストレージを使用することはないため、実際にはこの傾向はありません。

私は出くわしましstd::dynarrayたが、私の理解では、それは標準に受け入れられていませんでした (まだ?)。

Clang と gcc が C++ で VLA をサポートしていることは知っていますが、MSVC でも動作させる必要があります。実際、移植性の向上は、C++ として書き直す主な目標の 1 つです (もう 1 つの目標は、元はコマンド ライン ツールであったプログラムを再利用可能なライブラリにすることです)。


基準

MSLヒープ割り当てに切り替える配列サイズを指します。1D 配列と 2D 配列に異なる値を使用します。

元の C99 コード: 115 秒。
MSL = 0 (ヒープ割り当て): 367 秒 (3.2x)。
1D-MSL = 50、2D-MSL = 1000: 187 秒 (1.63x)。
1D-MSL = 200、2D-MSL = 4000: 143 秒 (1.24x)。
1D-MSL = 1000、2D-MSL = 20000: 131 (1.14x)。

さらに増やすMSLとパフォーマンスが向上しますが、最終的にプログラムは間違った結果を返し始めます (スタックオーバーフローが原因だと思います)。

これらのベンチマークは OS X の clang 3.7 を使用したものですが、gcc 5 は非常に類似した結果を示しています。


コード

これは、私が使用している現在の「smallvector」実装です。1D および 2D ベクトルが必要です。size を超えるヒープ割り当てに切り替えますMSL

template<typename T, size_t MSL=50>
class lad_vector {
    const size_t len;
    T sdata[MSL];
    T *data;
public:
    explicit lad_vector(size_t len_) : len(len_) {
        if (len <= MSL)
            data = &sdata[0];
        else
            data = new T[len];
    }

    ~lad_vector() {
        if (len > MSL)
            delete [] data;
    }

    const T &operator [] (size_t i) const { return data[i]; }
    T &operator [] (size_t i) { return data[i]; }

    operator T * () { return data; }
};


template<typename T, size_t MSL=1000>
class lad_matrix {
    const size_t rows, cols;
    T sdata[MSL];
    T *data;

public:
    explicit lad_matrix(size_t rows_, size_t cols_) : rows(rows_), cols(cols_) {
        if (rows*cols <= MSL)
            data = &sdata[0];
        else
            data = new T[rows*cols];
    }

    ~lad_matrix() {
        if (rows*cols > MSL)
            delete [] data;
    }

    T const * operator[] (size_t i) const { return &data[cols*i]; }
    T * operator[] (size_t i) { return &data[cols*i]; }
};
4

3 に答える 3

39

スレッド ローカル ストレージに大きなバッファー (MB+) を作成します。(ヒープ上の実際のメモリ、TLS での管理)。

クライアントが FILO 方式 (スタックのような) でメモリを要求できるようにします。(これは C VLA での動作を模倣しています。また、各リクエスト/リターンは整数の加算/減算であるため、効率的です)。

そこから VLA ストレージを取得します。

stack_array<T> x(1024);それをきれいにラップして、と言うことができ、それstack_arrayを構築/破壊に対処させるか ( ->~T()where Tisintは合法的なヌープであり、建設も同様にヌープになる可能性があることに注意してください)、またはstack_array<T>ラップを作成しstd::vector<T, TLS_stack_allocator>ます。

データは実質的に別のスタック上にあるため、C VLA データほどローカルではありません。SBO (小さなバッファーの最適化) を使用できます。これは、局所性が本当に重要な場合です。

SBOstack_array<T>は、アロケータと std 配列と結合された std ベクトル、または一意の ptr とカスタム デストロイダ、またはその他の無数の方法で実装できます。new/malloc/free/delete を上記の TLS ストレージへの呼び出しに置き換えて、おそらくソリューションを改良することができます。

マルチスレッドの使用を許可しながら同期オーバーヘッドの必要性を取り除き、スタック自体が暗黙的にTLSであるという事実を反映しているため、TLSを使用すると言います。

スタックバッファベースの STL アロケータ? 回答に少なくとも 2 つの「スタック」アロケータがある SO Q&A です。TLS からバッファを自動的に取得するには、何らかの調整が必要です。

1 つの大きなバッファーである TLS は、ある意味では実装の詳細であることに注意してください。大規模な割り当てを行うことができ、スペースがなくなったら別の大規模な割り当てを行うことができます。各「スタック ページ」の現在の容量とスタック ページのリストを追跡するだけでよいため、いずれかを空にすると、以前のページに移動できます。これにより、OOM の実行について心配することなく、TLS の初期割り当てをもう少し保守的にすることができます。重要な部分は、FILO バッファー全体が 1 つの連続したバッファーであるということではなく、FILO であり、めったに割り当てないことです。

于 2016-04-03T21:01:23.510 に答える
17

質問とコメントですでにほとんどのオプションを列挙していると思います。

  • を使用しstd::vectorます。これは最も明白で、最も手間のかからない方法ですが、おそらく最も遅い解決策でもあります。
  • それらを提供するプラットフォームでは、プラットフォーム固有の拡張機能を使用します。たとえば、GCC は拡張機能として C++の可変長配列をサポートしています。POSIXallocaは、スタックにメモリを割り当てるために広くサポートされているものを指定します。_malloca簡単な Web 検索でわかったように、Microsoft Windows でさえ を提供しています。

    メンテナンスの悪夢を避けるために、これらのプラットフォームの依存関係を、現在のプラットフォームに適したメカニズムを自動的かつ透過的に選択する抽象的なインターフェイスにカプセル化する必要があります。これをすべてのプラットフォームに実装するのは少し手間がかかりますが、この 1 つの機能で報告している 3 倍の速度差が発生する場合は、その価値があるかもしれません。std::vector不明なプラットフォームのフォールバックとして、私は最後の手段として予約しておきます。ゆっくりと正しく実行する方が、不安定な動作をしたり、まったく実行しないよりも優れています。

  • 質問で示したように、オブジェクト自体の内部にバッファーとして埋め込まれた「小さな配列」最適化を実装する独自の可変サイズの配列型を構築します。独自のコンテナーをローリングする代わりにunion、 astd::arrayと aを使用してみることをお勧めします。std::vector

    カスタム型を配置したら、この型のすべての発生のグローバル ハッシュ テーブルを (ソース コードの場所ごとに) 維持したり、プログラムのストレス テスト中に各割り当てサイズを記録したりするなど、興味深いプロファイリングを行うことができます。次に、プログラムの終了時にハッシュ テーブルをダンプし、個々の配列の割り当てサイズで分布をプロットできます。これは、スタック上の各アレイに個別に予約するストレージの量を微調整するのに役立つ場合があります。

  • std::vectorカスタム アロケータで a を使用します。プログラムの起動時に、数メガバイトのメモリを割り当て、単純なスタック アロケータに渡します。スタック アロケーターの場合、割り当ては単に 2 つの整数を比較して加算することであり、解放は単純に減算です。コンパイラによって生成されたスタック割り当てがはるかに高速になるとは思えません。「アレイ スタック」は、「プログラム スタック」に関連して脈動します。この設計には、偶発的なバッファ オーバーランが発生しても、未定義の動作が呼び出され、ランダム データやその他すべての悪いデータが破壊されても、ネイティブ VLA の場合ほど簡単にプログラム スタック (リターン アドレス) が破損しないという利点があります。

    C++ のカスタム アロケータはやや汚いビジネスですが、一部の人々はそれらをうまく使用していると報告しています。(私はそれらを自分で使用した経験があまりありません。) cppreferenceを見始めることをお勧めします。カスタム アロケーターの使用を促進する人物の 1 人である Alisdair Meredith は、CppCon'14 で「Making Allocators Work」(パート 1パート 2 ) というタイトルのダブルセッション トークを行いました。std::allocatorインターフェイスが使いにくい場合は、独自のアロケーターを使用して独自の変数(動的ではなく) サイズの配列クラスを実装することも可能です。

于 2016-04-03T20:48:34.637 に答える
9

MSVC のサポートについて:

MSVCには_alloca、スタックスペースを割り当てるものがあります。また_malloca、十分な空きスタック領域がある場合はスタック領域を割り当て、それ以外の場合は動的割り当てにフォールバックします。

VLA 型システムを利用することはできないため、そのような配列の最初の要素へのポインターに基づいて動作するようにコードを変更する必要があります。

プラットフォームによって定義が異なるマクロを使用する必要が生じる場合があります。たとえば、invoke_allocaまたは_mallocaMSVC、および g++ またはその他のコンパイラでは、alloca(サポートされている場合) を呼び出すか、VLA とポインターを作成します。


不明な量のスタックを割り当てる必要なくコードを書き直す方法を調査することを検討してください。1 つのオプションは、必要な最大の固定サイズのバッファーを割り当てることです。(それがスタック オーバーフローを引き起こす場合は、とにかくコードにバグがあることを意味します)。

于 2016-04-03T23:49:39.463 に答える