web-dev-qa-db-ja.com

std :: bitsetのパフォーマンスはどうですか?

私は最近、std::bitsetでプリミティブ型の手動ビット操作を使用する理由に関して Programmers に関する質問をしました。

その議論から、私はこの意見の測定された根拠を知らないが、その主な理由はその比較的低いパフォーマンスであると結論付けました。次の質問は:

what isプリミティブのビット操作でstd::bitsetを使用することで発生する可能性があるパフォーマンスヒット(ある場合)?

質問は意図的に広範です。なぜなら、オンラインで見ても何も見つからなかったので、手に入れることができるものを手に入れるからです。基本的に、GCC、Clang、VC++を使用する一般的なマシンアーキテクチャ上の同じ問題に対するstd::bitset対 'pre-bitset'の代替のプロファイリングを提供するリソースを探しています。ビットベクトルについてこの質問に答えようとする非常に包括的な論文があります:

http://www.cs.up.ac.za/cs/vpieterse/pub/PieterseEtAl_SAICSIT2010.pdf

残念ながら、それはstd::bitsetよりも前のものか、スコープ外とみなされているため、代わりにベクター/動的配列の実装に焦点を当てています。

私は本当にstd::bitsetbetterであるかどうか、それが解決しようとしているユースケースの代替案よりも知りたいだけなのです。私はすでに整数をビット操作するよりもeasierおよびclearerであることを知っていますが、fast

35
quant

順次およびランダムアクセス用のstd :: bitset vs bool配列をプロファイリングする短いテストを行いました-あなたもできます:

#include <iostream>
#include <bitset>
#include <cstdlib> // Rand
#include <ctime> // timer

inline unsigned long get_time_in_ms()
{
    return (unsigned long)((double(clock()) / CLOCKS_PER_SEC) * 1000);
}


void one_sec_delay()
{
    unsigned long end_time = get_time_in_ms() + 1000;

    while(get_time_in_ms() < end_time)
    {
    }
}



int main(int argc, char **argv)
{
    srand(get_time_in_ms());

    using namespace std;

    bitset<5000000> bits;
    bool *bools = new bool[5000000];

    unsigned long current_time, difference1, difference2;
    double total;

    one_sec_delay();

    total = 0;
    current_time = get_time_in_ms();

    for (unsigned int num = 0; num != 200000000; ++num)
    {
        bools[Rand() % 5000000] = Rand() % 2;
    }

    difference1 = get_time_in_ms() - current_time;
    current_time = get_time_in_ms();

    for (unsigned int num2 = 0; num2 != 100; ++num2)
    {
        for (unsigned int num = 0; num != 5000000; ++num)
        {
            total += bools[num];
        }
    }   

    difference2 = get_time_in_ms() - current_time;

    cout << "Bool:" << endl << "sum total = " << total << ", random access time = " << difference1 << ", sequential access time = " << difference2 << endl << endl;


    one_sec_delay();

    total = 0;
    current_time = get_time_in_ms();

    for (unsigned int num = 0; num != 200000000; ++num)
    {
        bits[Rand() % 5000000] = Rand() % 2;
    }

    difference1 = get_time_in_ms() - current_time;
    current_time = get_time_in_ms();

    for (unsigned int num2 = 0; num2 != 100; ++num2)
    {
        for (unsigned int num = 0; num != 5000000; ++num)
        {
            total += bits[num];
        }
    }   

    difference2 = get_time_in_ms() - current_time;

    cout << "Bitset:" << endl << "sum total = " << total << ", random access time = " << difference1 << ", sequential access time = " << difference2 << endl << endl;

    delete [] bools;

    cin.get();

    return 0;
}

注:合計の出力が必要なので、コンパイラーはforループを最適化しません-ループの結果が使用されない場合は最適化されません。

GCC x64で次のフラグを使用:-O2; -Wall; -march = native; -fomit-frame-pointer; -std = c ++ 11;次の結果が得られます。

ブール配列:ランダムアクセス時間= 4695、シーケンシャルアクセス時間= 390

ビットセット:ランダムアクセス時間= 5382、シーケンシャルアクセス時間= 749

12
metamorphosis

アクセスのパフォーマンスに関する他の回答に加えて、スペースのオーバーヘッドが大きくなる可能性があります。典型的なbitset<>実装では、単に最長の整数型を使用してビットをバックアップします。したがって、次のコード

#include <bitset>
#include <stdio.h>

struct Bitfield {
    unsigned char a:1, b:1, c:1, d:1, e:1, f:1, g:1, h:1;
};

struct Bitset {
    std::bitset<8> bits;
};

int main() {
    printf("sizeof(Bitfield) = %zd\n", sizeof(Bitfield));
    printf("sizeof(Bitset) = %zd\n", sizeof(Bitset));
    printf("sizeof(std::bitset<1>) = %zd\n", sizeof(std::bitset<1>));
}

私のマシンで次の出力を生成します:

sizeof(Bitfield) = 1
sizeof(Bitset) = 8
sizeof(std::bitset<1>) = 8

ご覧のとおり、私のコンパイラーは64ビットを割り当てて1つのビットを格納します。ビットフィールドアプローチでは、8ビットに切り上げるだけで済みます。

小さなビットセットがたくさんある場合、スペースの使用におけるこの8つの要素は重要になります。

4
cmaster

修辞的な質問:なぜstd::bitsetはその非効率的な方法で書かれているのですか?回答:そうではありません。

別の修辞的な質問:違いは何ですか:

std::bitset<128> a = src;
a[i] = true;
a = a << 64;

そして

std::bitset<129> a = src;
a[i] = true;
a = a << 63;

回答:パフォーマンスの50倍の違い http://quick-bench.com/iRokweQ6JqF2Il-T-9JSmR0bdyw

bitsetは多くのものをサポートしますが、それぞれにコストがかかります。正しく処理すると、生コードとまったく同じ動作になります。

void f(std::bitset<64>& b, int i)
{
    b |= 1L << i;
    b = b << 15;
}
void f(unsigned long& b, int i)
{
    b |= 1L << i;
    b = b << 15;
}

どちらも同じアセンブリを生成します: https://godbolt.org/g/PUUUyd (64ビットGCC)

もう1つは、bitsetの方が移植性が高いことですが、これにはコストもかかります。

void h(std::bitset<64>& b, unsigned i)
{
    b = b << i;
}
void h(unsigned long& b, unsigned i)
{
    b = b << i;
}

i > 64の場合、ビットセットはゼロになり、符号なしの場合はUBになります。

void h(std::bitset<64>& b, unsigned i)
{
    if (i < 64) b = b << i;
}
void h(unsigned long& b, unsigned i)
{
    if (i < 64) b = b << i;
}

チェックを無効にすると、UBは両方とも同じコードを生成します。

別の場所はset[]です。最初の場所は安全であり、UBを取得することはありませんが、これにはブランチがかかります。 []は、間違った値を使用してもUBを持っていますが、var |= 1L<< i;を使用するよりも高速です。 std::bitsetがシステムで利用できる最大の整数よりも多くのビットを必要としない場合は、内部テーブルの正しい要素を取得するために値を分割する必要があるためです。 std::bitset<N>サイズNのこの平均は、パフォーマンスにとって非常に重要です。最適なものよりも大きいか小さい場合は、その費用をお支払いいただきます。

全体として、そのようなものを使用するのが最善の方法であることがわかりました。

constexpr size_t minBitSet = sizeof(std::bitset<1>)*8;

template<size_t N>
using fasterBitSet = std::bitset<minBitSet * ((N  + minBitSet - 1) / minBitSet)>;

これにより、ビットを超えるトリミングのコストが削除されます。 http://quick-bench.com/Di1tE0vyhFNQERvucAHLaOgucAY

3
Yankes

ここでは良い答えではなく、関連する逸話です。

数年前、私はリアルタイムソフトウェアに取り組んでいて、スケジューリングの問題にぶつかりました。予算をはるかに超えるモジュールがありましたが、これは非常に驚くべきことでした。モジュールは、32ビットワードへの/からのビットのマッピングとパッキング/アンパッキングのみを担当するためです。

モジュールがstd :: bitsetを使用していることが判明しました。これを手動操作に置き換え、実行時間を3ミリ秒から25マイクロ秒に短縮しました。これは、重大なパフォーマンスの問題であり、大幅な改善でした。

重要なのは、このクラスによって引き起こされるパフォーマンスの問題は非常に現実的なものになる可能性があるということです。

3
Stewart