良いC可変長配列の例[終了]


9

この質問はSOでかなりフリーズしたレセプションを得たので、そこで削除して代わりにここで試すことにしました。ここにも当てはまらないと思われる場合は、少なくとも私が求めている例を見つける方法の提案についてコメントを残してください...

C99 VLAを使用することで、現在の標準ヒープを使用するC ++ RAIIメカニズムなどよりも優れた例を挙げていただけますか?

私が後の例は次のとおりです:

  1. ヒープを使用するよりも簡単に測定できる(おそらく10%)パフォーマンスの利点を実現します。
  2. アレイ全体をまったく必要としない、適切な回避策はありません。
  3. 実際には、最大サイズを固定する代わりに、動的サイズを使用するメリットがあります。
  4. 通常の使用シナリオでスタックオーバーフローが発生することはほとんどありません。
  5. C ++プロジェクトにC99ソースファイルを含めるためのパフォーマンスを必要とする開発者を誘惑するのに十分な強さである。

文脈上のいくつかの明確化を追加:C99の意味と、標準C ++に含まれていないように私は、VLAを意味:int array[n]どこn変数です。そして、私はそれが他の標準(C90、C ++ 11)によって提供される代替手段に勝るユースケースの例の後にいます:

int array[MAXSIZE]; // C stack array with compile time constant size
int *array = calloc(n, sizeof int); // C heap array with manual free
int *array = new int[n]; // C++ heap array with manual delete
std::unique_ptr<int[]> array(new int[n]); // C++ heap array with RAII
std::vector<int> array(n); // STL container with preallocated size

いくつかのアイデア:

  • varargsを取る関数は、当然ながらアイテム数を適切なものに制限しますが、APIレベルの上限はありません。
  • スタックの浪費が望ましくない再帰関数
  • ヒープのオーバーヘッドが悪いであろう多くの小さな割り当てと解放。
  • 多次元配列(任意のサイズの行列など)の処理。パフォーマンスが重要であり、小さな関数は多くのインライン化が期待されます。
  • コメントより:ヒープ割り当てに同期オーバーヘッドがある並行アルゴリズム。

Wikipediaには、私の基準を満たさない例があります。これは、ヒープを使用することの実際的な違いは、少なくともコンテキストがないと無関係であるように見えるためです。コンテキストがなければ、アイテム数がスタックオーバーフローを引き起こす可能性が非常に高いため、これも理想的ではありません。

注:私は具体的にはサンプルコード、またはこの例を自分で実装するためにこれから利益を得るアルゴリズムの提案を求めています。


1
少し投機的です(これは釘を探しているハンマーであるため)が、マルチスレッド環境ではロック競合のため、おそらくマルチスレッド環境でalloca()本当に優れmalloc()ています。しかし、小さな配列は固定サイズを使用するだけで十分であり、大きな配列にはおそらくヒープが必要になるため、これは実際の拡張です。
chrisaycock 2013年

1
@chrisaycockはい、釘を探している非常に多くのハンマーですが、実際に存在するハンマーです(C99 VLAでも、実際には標準allocaではないものでも、基本的に同じものだと思います)。しかし、マルチスレッド化されたものは良いので、質問を編集してそれを含めます!
hyde

VLAの欠点の1つは、割り当ての失敗を検出するメカニズムがないことです。十分なメモリがない場合、動作は未定義です。(同じことが、固定サイズの配列にも当てはまります-alloca()にも当てはまります。)
キース・トンプソン

@KeithThompsonまあ、malloc / newが割り当ての失敗を検出する保証はありません。たとえば、Linuxのmallocのマニュアルページ(linux.die.net/man/3/malloc)を参照してください。
hyde

@hyde:Linuxのmalloc動作がC標準に準拠しているかどうかは議論の余地があります。
キーストンプソン

回答:


9

毎回同じシードで再起動する一連の乱数を生成する小さなプログラムをハッキングして、「公平」かつ「同等」であることを確認しました。それが進むにつれて、これらの値の最小値と最大値がわかります。また、一連の数値が生成されると、minおよびの平均を超える数がカウントされますmax

非常に小さなアレイの場合、VLAが優れていることは明らかな利点を示していますstd::vector<>

これは本当の問題ではありませんが、乱数を使用する代わりに小さなファイルから値を読み取り、同じ種類のコードを使用して他のより意味のあるカウント/最小/最大計算を実行することを簡単に想像できます。

関連する関数の「乱数の数」(x)vlaの非常に小さい値の場合、ソリューションは非常に大きな差で勝ちます。サイズが大きくなると、「勝つ」は小さくなり、十分なサイズが与えられると、ベクトルソリューションはより効率的になります。VLAに何千もの要素が含まれ始めたとき、そのバリアントはあまり研究していませんでした。彼らが何をするつもりだったのか...

そして、誰かがたくさんのテンプレートを使ってこのすべてのコードを書く方法がありcout、実行時にRDTSCとビット以上を実行せずにこれを行う方法があると私に言われると確信しています...しかし、それは本当にそうではないと思いますポイント。

この特定のバリアントを実行すると、func1(VLA)とfunc2(std :: vector)の間に約10%の差が出ます。

count = 9884
func1 time in clocks per iteration 7048685
count = 9884
func2 time in clocks per iteration 7661067
count = 9884
func3 time in clocks per iteration 8971878

これは以下でコンパイルされます: g++ -O3 -Wall -Wextra -std=gnu++0x -o vla vla.cpp

これがコードです:

#include <iostream>
#include <vector>
#include <cstdint>
#include <cstdlib>

using namespace std;

const int SIZE = 1000000;

uint64_t g_val[SIZE];


static __inline__ unsigned long long rdtsc(void)
{
    unsigned hi, lo;
    __asm__ __volatile__ ("rdtsc" : "=a"(lo), "=d"(hi));
    return ( (unsigned long long)lo)|( ((unsigned long long)hi)<<32 );
}


int func1(int x)
{
    int v[x];

    int vmax = 0;
    int vmin = x;
    for(int i = 0; i < x; i++)
    {
        v[i] = rand() % x;
        if (v[i] > vmax) 
            vmax = v[i];
        if (v[i] < vmin) 
            vmin = v[i];
    }
    int avg = (vmax + vmin) / 2;
    int count = 0;
    for(int i = 0; i < x; i++)
    {
        if (v[i] > avg)
        {
            count++;
        }
    }
    return count;
}

int func2(int x)
{
    vector<int> v;
    v.resize(x); 

    int vmax = 0;
    int vmin = x;
    for(int i = 0; i < x; i++)
    {
        v[i] = rand() % x;
        if (v[i] > vmax) 
            vmax = v[i];
        if (v[i] < vmin) 
            vmin = v[i];
    }
    int avg = (vmax + vmin) / 2;
    int count = 0;
    for(int i = 0; i < x; i++)
    {
        if (v[i] > avg)
        {
            count++;
        }
    }
    return count;
}    

int func3(int x)
{
    vector<int> v;

    int vmax = 0;
    int vmin = x;
    for(int i = 0; i < x; i++)
    {
        v.push_back(rand() % x);
        if (v[i] > vmax) 
            vmax = v[i];
        if (v[i] < vmin) 
            vmin = v[i];
    }
    int avg = (vmax + vmin) / 2;
    int count = 0;
    for(int i = 0; i < x; i++)
    {
        if (v[i] > avg)
        {
            count++;
        }
    }
    return count;
}    

void runbench(int (*f)(int), const char *name)
{
    srand(41711211);
    uint64_t long t = rdtsc();
    int count = 0;
    for(int i = 20; i < 200; i++)
    {
        count += f(i);
    }
    t = rdtsc() - t;
    cout << "count = " << count << endl;
    cout << name << " time in clocks per iteration " << dec << t << endl;
}

struct function
{
    int (*func)(int);
    const char *name;
};


#define FUNC(f) { f, #f }

function funcs[] = 
{
    FUNC(func1),
    FUNC(func2),
    FUNC(func3),
}; 


int main()
{
    for(size_t i = 0; i < sizeof(funcs)/sizeof(funcs[0]); i++)
    {
        runbench(funcs[i].func, funcs[i].name);
    }
}

うわー、私のシステムではVLAバージョンが30%向上していstd::vectorます。
chrisaycock

1
まあ、サイズ範囲を20〜200ではなく5〜15にしてみてください。そうすれば、おそらく1000%以上の改善になるでしょう。[コンパイラオプションにも依存-上記のコードを編集して、gccでコンパイラオプションを表示します]
Mats Petersson

の代わりにfunc3を使用しての必要性を削除したを追加しました。を使用する場合と比較して、約10%長くかかります。[もちろん、その過程で、の使用が関数の所要時間の主な原因であることがわかりました-少し驚いています]。v.push_back(rand())v[i] = rand();resize()resize()v[i]
Mats Petersson 2013年

1
@MikeBrown std::vectorVLA /を使用する実際の実装を知っていますかalloca、それとも単なる推測ですか?
hyde

3
ベクトルは確かに内部的に配列を使用しますが、私の知る限り、VLAを使用する方法はありません。私の例では、データ量が少ない場合(おそらく多くても)にVLAが役立つことを示しています。ベクトルがVLAを使用している場合でも、vector実装内で追加の作業が必要になります。
Mats Petersson 2013年

0

VLAとベクターについて

ベクターがVLA自体を利用できると考えましたか?VLAがない場合、ベクターは配列の特定の「スケール」、たとえば10、100、10000をストレージに指定する必要があるため、101アイテムを保持するために10000アイテムの配列を割り当てることになります。VLAを使用して、サイズを200に変更すると、アルゴリズムは200のみが必要であると想定し、200の項目配列を割り当てることができます。または、n * 1.5のバッファを割り当てることもできます。

とにかく、実行時に必要なアイテムの数がわかっている場合は、VLAの方がパフォーマンスが高い(Matsのベンチマークが示すように)と私は主張します。彼が示したのは、単純な2パスの反復です。ランダムなサンプルが繰り返し取られるモンテカルロシミュレーション、または各要素に対して複数回計算が行われる画像操作(Photoshopフィルターなど)について考えてみてください。各要素の各計算では、近傍の確認が必要になる可能性があります。

ベクトルからその内部配列への追加のポインタージャンプが加算されます。

主な質問に答える

ただし、LinkedListのような動的に割り当てられる構造の使用について話すとき、比較はありません。配列は、その要素へのポインタ演算を使用して直接アクセスを提供します。リンクされたリストを使用して、特定の要素に到達するためにノードをウォークする必要があります。したがって、このシナリオではVLAが勝者となります。

この回答による、それはアーキテクチャに依存していますが、場合によっては、スタックがキャッシュで使用できるため、スタックへのメモリアクセスが高速になります。多数の要素がある場合、これは無効になる可能性があります(マットがベンチマークで見た利益の減少の原因となる可能性があります)。ただし、キャッシュサイズが大幅に増加しているため、それに応じてその数も増加する可能性があることに注意してください。


リンクされたリストへの参照を理解しているとは思えないので、質問にセクションを追加し、コンテキストをもう少し説明し、考えている選択肢の例を追加しました。
hyde

なぜstd::vectorアレイのスケールが必要なのでしょうか?101個しか必要ないのに、なぜ10K要素のスペースが必要なのでしょうか また、この質問ではリンクリストについては触れられていないため、どこから取得したのかはわかりません。最後に、C99のVLAはスタックに割り当てられます。それらはの標準形式ですalloca()。ヒープストレージ(関数が戻った後も存続する)またはa realloc()(配列のサイズが変更される)が必要な場合は、いずれにしてもVLAは禁止されます。
chrisaycock

@chrisaycock C ++は、メモリがnew []で割り当てられると想定して、何らかの理由でrealloc()関数を欠いています。std :: vectorがスケールを使用する必要があるのは、それが主な理由ではありませんか?

@Lundin C ++はベクトルを10の累乗でスケーリングしますか?リンクされたリストの参照を考えると、マイクブラウンは質問で本当に混乱しているという印象を受けました。(彼はまた、C99 VLAがヒープ上に存在することを暗示するという以前の主張をしました。)
chrisaycock 2013年

@hydeそれがあなたの言っていることに気づかなかった。他のヒープベースのデータ構造を意味していると思いました。この説明を追加したので興味深いです。これらの違いを説明するのに十分なC ++オタクではありません。
マイケルブラウン

0

VLAを使用する理由は主にパフォーマンスです。ウィキの例を「無関係な」違いだけがあるとして無視するのは誤りです。たとえば、その関数がタイトなループで呼び出された場合、そのコードに大きな違いがある可能性があるケースを簡単に確認できます。read_val。IO関数は、速度が重要なある種のシステムで非常に迅速に返されました。

実際、VLAがこのように使用されるほとんどの場所では、VLAはヒープ呼び出しを置き換えるのではなく、次のようなものを置き換えます。

float vals[256]; /* I hope we never get more! */

ローカル宣言についての事はそれが非常に速いということです。この行はfloat vals[n]通常、2、3のプロセッサ命令のみを必要とします(おそらく1つだけです)。nスタックポインタにです。

一方、ヒープ割り当てでは、データ構造をウォークして空き領域を見つける必要があります。幸運な場合でも、時間はおそらく1桁長くなります。(nつまり、スタックに配置して呼び出すという行為mallocは、おそらく5〜10命令です。)ヒープに適切な量のデータがある場合は、おそらくもっと悪いことになります。こんなケースを見て驚かなかったmalloc実際のプログラムで100倍から1000倍遅いも。

もちろん、マッチングによってパフォーマンスにも影響がありますfree。おそらく、malloc呼び出しとです。

さらに、メモリの断片化の問題があります。小さな割り当てがたくさんあると、ヒープが断片化する傾向があります。断片化されたヒープは、メモリを浪費し、メモリの割り当てに必要な時間を増加させます。


ウィキペディアの例について:それは良い例の一部である可能性がありますが、コンテキストがなければ、その周りのより多くのコードで、私の質問に列挙されている5つのことのどれも実際には表示されません。そうでなければ、私はあなたの説明に同意します。ただし、VLAを使用するとローカル変数にアクセスするコストがかかる可能性があるため、コンパイル時にすべてのローカル変数のオフセットがわかっているとは限らないため、1回限りのヒープコストをすべての反復に対する内部ループのペナルティ。
hyde

ええと...どういう意味かわからない。ローカル変数の宣言は単一の操作であり、穏やかに最適化されたコンパイラーは、内部ループから割り当てを引き出します。ローカル変数へのアクセスに特定の「コスト」はありません。確かに、VLAが増加することはありません。
Gort the Robot

具体例::のint vla[n]; if(test()) { struct LargeStruct s; int i; }スタックオフセットはsコンパイル時に不明であり、コンパイラがi内部スコープのストレージを固定スタックオフセットに移動するかどうかも疑わしいです。したがって、間接性のために追加のマシンコードが必要であり、これはPCハードウェアで重要なレジスタを消費する可能性もあります。コンパイラアセンブリ出力を含むサンプルコードが必要な場合は、別の質問をしてください;)
hyde

コンパイラは、コードで検出された順序で割り当てる必要はなく、スペースが割り当てられていても使用されていなくてもかまいません。スマートオプティマイザはのためのスペースを割り当てるだろうsし、i機能が入力されると、前testに呼び出されたりvla、割り当てられているために配分として、sかつi副作用がありません。(そして、実際にiは、レジスターに配置されることもあり、「割り当て」がまったくないことを意味します。)スタック上の割り当ての順序、またはスタックが使用されることさえも、コンパイラーは保証しません。
Gort the Robot

(愚かな間違いのために間違っていたコメントを削除しました)
hyde
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.