web-dev-qa-db-ja.com

良いC可変長配列の例

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

を与えることができますが、C99 VLAを使用すると、現在の標準ヒープを使用するC++ RAIIメカニズムのようなものよりも大きな利点がありますか?

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

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

コンテキストに関する説明を追加します。つまり、C99が意味するVLAを意味し、標準のC++には含まれていません: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レベルの便利な上限はありません。
  • スタックの浪費が望ましくない再帰関数
  • ヒープのオーバーヘッドが悪いであろう多くの小さな割り当てと解放。
  • 多次元配列(任意のサイズの行列など)の処理。パフォーマンスが重要であり、小さな関数は多くのインライン化が期待されます。
  • コメントから:同時アルゴリズム、ここで ヒープ割り当てには同期オーバーヘッドがあります

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

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

9
hyde

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

非常に小さなアレイの場合、VLAがstd::vector<>

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

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

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

この特定のバリアントを実行すると、func1(VLA)およびfunc2(std :: vector)。

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);
    }
}
9
Mats Petersson

VLAとベクターについて

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

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

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

主な質問への回答

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

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

0
Michael Brown

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

実際、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の呼び出しと同等の大きさです。

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

0
Gort the Robot