-3

最近、いくつかのパフォーマンス ベンチマークを実行しようとしていてstd::stack<int, std::vector<int>>、スタックの単純な実装 (事前に割り当てられたメモリを使用) と比較しました。今、私はいくつかの奇妙な行動を経験しています。

最初に質問したいのは、スタック ベンチマーク コードの次の行です。

//  std::vector<int> magicVector(10);

この行のコメントを外すと、パフォーマンスが約 17% 向上します (ベンチマーク時間が 6.5 秒から 5.4 秒に短縮されます)。ただし、この行は他のメンバーを変更していないため、プログラムの残りの部分に影響を与えることはありません。それに、intのベクトルかdoubleのベクトルかは関係ありません...

2 番目に聞きたいことは、スタックの実装とstd::stack. std::stack私のスタックと同じくらい速いはずだと 言われましたが、結果は私の「FastStack」が2倍速いことを示しています。

結果(コメントなしのパフォーマンス向上行):
stack 5.38979
stack 5.34406
stack 5.32404
stack 5.30519
FastStack 2.59635
FastStack 2.59204
FastStack 2.59713
FastStack 2.64814

これらの結果は、/O2、/Ot、/Ob2 およびその他の既定の最適化を使用した VS2010 からのリリース ビルドから得られたものです。私の CPU は Intel i5 3570k で、デフォルトのクロック (1 スレッドで 3.6 GHz) です。

誰でも簡単にテストできるように、すべてのコードを 1 つのファイルにまとめました。

#define _SECURE_SCL 0

#include <iostream>
#include <vector>
#include <stack>
#include <Windows.h>

using namespace std;

//---------------------------------------------------------------------------------
//---------------------------------------------------------------------------------
//  Purpose:    High Resolution Timer
//---------------------------------------------------------------------------------

class HRTimer
{
public:
    HRTimer();
    double GetFrequency(void);
    void Start(void) ;
    double Stop(void);
    double GetTime();

private:
    LARGE_INTEGER start;
    LARGE_INTEGER stop;
    double frequency;
};

HRTimer::HRTimer()
{
    frequency = this->GetFrequency();
}

double HRTimer::GetFrequency(void)
{
    LARGE_INTEGER proc_freq;
    if (!::QueryPerformanceFrequency(&proc_freq))
        return -1;
    return proc_freq.QuadPart;
}

void HRTimer::Start(void)
{
    DWORD_PTR oldmask = ::SetThreadAffinityMask(::GetCurrentThread(), 0);
    ::QueryPerformanceCounter(&start);
    ::SetThreadAffinityMask(::GetCurrentThread(), oldmask);
}

double HRTimer::Stop(void)
{
    DWORD_PTR oldmask = ::SetThreadAffinityMask(::GetCurrentThread(), 0);
    ::QueryPerformanceCounter(&stop);
    ::SetThreadAffinityMask(::GetCurrentThread(), oldmask);
    return ((stop.QuadPart - start.QuadPart) / frequency);
} 

double HRTimer::GetTime()
{
    LARGE_INTEGER time;
    ::QueryPerformanceCounter(&time);
    return time.QuadPart / frequency;
}

//---------------------------------------------------------------------------------
//---------------------------------------------------------------------------------
//  Purpose:    Should be faster than std::stack
//---------------------------------------------------------------------------------

template <class T>

class FastStack
{
public:
    T* st;
    int allocationSize;
    int lastIndex;

public:
    FastStack(int stackSize);
    ~FastStack();

    inline void resize(int newSize);
    inline void push(T x);
    inline void pop();
    inline T getAndRemove();
    inline T getLast();
    inline void clear();
};

template <class T>
FastStack<T>::FastStack( int stackSize )
{
    st = NULL;
    this->allocationSize = stackSize;
    st = new T[stackSize];
    lastIndex = -1;
}

template <class T>
FastStack<T>::~FastStack()
{
    delete [] st;
}

template <class T>
void FastStack<T>::clear()
{
    lastIndex = -1;
}

template <class T>
T FastStack<T>::getLast()
{
    return st[lastIndex];
}

template <class T>
T FastStack<T>::getAndRemove()
{
    return st[lastIndex--];
}

template <class T>
void FastStack<T>::pop()
{
    --lastIndex;
}

template <class T>
void FastStack<T>::push( T x )
{
    st[++lastIndex] = x;
}

template <class T>
void FastStack<T>::resize( int newSize )
{
    if (st != NULL)
        delete [] st;
    st = new T[newSize];
}
//---------------------------------------------------------------------------------
//---------------------------------------------------------------------------------
//---------------------------------------------------------------------------------
//  Purpose:    Benchmark of std::stack and FastStack
//---------------------------------------------------------------------------------


int main(int argc, char *argv[])
{
#if 1
    for (int it = 0; it < 4; it++)
    {
        std::stack<int, std::vector<int>> bStack;
        int x;

        for (int i = 0; i < 100; i++)   // after this two loops, bStack's capacity will be 141 so there will be no more reallocating
            bStack.push(i);
        for (int i = 0; i < 100; i++)
            bStack.pop();
    //  std::vector<int> magicVector(10);           // when you uncomment this line, performance will magically rise about 18%

        HRTimer timer;
        timer.Start();

        for (int i = 0; i < 2000000000; i++)
        {
            bStack.push(i);
            x = bStack.top();
            if (i % 100 == 0 && i != 0)
                for (int j = 0; j < 100; j++)
                    bStack.pop();
        }

        double totalTime = timer.Stop();
        cout << "stack " << totalTime << endl;
    }
#endif

    //------------------------------------------------------------------------------------

#if 1
    for (int it = 0; it < 4; it++)
    {
        FastStack<int> fstack(200);
        int x;

        HRTimer timer;
        timer.Start();

        for (int i = 0; i < 2000000000; i++)
        {
            fstack.push(i);
            x = fstack.getLast();
            if (i % 100 == 0 && i != 0)
                for (int j = 0; j < 100; j++)
                    fstack.pop();
        }

        double totalTime = timer.Stop();
        cout << "FastStack " << totalTime << endl;
    }
#endif

    cout << "Done";
    cin.get();
    return 0;
}

.
編集:私のスタックの本当に悪い実装について誰もが話しているので、私は物事を正しく設定したいと思います。そのスタックを数分で作成し、現在必要な機能をいくつか実装しました。std::stack :) の代わりになることや、すべての場合に使用するために保存することを意図したものではありませんでした。唯一の目標は、最高速度と正しい結果を達成することでした。この誤解については申し訳ありません…私はいくつかの答えを知りたいだけです…</p>

4

3 に答える 3

5

メソッドの実装はすべて壊れています。コピー コンストラクターやその他の不足している操作を無視すると、pushプッシュしすぎると UB が呼び出されます。以前のデータコピーせず、例外セーフはなく、プッシュが例外セーフはなく、呼び出しが多すぎるため、UBresizeは明らかに壊れています。コピーあなたは例外安全ではなく、ポップされた要素破棄せず、新しい要素を適切に構築せず、それら割り当てるだけで、作成時に不必要にデフォルト構築します。getAndRemove

基本的に、あなたのクラスは、考えられるあらゆる点で非常に恐ろしく危険であり、ユーザーのデータを一瞬で破壊し、で間違った関数をすべて呼び出し、Tどこかで例外がスローされた瞬間に隅で泣き崩れます。

それは悪いことの巨大な山であり、それがより「速い」という事実std::stackは、まあ、まったく無関係です。なぜなら、あなたが証明したのは、要件を満たす必要がなければ、好きなだけ速く進むことができるということだけだからです。私たち全員がすでに知っていたこと。

基本的に、sbi が言ったように、あなたは明らかに のセマンティクスやstd::stack、例外の安全性などの重要な C++ の側面を理解していません。先は長いよ、友よ。

于 2012-10-03T10:37:56.423 に答える
4

std::stackusingとは対照的に、std::vectorスペースがなくなってもスタックは再割り当てされず、単に惑星を爆破します。ただし、割り当てはパフォーマンスを大幅に低下させるため、それをスキップすると確実にパフォーマンスが向上します。

ただし、あなたの代わりに、ウェブ上に浮かぶ老朽化した実装の1つを取得static_vectorし、. そうすれば、パフォーマンスを大量に消費する動的メモリ処理をすべてスキップできますが、その下にメモリ処理用のコンテナを備えた有効なスタック実装があり、それはあなたが思いついたものよりもはるかに優れている可能性が非常に高いです.std::stackstd::vector

于 2012-10-03T12:57:42.370 に答える
3

多くのコメント (および回答) は、実装におけるリスクに焦点を当てています。しかし、疑問は残ります。

以下に直接示されているように、認識されたコードの欠点を修正しても、パフォーマンスに関して大きな変化はありません。

std::stackこれは、(A) 安全であり、(B) と同じ操作をサポートし、(C) にもバッファ スペースを予約するように変更された OP のコードstd::stackです。パフォーマンス:

#define _SECURE_SCL 0
#define _SCL_SECURE_NO_WARNINGS

#include <algorithm>        // std::swap
#include <iostream>
#include <vector>
#include <stack>
#include <stddef.h>         // ptrdiff_t
#include <type_traits>      // std::is_pod
using namespace std;

#undef UNICODE
#define UNICODE
#include <Windows.h>

typedef ptrdiff_t   Size;
typedef Size        Index;

template< class Type, class Container >
void reserve( Size const newBufSize, std::stack< Type, Container >& st )
{
    struct Access: std::stack< Type, Container >
    {
        static Container& container( std::stack< Type, Container >& st )
        {
            return st.*&Access::c;
        }
    };

    Access::container( st ).reserve( newBufSize );
}

class HighResolutionTimer
{
public:
    HighResolutionTimer();
    double GetFrequency() const;
    void Start() ;
    double Stop();
    double GetTime() const;

private:
    LARGE_INTEGER start;
    LARGE_INTEGER stop;
    double frequency;
};

HighResolutionTimer::HighResolutionTimer()
{
    frequency = GetFrequency();
}

double HighResolutionTimer::GetFrequency() const
{
    LARGE_INTEGER proc_freq;
    if (!::QueryPerformanceFrequency(&proc_freq))
        return -1;
    return static_cast< double >( proc_freq.QuadPart );
}

void HighResolutionTimer::Start()
{
    DWORD_PTR oldmask = ::SetThreadAffinityMask(::GetCurrentThread(), 0);
    ::QueryPerformanceCounter(&start);
    ::SetThreadAffinityMask(::GetCurrentThread(), oldmask);
}

double HighResolutionTimer::Stop()
{
    DWORD_PTR oldmask = ::SetThreadAffinityMask(::GetCurrentThread(), 0);
    ::QueryPerformanceCounter(&stop);
    ::SetThreadAffinityMask(::GetCurrentThread(), oldmask);
    return ((stop.QuadPart - start.QuadPart) / frequency);
} 

double HighResolutionTimer::GetTime() const
{
    LARGE_INTEGER time;
    ::QueryPerformanceCounter(&time);
    return time.QuadPart / frequency;
}

template< class Type, bool elemTypeIsPOD = !!std::is_pod< Type >::value >
class FastStack;

template< class Type >
class FastStack< Type, true >
{
private:
    Type*   st_;
    Index   lastIndex_;
    Size    capacity_;

public:
    Size const size() const { return lastIndex_ + 1; }
    Size const capacity() const { return capacity_; }

    void reserve( Size const newCapacity )
    {
        if( newCapacity > capacity_ )
        {
            FastStack< Type >( *this, newCapacity ).swapWith( *this );
        }
    }

    void push( Type const& x )
    {
        if( size() == capacity() )
        {
            reserve( 2*capacity() );
        }
        st_[++lastIndex_] = x;
    }

    void pop()
    {
        --lastIndex_;
    }

    Type top() const
    {
        return st_[lastIndex_];
    }

    void swapWith( FastStack& other ) throw()
    {
        using std::swap;
        swap( st_, other.st_ );
        swap( lastIndex_, other.lastIndex_ );
        swap( capacity_, other.capacity_ );
    }

    void operator=( FastStack other )
    {
        other.swapWith( *this );
    }

    ~FastStack()
    {
        delete[] st_;
    }

    FastStack( Size const aCapacity = 0 )
        : st_( new Type[aCapacity] )
        , capacity_( aCapacity )
    {
        lastIndex_ = -1;
    }

    FastStack( FastStack const& other, int const newBufSize = -1 )
    {
        capacity_ = (newBufSize < other.size()? other.size(): newBufSize);
        st_ = new Type[capacity_];
        lastIndex_ = other.lastIndex_;
        copy( other.st_, other.st_ + other.size(), st_ );   // Can't throw for POD.
    }
};

template< class Type >
void reserve( Size const newCapacity, FastStack< Type >& st )
{
    st.reserve( newCapacity );
}

template< class StackType >
void test( char const* const description )
{
    for( int it = 0; it < 4; ++it )
    {
        StackType st;
        reserve( 200, st );

        // after this two loops, st's capacity will be 141 so there will be no more reallocating
        for( int i = 0; i < 100; ++i ) { st.push( i ); }
        for( int i = 0; i < 100; ++i ) { st.pop(); }

        // when you uncomment this line, std::stack performance will magically rise about 18%
        // std::vector<int> magicVector(10);

        HighResolutionTimer timer;
        timer.Start();

        for( Index i = 0; i < 1000000000; ++i )
        {
            st.push( i );
            (void) st.top();
            if( i % 100 == 0 && i != 0 )
            {
                for( int j = 0; j < 100; ++j ) { st.pop(); }
            }
        }

        double const totalTime = timer.Stop();
        wcout << description << ": "  << totalTime << endl;
    }
}

int main()
{
    typedef stack< Index, vector< Index > > SStack;
    typedef FastStack< Index >              FStack;

    test< SStack >( "std::stack" );
    test< FStack >( "FastStack" );

    cout << "Done";
}

この糖蜜のように遅い Samsung RC530 ラップトップでの結果:

[D:\dev\test\so\12704314]
>
標準::スタック: 3.21319
標準::スタック: 3.16456
標準::スタック: 3.23298
標準::スタック: 3.20854
ファストスタック: 1.97636
ファストスタック: 1.97958
ファストスタック: 2.12977
ファストスタック: 2.13507
終わり
[D:\dev\test\so\12704314]
> _

Visual C++ の場合も同様です。

std::vector::push_back次に、によって呼び出されるの典型的な実装を見てみましょうstd::stack<T, std::vector<T>>::push(ちなみに、このインデント スタイルを使用したことのあるプログラマは 3 人しか知りません。つまり、PJP、Petzold、および私自身です。1998 年かそこらから今では、それは恐ろしいことだと思います! ):

void push_back(const value_type& _Val)
    {   // insert element at end
    if (_Inside(_STD addressof(_Val)))
        {   // push back an element
        size_type _Idx = _STD addressof(_Val) - this->_Myfirst;
        if (this->_Mylast == this->_Myend)
            _Reserve(1);
        _Orphan_range(this->_Mylast, this->_Mylast);
        this->_Getal().construct(this->_Mylast,
            this->_Myfirst[_Idx]);
        ++this->_Mylast;
        }
    else
        {   // push back a non-element
        if (this->_Mylast == this->_Myend)
            _Reserve(1);
        _Orphan_range(this->_Mylast, this->_Mylast);
        this->_Getal().construct(this->_Mylast,
            _Val);
        ++this->_Mylast;
        }
    }

測定された非効率性は、少なくとも部分的にはそこで行われているすべてのものにあると思われます。おそらく、自動的に生成された安全性チェックの問題でもあります。

デバッグビルドの場合、std::stackパフォーマンスが非常に悪いため、結果を待つことをあきらめました。


編集: 以下の Xeo のコメントに続いpushて、別の関数としてそれを除外することにより、バッファーの再割り当ての場合に「セルフプッシュ」をチェックするように更新しました。

void push( Type const& x )
{
    if( size() == capacity() )
    {
        reserveAndPush( x );
    }
    st_[++lastIndex_] = x;
}

不思議なことに、このテストで呼び出されることreserveAndPushはありませんが、コード サイズがキャッシュに収まらないため、パフォーマンスに影響しますか?

[D:\dev\test\so\12704314]
>
標準::スタック: 3.21623
標準::スタック: 3.30501
標準::スタック: 3.24337
標準::スタック: 3.27711
ファストスタック: 2.52791
ファストスタック: 2.44621
ファストスタック: 2.44759
ファストスタック: 2.47287
終わり
[D:\dev\test\so\12704314]
> _


EDIT 2 : DeadMG は、コードにバグがあるに違いないことを示しました。問題は の欠落returnと、新しいサイズを計算する式 (ゼロの 2 倍はまだゼロ) にあると思います。彼はまた、私が見せるのを忘れたことを指摘しましたreserveAndPush。次のようにする必要があります。

void reserveAndPush( Type const& x )
{
    Type const xVal = x;
    reserve( capacity_ == 0? 1 : 2*capacity_ );
    push( xVal );
}

void push( Type const& x )
{
    if( size() == capacity() )
    {
        return reserveAndPush( x );    // <-- The crucial "return".
    }
    st_[++lastIndex_] = x;
}
于 2012-10-03T12:30:16.670 に答える