【问题标题】:vector is faster than list in list fastest operations在 list 最快的操作中,vector 比 list 快
【发布时间】:2017-12-17 13:56:41
【问题描述】:

我为我的游戏编写了一个简单的 A* 算法:

导航.hpp

#pragma once

#include <SFML\Graphics.hpp>
#include <algorithm>
#include <iostream>

#include <vector>
#include <list>
#include <set>

using namespace std;

class Field;

//Comparer using by set to allow priority
struct FieldComparer
{
    bool operator()(const Field *, const Field *) const;
};

using FieldSet = set<Field*, FieldComparer>;
using FieldContainer = vector<Field*>; ///////////////////////////faster than list ?!

//Contains info about field, buildings builded on it and type of terrain
class Field
{
private:
    sf::Vector2i mapPosition{ 0, 0 };

    unsigned hCost{ 0 }; //to goal
    unsigned gCost{ 0 }; //to start

    Field *  parent;

    bool     isWalkable { true };
    bool     isPathPart { false };
public:
    void SetParent(Field&);
    Field * GetParent() const;

    unsigned GetFCost() const; //sum of hCost and gCost
    unsigned GetHCost() const;
    unsigned GetGCost() const;

    bool     IsWalkable() const;
    bool     IsPathPart() const;

    void SetHCost(unsigned);
    void SetGCost(unsigned);

    void SetWalkable(bool);
    void SetAsPartOfPath(bool);

    sf::Vector2i GetMapPosition() const;

    //compares positions
    bool operator == (const Field& other);

    Field(sf::Vector2i mapPosition, bool isWalkable) 
        : mapPosition(mapPosition), isWalkable(isWalkable) {}
};

//Contains fields and describes them
class Map
{
private:
    sf::Vector2u mapSize;
    Field *** fields; //two dimensional array of fields gives the fastest access
public:

    sf::Vector2u GetMapSize() const;
    Field *** GetFields();

    Map(sf::Vector2u);
    Map() {}
};

//Searching patch after giving a specified map
class PathFinder
{
private:
    //Calculate score between two fields
    unsigned CalcScore(Field&, Field&) const;

    //Get neighbours of field in specified map
    FieldContainer GetNeighbours(Field&, Map&) const;
public:

    //Find path that have the lowest cost, from a to b in map
    FieldContainer FindPath(Map&, Field&, Field&);

    //Reconstruct path using pointers to parent
    FieldContainer ReconstructPath(Field*, Field*) const;
};

导航.cpp

#include "Navigation.hpp"

#pragma region Field

    void Field::SetParent(Field & parent) { this->parent = &parent; }
    Field * Field::GetParent() const { return parent; }

    unsigned Field::GetFCost() const { return hCost + gCost; }
    unsigned Field::GetHCost() const { return hCost; }
    unsigned Field::GetGCost() const { return gCost; }

    bool Field::IsWalkable()   const { return isWalkable; }
    bool Field::IsPathPart()   const { return isPathPart; }

    void Field::SetHCost(unsigned value) { hCost = value; }
    void Field::SetGCost(unsigned value) { gCost = value; }

    void Field::SetWalkable(bool isWalkable)     { this->isWalkable = isWalkable; }
    void Field::SetAsPartOfPath(bool isPathPart) { this->isPathPart = isPathPart; }

    sf::Vector2i Field::GetMapPosition() const { return mapPosition; }
    bool Field::operator == (const Field& other)
    {
        return this->mapPosition == other.GetMapPosition();
    }

#pragma endregion Field

#pragma region Map

    sf::Vector2u Map::GetMapSize() const { return mapSize; }
    Field *** Map::GetFields() { return fields; }
    Map::Map(sf::Vector2u mapSize) : mapSize(mapSize)
    {
        //initialize map
        fields = new Field**[mapSize.x];

        //initialize all fields
        for (unsigned x = 0; x < mapSize.x; x++)
        {
            fields[x] = new Field*[mapSize.y];

            for (unsigned y = 0; y < mapSize.y; y++)
            {
                fields[x][y] = new Field({static_cast<int>(x), static_cast<int>(y)}, 
                { 
                    (!(y == 3 && x >= 1) || (x == 5 && y < 4))              
                });
            }
        }
    }

#pragma endregion Map

#pragma region PathFinder

    bool FieldComparer::operator()(const Field * l, const Field * r) const
    {
        return l->GetFCost() <  r->GetFCost() ||   //another field has smaller fcost
               l->GetFCost() == r->GetFCost() &&   //or fcost equals, and checked field is nearer to goal than current field
               l->GetHCost() <  r->GetHCost();

    }

    unsigned PathFinder::CalcScore(Field & a, Field & b) const
    {
        sf::Vector2u dst
        {
            static_cast<unsigned>(abs(b.GetMapPosition().x - a.GetMapPosition().x)),
            static_cast<unsigned>(abs(b.GetMapPosition().y - a.GetMapPosition().y))
        };

        return (dst.x > dst.y ? 14 * dst.y + 10 * (dst.x - dst.y) :
                                14 * dst.x + 10 * (dst.y - dst.x));
    }

    FieldContainer PathFinder::GetNeighbours(Field & f, Map & m) const
    {
        FieldContainer neighbours{};

        //cout << "checking neighbours for field: { " << f.GetMapPosition().x << ", " << f.GetMapPosition().y << " }\n";
        for (int x = -1; x <= 1; x++)
        {
            for (int y = -1; y <= 1; y++)
            {
                int xPos = f.GetMapPosition().x + x;
                int yPos = f.GetMapPosition().y + y;

                if (x == 0 && y == 0) //dont check the same field
                    continue;

                //check that field is in the map
                bool isInTheMap = (xPos >= 0 && yPos >= 0 && xPos < m.GetMapSize().x && yPos < m.GetMapSize().y);

                if (isInTheMap)
                {
                    neighbours.push_back(m.GetFields()[xPos][yPos]);
                }
            }
        }

        return neighbours;
    }

    FieldContainer PathFinder::FindPath(Map& map, Field& a, Field& b)
    {
        FieldSet open = {};   //not expanded fields
        FieldSet closed = {}; //expanded fields

        a.SetHCost(CalcScore(a, b)); //calculate h cost for start field, gcost equals 0
        open.insert(&a);             //add start field to open vector

        while (!open.empty()) //while we have unexpanded fields in open set
        {
            Field * current = *open.begin(); //set current field

            //if current checked field is our goal field
            if (*current == b)
            {
                return
                    ReconstructPath(&a, current); //return reversed path
            }

            closed.insert(current); //end of checking current field, add it to closed vector...
            open.erase(open.find(current)); //set solution

            //get neighbours of current field
            for (auto f : GetNeighbours(*current, map))
            {
                //continue if f is unavailable
                if (closed.find(f) != closed.end() || !f->IsWalkable())
                {
                    continue;
                }

                //calculate tentative g cost, based on current cost and direction changed
                unsigned tentativeGCost = current->GetGCost() + (current->GetMapPosition().x != f->GetMapPosition().x && current->GetMapPosition().y != f->GetMapPosition().y ? 14 : 10);

                bool fieldIsNotInOpenSet = open.find(f) == open.end();
                if (tentativeGCost < f->GetGCost() || fieldIsNotInOpenSet)
                {
                    f->SetGCost(tentativeGCost);
                    f->SetHCost(CalcScore(*f, b));
                    f->SetParent(*current);

                    if (fieldIsNotInOpenSet)
                    {
                        open.insert(f);
                    }
                }
            }
        }
        return {}; //no path anaviable
    }

    FieldContainer PathFinder::ReconstructPath(Field * a, Field * current) const
    {
        FieldContainer totalPath { current };

        while (!(current == a))
        {
            totalPath.push_back(current);
            current->SetAsPartOfPath(true);
            current = current->GetParent();
        }

        std::reverse(totalPath.begin(), totalPath.end()); //reverse the path
        return totalPath;
    }

#pragma endregion PathFinder

...我想知道为什么当我使用 std::list 而不是 std::vector 时,执行路径查找算法的时间等于或大于 std::vector 中的时间,因为只有 FieldContainer 的添加操作.

我使用 std::chrono 高分辨率计时器在循环中检查了 10 次执行时间:

#include "MapDrawer.h"
#include <iostream>

#include "Navigation.hpp"
//clock
#include <chrono>

typedef std::chrono::high_resolution_clock ChronoClock;

using namespace sf;

bool pathFound = false;

FieldContainer path;
Map gameMap;

const sf::Vector2f mapSize { 300, 300 };

void Test()
{
    for (int i = 0; i < 10; i++)
    {
        gameMap = { static_cast<sf::Vector2u>(mapSize) };
        PathFinder pathFinder{};

        auto t1 = ChronoClock::now();

        path = pathFinder.FindPath(gameMap, *gameMap.GetFields()[0][0], *gameMap.GetFields()[(int)(mapSize.x - 1)][(int)(mapSize.y - 1)]);

        auto t2 = ChronoClock::now();

        std::cout << "Delta: " << std::chrono::duration_cast<std::chrono::milliseconds>(t2 - t1).count() << " milliseconds\n";
    }
}


void MapDrawer::draw(RenderTarget &target, RenderStates states) const
{
    if (!pathFound)
    {
        Test();
        pathFound = true;
    }
    ////////////////
}

以毫秒为单位的结果(调试)

map size        container       time (average)
  50x50           list               13,8
  50x50          vector              12,4
 150x150          list               54,0
 150x150         vector              41,9
 300x300          list              109,9
 300x300         vector             100,8

以毫秒为单位的结果(RELEASE)

map size        container       time (average)
 500x500          list               9,3
 500x500         vector              7,4
1500x1500         list              23,9
1500x1500        vector             23,7

所以看起来向量比列表更快,但是为什么,因为列表在添加操作时应该更快?

顺便说一句。那个算法快吗?如果不是,那我可以改变什么来提高速度?

谢谢。

【问题讨论】:

  • 帮助阅读:isocpp.org/blog/2014/06/stroustrup-lists。 “我的 2012 年“走向本土”主题演讲。”从链接页面链接到一些非常好的了解细节。
  • 当然列表更慢:它必须将每个元素存储在非连续内存中。如果可能,您应该使用dequevector
  • Vectors 在尝试扩展时会出现性能问题,否则访问量为 O[1]。列表必须搜索列表的末尾(除非它有尾指针),分配一个新节点,然后将新节点附加到列表中。
  • 优化版本构建与否?
  • @RetiredNinja 哦,我忘记了发布模式,顺便说一句。 WOW 100ms -> 4ms 没想到释放模式这么厉害……

标签: algorithm performance list c++11 vector


【解决方案1】:

种情况下列表会比向量快。

显而易见的是在集合的开头插入了很多项目:

template <class T>
void build(T &t, int max) {
    for (int i = 0; i < max; i++)
        t.insert(t.begin(), i);
}

快速测试表明,像这样插入 100000 个整数对于 vector 需要数百毫秒,对于 list 需要 5 毫秒(尽管对于这种情况,双端队列是显而易见的选择——大约半微秒,它是比向量或列表快得多)。

为了使列表有意义,您需要在已经相当大的列表中间的某个位置插入 - 并且(重要的部分)您需要在同一点进行 许多 插入,所以你不会花很多时间遍历列表来找到在哪里进行插入。例如,如果我们这样做:

template <class T>
void build2(T &t, int max) {
    max = max / 3;

    for (int i = 0; i < max; i++)
        t.insert(t.begin(), i);

    auto pos = t.begin();

    for (int i = -max; i < 0; i++)
        t.insert(t.begin(), i);

    // Now `pos` refers to the middle of the collection, insert some there:    
    for (int i = 0; i < max; i++)
        t.insert(pos, i);
}

(除了次要:对于dequevector,代码必须略有不同——因为pos 可能被任何后续插入无效,它们的第三个循环看起来像:

for (int i = 0; i < max; i++)
    t.insert(t.begin() + max, i);

无论如何,我们终于有了list 战胜vectordeque 的比赛。对于这个任务,我得到了时间(注意:这不是相同数量的对象,所以时间不能与前面的比较):

  List: 500 us
 Deque: 3500 us
Vector: 6500 us

当然,这也不是关于该主题的最后(或唯一)词——很大程度上还取决于您要处理的物品有多大,因为大型物品(至少通常是这样)复制速度相当慢。

有趣的是:vector 总是在末尾扩展,但 deque 可以在末尾或开头扩展,因此即使不理想,它仍然明显比 vector 快。

哦,还有一点:编译器/库的时间也会有所不同。之前的时间是针对 gcc 的。使用 VC++,list 差不多,vector 快两倍,deque 慢很多

  List: 526 us
 Deque: 37478 us
Vector: 3657 us

(在与前面相同的系统上执行)。

【讨论】:

  • 在 MSVC 的 libstdcpp sucks balls 中实现 std::deque 所以这并不奇怪 :)
  • @Rerito:不幸的是,他未能以我认为足够详细的方式解释问题(尽管我承认,我也没有试图在这个答案中解释它——但主要是因为他一开始没有问过 deque)。
猜你喜欢
  • 1970-01-01
  • 2014-02-21
  • 2015-07-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多