-3

最近我试图做一些性能基准测试,比较std::stack<int, std::vector<int>>我自己简单的堆栈实现(使用预分配内存)。现在我遇到了一些奇怪的行为。

我想问的第一件事是堆栈基准代码中的这一行:

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

当我取消注释这条线时,性能提高了大约 17%(基准时间从 6.5 秒下降到 5.4 秒)。但是该行应该对程序的其余部分没有影响,因为它不会修改任何其他成员。此外,它是 int 的向量还是 double 的向量都没有关系......

我想问的第二件事是我的堆栈实现和std::stack. 有人告诉我这std::stack应该和我的堆栈一样快,但结果显示我的“FastStack”快两倍。

结果(未注释的性能增加线):
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,默认时钟(一个线程 3.6 GHz)。

我将所有代码放在一个文件中,以便任何人都可以轻松测试它。

#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,并且您resize显然已损坏,因为它不会复制以前的数据并且它不是异常安全的,并且您的推送不是异常安全的并且您调用了太多副本并且getAndRemove的不是异常安全的,并且您不会破坏弹出的元素也不会正确构造新元素,只分配它们并且您在创建时不必要地默认构造,并且可能还有更多我还没有找到。

基本上,您的课程在任何可以想象的方面都极其不安全,会T立即破坏用户的数据,调用所有错误的函数,并且在任何地方抛出异常的那一刻都会在角落里哭泣。

这是一大堆坏事,而且它“更快”的事实std::stack完全无关紧要,因为你所证明的是,如果你不必满足要求,你可以随心所欲地去,我们都已经知道了。

从根本上说,正如 sbi 所说,您显然不了解 的语义std::stack,也不了解异常安全等重要的 C++ 方面,您的代码无法正常工作的方式是使其执行速度更快的原因。你还有很长的路要走,我的朋友。

于 2012-10-03T10:37:56.423 回答
4

std::stackusing相反std::vector,您的堆栈在空间不足时不会重新分配,而只会炸毁地球。然而,分配会极大地消耗性能,因此跳过它肯定会提高性能。

但是,在您的位置,我会抓住一个漂浮在网络上的成熟static_vector实现,并将其放入. 这样,您就可以跳过所有需要性能的动态内存处理,但是您有一个有效的堆栈实现,其中包含一个用于在下面进行内存处理的容器,这可能比您想出的要好得多。std::stackstd::vector

于 2012-10-03T12:57:42.370 回答
3

许多评论(甚至答案)都集中在您的实施中的风险上。然而问题仍然存在。

正如下面直接展示的那样,纠正感知到的代码缺陷不会改变任何重要的性能。

这是 OP 的代码修改为 (A) 安全,(B) 支持与 相同的操作std::stack,以及 (C) 也为 保留缓冲区空间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";
}

这台慢如糖蜜的三星 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_backstd::stack<T, std::vector<T>>::push. ):

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]
> _


编辑 2: DeadMG 表明代码一定是错误的。我相信问题是缺失的return,加上计算新大小的表达式(两次零仍然为零)。他还指出我忘了展示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 回答