Я реализую алгоритм поиска пути A* на C++ для решения лабиринта, но сталкиваюсь с ошибкой сегментации (SIGSEGV), когда размер лабиринта велик (~ 10 000x10 000 или больше).
Ошибка возникает в функции
~__shared_count()
вshared_ptr_base.h
после вызоваnode = queue.top()
в строке 58 и после этого вызываются десятки деконструкторов~Node()
.
Обновлено: теперь ошибка действительно возникает в функции _Sp_counted_base<_S_atomic>::_M_release()
, хотя я не знаю, что я изменил.
Вот части моего кода, которые должны иметь значение. Я новичок в использовании общих указателей, поэтому не совсем понимаю, в чем проблема и как ее исправить.
main.cpp
#include <iostream>
#include <queue>
#include <random>
#include <vector>
#include <algorithm>
#include <stack>
#include <fstream>
#include "Node.hpp"
constexpr uint16_t maze_size = 10000;
void solve(const uint16_t start_x, const uint16_t start_y, const uint16_t goal_x,
const uint16_t goal_y,
const std::vector<std::vector<std::pair<bool, bool> > > &walls) {
std::vector<std::vector<bool> > visited(maze_size, std::vector<bool>(maze_size, false));
// Min heap for our queue
std::priority_queue<Node, std::vector<Node>, std::greater<> > queue;
Node node(start_x, start_y, goal_x, goal_y);
visited[node.x][node.y] = true;
while (!node.solved(goal_x, goal_y)) {
// Add all valid neighbors to the queue
{
const auto x = node.x;
const auto y = node.y;
const auto ptr = std::make_shared<Node>(node);
// Right
if (x < maze_size - 1 && !walls[x][y].first && !visited[x + 1][y]) {
queue.emplace(x + 1, y, goal_x, goal_y, ptr);
visited[x + 1][y] = true;
}
// Left
if (x > 0 && !walls[x - 1][y].first && !visited[x - 1][y]) {
queue.emplace(x - 1, y, goal_x, goal_y, ptr);
visited[x - 1][y] = true;
}
// Down
if (y < maze_size - 1 && !walls[x][y].second && !visited[x][y + 1]) {
queue.emplace(x, y + 1, goal_x, goal_y, ptr);
visited[x][y + 1] = true;
}
// Up
if (y > 0 && !walls[x][y - 1].second && !visited[x][y - 1]) {
queue.emplace(x, y - 1, goal_x, goal_y, ptr);
visited[x][y - 1] = true;
}
}
node = queue.top();
queue.pop();
}
}
Узел.hpp
#ifndef NODE_HPP
#define NODE_HPP
#include <cstdint>
#include <memory>
class Node {
public:
uint16_t x, y;
uint32_t steps;
uint32_t value;
std::shared_ptr<Node> parent;
Node(const uint16_t x, const uint16_t y, const uint16_t goal_x, const uint16_t goal_y,
const std::shared_ptr<Node> &parent = nullptr) : x(x), y(y), parent(parent) {
steps = parent ? parent->steps + 1 : 0;
value = abs(static_cast<int32_t>(goal_x) - static_cast<int32_t>(x)) +
abs(static_cast<int32_t>(goal_y) - static_cast<int32_t>(y)) +
steps;
}
[[nodiscard]] bool solved(const uint16_t goal_x, const uint16_t goal_y) const {
return x == goal_x && y == goal_y;
}
// Overload the > operator for the priority queue
bool operator>(const Node &rhs) const {
if (value == rhs.value) {
return steps < rhs.steps;
}
return value > rhs.value;
}
};
#endif
Выход Валгринда
valgrind --leak-check=full ./Tests
==5385== Memcheck, a memory error detector
==5385== Copyright (C) 2002-2022, and GNU GPL'd, by Julian Seward et al.
==5385== Using Valgrind-3.22.0 and LibVEX; rerun with -h for copyright info
==5385== Command: ./Tests
==5385==
Maze loaded10000
==5385== Stack overflow in thread #1: can't grow stack to 0x1ffe801000
==5385==
==5385== Process terminating with default action of signal 11 (SIGSEGV)
==5385== Access not within mapped region at address 0x1FFE801FF8
==5385== Stack overflow in thread #1: can't grow stack to 0x1ffe801000
==5385== at 0x116230: std::_Sp_counted_ptr_inplace<Node, std::allocator<void>, (__gnu_cxx::_Lock_policy)2>::_M_dispose() (shared_ptr_base.h:613)
==5385== If you believe this happened as a result of a stack
==5385== overflow in your program's main thread (unlikely but
==5385== possible), you can try to increase the size of the
==5385== main thread stack using the --main-stacksize= flag.
==5385== The main thread stack size used in this run was 8388608.
==5385==
==5385== HEAP SUMMARY:
==5385== in use at exit: 239,026,864 bytes in 556,422 blocks
==5385== total heap usage: 3,380,473 allocs, 2,824,051 frees, 374,594,816 bytes allocated
==5385==
==5385== LEAK SUMMARY:
==5385== definitely lost: 0 bytes in 0 blocks
==5385== indirectly lost: 0 bytes in 0 blocks
==5385== possibly lost: 0 bytes in 0 blocks
==5385== still reachable: 239,026,864 bytes in 556,422 blocks
==5385== suppressed: 0 bytes in 0 blocks
==5385== Reachable blocks (those to which a pointer was found) are not shown.
==5385== To see them, rerun with: --leak-check=full --show-leak-kinds=all
==5385==
==5385== For lists of detected and suppressed errors, rerun with: -s
==5385== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
Segmentation fault (core dumped)
Вы можете попытаться воспроизвести ошибку, используя следующую функцию для загрузки этого файла лабиринта. Извините за размер. Вы можете загрузить его с помощью этой функции:
std::vector<std::vector<std::pair<bool, bool> > > readVectorFromFile(const std::string &filename) {
std::vector<std::vector<std::pair<bool, bool> > > vec;
std::ifstream inFile(filename, std::ios::binary);
if (!inFile) {
std::cerr << "Error opening file for reading: " << filename << std::endl;
return vec;
}
// Read the size of the outer vector
size_t outerSize;
inFile.read(reinterpret_cast<char *>(&outerSize), sizeof(outerSize));
vec.resize(outerSize);
for (auto &innerVec: vec) {
// Read the size of each inner vector
size_t innerSize;
inFile.read(reinterpret_cast<char *>(&innerSize), sizeof(innerSize));
innerVec.resize(innerSize);
// Read each pair in the inner vector
for (auto &pair: innerVec) {
inFile.read(reinterpret_cast<char *>(&pair.first), sizeof(bool));
inFile.read(reinterpret_cast<char *>(&pair.second), sizeof(bool));
}
}
inFile.close();
return vec;
}
Основное будет выглядеть примерно так:
int main() {
auto walls = readVectorFromFile("maze.dat");
std::cout << "Maze loaded" << std::endl;
solve(0, 0, maze_size - 1, maze_size - 1, walls);
return 0;
}
Создайте сборку с отладочной информацией (добавьте параметр -g
при сборке), и мы надеемся, что Valgrind сможет предоставить вам дополнительную информацию.
Хотя теория переполнения стека требует рекурсивного кода, а это не совсем так, судя по тому, что было опубликовано.
Кроме того, никогда не вызывайте top
или pop
на std::queue
(или std::stack
), не убедившись, что он не пуст.
@john Да, я этого не понимаю. Я все еще новичок, поэтому надеялся, что кто-нибудь мне здесь поможет.
@hubenhau Ну, я не уверен, есть ли у вас переполнение стека или нет. Являются ли какие-либо из ваших функций рекурсивными?
@john Мои функции - нет, но деконструктор Node может вызываться рекурсивно, когда вызывается один ~Node(), а smart_ptr освобождает родителя и так далее...
@hubenhau Не заметил этого, я бы назвал это дымящимся пистолетом.
В вашем Node
std::shared_ptr<Node> parent;
на самом деле не нужен и склонен к циклическим зависимостям, что, видимо, и произошло в вашем лабиринте (хотя мне лень это подтверждать). Удалите shared_ptr
и просто предоставьте информацию о родительском узле steps
:
class Node {
public:
uint16_t x, y;
uint32_t steps;
uint32_t value;
Node(uint16_t x, uint16_t y, uint16_t goal_x, uint16_t goal_y, int parentSteps = -1) : x(x), y(y) {
steps = parentSteps + 1;
value = abs(static_cast<int32_t>(goal_x) - static_cast<int32_t>(x)) +
abs(static_cast<int32_t>(goal_y) - static_cast<int32_t>(y)) + steps;
}
[[nodiscard]] bool solved(const uint16_t goal_x, const uint16_t goal_y) const { return x == goal_x && y == goal_y; }
// Overload the < operator for the priority queue
bool operator>(const Node &rhs) const {
if (value == rhs.value) {
return steps < rhs.steps;
}
return value > rhs.value;
}
};
Затем вы можете создавать новые узлы следующим образом:
queue.emplace(x + 1, y, goal_x, goal_y, node.steps);
Мне нужно, чтобы родитель отследил оптимальный путь через лабиринт после его решения. Пройдите от последнего через всех родителей обратно к началу, чтобы найти путь.
Ммм, об этом не упоминалось. Но в любом случае вам не нужен указатель на Node
, просто сохраните информацию о координатах родителей, идентификаторе родителя или создайте коллекцию (например, std::vector>
), в которой вы будете хранить свой путь на лету.
Да, я уже реализовал другое решение, которое хранит все узлы в векторе и отслеживает родительский_ид узлов, но я думал, что smart_pointers потребует меньше памяти, поскольку им не нужно хранить все узлы и они могут автоматически освобождать узлы. это определенно не будет частью конечного пути
Это скорее проблема дизайна, а не указателей, требующих больше или меньше памяти. Подход с сущностями, связанными друг с другом, сам по себе хорош, но иметь узел, который также ведет себя как список, проблематично, например. потому что это запускает цепную реакцию при уничтожении одного узла. Обычно у вас есть класс Tree
или List
, который управляет Nodes
, связывает их вместе и решает, когда их уничтожить.
Хорошо, было бы неплохо написать собственный класс, который бы делал это, вместо того, чтобы полагаться на общие указатели? Кроме того, я до сих пор не уверен, означает ли все это, что мой код будет работать корректно, скажем, без аппаратных ограничений?
Вы не определили Node::~Node()
явно, но компилятор сделал это за вас. Там оно звонит std::shared_ptr<Node>::~shared_ptr
. В свою очередь это вызывает Node::~Node
.
Это рекурсивный вызов. В зависимости от вашего лабиринта и запроса это может привести к переполнению стека вызовов.
Более простое решение — позволить solve()
управлять владением всеми узлами; у каждого узла может быть просто невладеющий Node* parent
. Это владение может быть таким же простым, как std::deque<Node>
(но не std::vector<Node>
, потому что это перераспределяет и ломает Node* parent
)
[редактировать]
Использование интеллектуальных указателей в объектах узлов не обязательно является плохой идеей. В широких деревьях, где у каждого родителя было по 10 дочерних элементов, вы столкнетесь с проблемами памяти на глубине около 9 уровней (один миллиард узлов), но стек легко справится с таким уровнем вложенности. Даже сбалансированное двоичное дерево с миллиардом узлов — это не так уж и плохо (глубина 32). В этих случаях дерева каждый узел является единственным владельцем нескольких дочерних элементов. Это можно реализовать с помощью std::vector<std::unique_ptr>> children
.
Спасибо. Означает ли это, что код в целом работает, но стек для него слишком мал? Извините, я действительно не знаю, что это значит
@hubenhau нет. Это означает, что в вашем коде есть проблема, которая только и ждет, чтобы ее взорвать. Даже если ваш код не взрывается, проблема остается. Вместо того, чтобы надеяться, что оно не появится, вам следует это исправить.
@hubenhau: Строго говоря, да — это работает, но размер стека ограничивает размер лабиринта, который может решить ваша реализация. Возможно, еще много доступной памяти, но это не стековая память.
@MSalters Спасибо. Я только что попробовал с valgrind и --main-stacksize увеличился, и это работает. Думаю, мне придется найти для этого другой метод, но я рад, что, по крайней мере, логика в конце концов сработала.
@MSalters Не совсем то, что вы предложили, но я хотел спросить, подойдет ли следующее исправление или нет. Я добавил деконструктор для узлов: ~Node() { while (parent.use_count() == 1) { parent = parent->parent; } }
Если родительский.use_count() == 1, это означает, что родитель умрет, поскольку текущий узел имеет последний общий_ptr, указывающий на родителя. При установке родительского узла в родительский->родительский узел, по сути, умирает, не вызывая цепной реакции, поскольку наш узел теперь указывает на своего дедушку и так далее... Подходит ли этот подход или все еще проблематичен?
Это ошибка, которую вы получаете:
Переполнение стека в потоке № 1: невозможно увеличить стек до 0x1ffe801000
Переполнение стека означает, что стек переполнен, поэтому давайте разберемся, что такое стек.
Стек — это структура данных типа LIFO, то есть элементы складываются друг на друга и всегда можно сначала выдвинуть элемент сверху.
В информатике есть раздел памяти, который называется стек вызовов.
В этом разделе вызываются вызовы подпрограмм. Итак, если вы вызываете функцию, которая вызывает функцию, которая вызывает функцию, то эти функции складываются в стек вызовов. Это работает до определенного предела, но если у вас слишком много вызовов функций без выполнения функций (и извлечения их из стека), тогда ваши незавершенные вызовы функций накапливаются и заполняют размер стека вызовов. На этом этапе, если есть еще один вызов функции, его пытаются поместить в стек вызовов, но это не удается из-за отсутствия достаточного места для хранения. Вот что произошло.
Как вы, наверное, уже поняли, вы можете увеличить размер стека вызовов, что устранит симптомы, но основная проблема все равно останется, просто предела стека будет труднее достичь.
Итак, решение состоит в том, чтобы убедиться, что ваш алгоритм будет итеративным, а не рекурсивным. Один из способов попытаться решить эту проблему — попытаться создать итеративный стек (на самом деле вы явно используете стек, а не полагаетесь на стек вызовов), и если вы хотите освободить Node
, затем поместите все его ссылки в свой стек и установите их. обнулить, чтобы ничего подозрительного не произошло, освободите Node
, на который теперь больше нет ссылок для запуска деструктора, извлеките свой узел из вершины стека, ведите себя аналогично, то есть поместите его ссылки в стек, удалите ссылки этого объекта, затем освободите его, затем продолжите работу с вершиной стека и так далее, пока не останется указателя, который нужно освободить.
Спасибо за ответ. Не уверен, что я действительно понимаю, как написать предложенное вами решение, но попробую.
Теперь мне удалось избавиться от ошибки, вызвав эту функцию на каждом узле, прежде чем он умрет: void free(Node &node) { while(node.parent.use_count() == 1) { auto parent = node.parent; node.parent = nullptr; node = *parent; } }
Можно ли сделать это так?
@hubenhau Я предлагал использовать стек. См. programiz.com/cpp-programming/stack. Вы должны активно поместить все ссылки, которые имеет ваш объект, в стек, установить для них все нулевые значения внутри вашего объекта, удалить свой объект с помощью теперь безопасного деструктора, затем продолжить извлечение следующего элемента из стека, делать то же самое, пока все ссылки были успешно освобождены.
Stack overflow in thread #1: can't grow stack to 0x1ffe801000
мне кажется довольно ясным. Вы этого не заметили или не поняли?