-1

I wrote a simply A* algorithm for my game:

Navigation.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;
};

Navigation.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

...and I am wondering why when I use std::list instead of std::vector, time of execute path finding algorithm equals or is bigger than in std::vector, because there are only adding operations for FieldContainer.

I was checking execution times 10 times in a loop using std::chrono high resolution timer:

#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;
    }
    ////////////////
}

Results in ms (DEBUG)

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

Result in ms (RELEASE)

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

So it looks like the vector is more fast than list, but why, since list should be faster in adding operations?

Btw. is that algorithm fast? If no, then what I could change to increase the speed?

Thanks.

Michael
  • 449
  • 1
  • 6
  • 13
  • 3
    Helpful reading: https://isocpp.org/blog/2014/06/stroustrup-lists . The "My 2012 “Going Native” Keynote." linked from the linked page goes into some pretty good-to-know details. – user4581301 Jul 13 '17 at 00:21
  • Of course a list is slower: it has to store each element in non-contiguous memory. You should use a `deque`, or `vector` if possible. – Alex Huszagh Jul 13 '17 at 00:21
  • 3
    Vectors will have performance issues when trying to expand, otherwise access is O[1]. A list will have to search for the end of the list (unless it has a tail pointer), allocate a new node, then append the new node to the list. – Thomas Matthews Jul 13 '17 at 00:22
  • 2
    Optimized release build or not? – Retired Ninja Jul 13 '17 at 00:22
  • 1
    @RetiredNinja ohh I forgot about release mode, btw. WOW 100ms -> 4ms I didn't know that release mode is so powerful... – Michael Jul 13 '17 at 00:25

1 Answers1

3

There are cases where a list will be faster than a vector.

The obvious is inserting a lot of items at the beginning of the collection:

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

A quick test shows that inserting 100000 ints like this takes hundreds of milliseconds for vector and 5 ms for a list (though for this case, a deque is the obvious choice--at about half a microsecond, it's much faster than either vector or list).

For list to make sense, you need to insert somewhere in the middle of a list that's already fairly large--and (the important part) you need to make many insertions at that same point, so you don't spend a lot of time traversing the list to find where to do your insertions. For example, if we do like this:

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);
}

(Minor aside: for deque or vector the code has to be slightly different--since pos could be invalidated by any of the subsequent insertions, the third loop for them looks like:

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

So anyway, with this we finally have contest where list wins over either vector or deque. For this task I get times of (note: this isn't with the same number of objects, so the times aren't comparable to the preceding):

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

Of course, this isn't the final (or only) word on the subject either--a great deal also depends on things like how large of items you're dealing with, since large items are (at least usually) fairly slow to copy.

Interesting aside: vector always expands at the end, but deque can expand at either the end or the beginning, so even though it's not ideal, it's still noticeably faster for this task than a vector.

Oh, one other point: the timing can vary somewhat with the compiler/library as well. The immediately preceding times were for gcc. With VC++, list is about the same, vector is about twice as fast, and deque is much slower:

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

(Executed on the same system as the preceding).

Jerry Coffin
  • 476,176
  • 80
  • 629
  • 1,111
  • The implementation of `std::deque` in MSVC's libstdcpp [sucks balls](https://stackoverflow.com/a/16252347/1794345) so that's not such a surprise :) – Rerito Jul 13 '17 at 06:37
  • @Rerito: Unfortunately, he failed to explain the problem in what I'd consider sufficient detail (though I'll admit, I didn't try to explain it at all in this answer either--but mostly because he hadn't asked about deque in the first place). – Jerry Coffin Jul 13 '17 at 06:39