SIGSEGV при попытке решить большие лабиринты с помощью A*

Я реализую алгоритм поиска пути 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;
}
Stack overflow in thread #1: can't grow stack to 0x1ffe801000 мне кажется довольно ясным. Вы этого не заметили или не поняли?
john 08.08.2024 16:20

Создайте сборку с отладочной информацией (добавьте параметр -g при сборке), и мы надеемся, что Valgrind сможет предоставить вам дополнительную информацию.

Some programmer dude 08.08.2024 16:21

Хотя теория переполнения стека требует рекурсивного кода, а это не совсем так, судя по тому, что было опубликовано.

john 08.08.2024 16:23

Кроме того, никогда не вызывайте top или pop на std::queue (или std::stack), не убедившись, что он не пуст.

Some programmer dude 08.08.2024 16:24

@john Да, я этого не понимаю. Я все еще новичок, поэтому надеялся, что кто-нибудь мне здесь поможет.

hubenhau 08.08.2024 16:24

@hubenhau Ну, я не уверен, есть ли у вас переполнение стека или нет. Являются ли какие-либо из ваших функций рекурсивными?

john 08.08.2024 16:25

@john Мои функции - нет, но деконструктор Node может вызываться рекурсивно, когда вызывается один ~Node(), а smart_ptr освобождает родителя и так далее...

hubenhau 08.08.2024 16:28

@hubenhau Не заметил этого, я бы назвал это дымящимся пистолетом.

john 08.08.2024 16:30
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
5
8
75
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

В вашем Nodestd::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);

Мне нужно, чтобы родитель отследил оптимальный путь через лабиринт после его решения. Пройдите от последнего через всех родителей обратно к началу, чтобы найти путь.

hubenhau 08.08.2024 16:31

Ммм, об этом не упоминалось. Но в любом случае вам не нужен указатель на Node, просто сохраните информацию о координатах родителей, идентификаторе родителя или создайте коллекцию (например, std::vector>), в которой вы будете хранить свой путь на лету.

pptaszni 08.08.2024 16:33

Да, я уже реализовал другое решение, которое хранит все узлы в векторе и отслеживает родительский_ид узлов, но я думал, что smart_pointers потребует меньше памяти, поскольку им не нужно хранить все узлы и они могут автоматически освобождать узлы. это определенно не будет частью конечного пути

hubenhau 08.08.2024 16:36

Это скорее проблема дизайна, а не указателей, требующих больше или меньше памяти. Подход с сущностями, связанными друг с другом, сам по себе хорош, но иметь узел, который также ведет себя как список, проблематично, например. потому что это запускает цепную реакцию при уничтожении одного узла. Обычно у вас есть класс Tree или List, который управляет Nodes, связывает их вместе и решает, когда их уничтожить.

pptaszni 08.08.2024 16:41

Хорошо, было бы неплохо написать собственный класс, который бы делал это, вместо того, чтобы полагаться на общие указатели? Кроме того, я до сих пор не уверен, означает ли все это, что мой код будет работать корректно, скажем, без аппаратных ограничений?

hubenhau 08.08.2024 16:47
Ответ принят как подходящий

Вы не определили 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 08.08.2024 16:33

@hubenhau нет. Это означает, что в вашем коде есть проблема, которая только и ждет, чтобы ее взорвать. Даже если ваш код не взрывается, проблема остается. Вместо того, чтобы надеяться, что оно не появится, вам следует это исправить.

463035818_is_not_an_ai 08.08.2024 16:39

@hubenhau: Строго говоря, да — это работает, но размер стека ограничивает размер лабиринта, который может решить ваша реализация. Возможно, еще много доступной памяти, но это не стековая память.

MSalters 08.08.2024 17:02

@MSalters Спасибо. Я только что попробовал с valgrind и --main-stacksize увеличился, и это работает. Думаю, мне придется найти для этого другой метод, но я рад, что, по крайней мере, логика в конце концов сработала.

hubenhau 08.08.2024 17:14

@MSalters Не совсем то, что вы предложили, но я хотел спросить, подойдет ли следующее исправление или нет. Я добавил деконструктор для узлов: ~Node() { while (parent.use_count() == 1) { parent = parent->parent; } } Если родительский.use_count() == 1, это означает, что родитель умрет, поскольку текущий узел имеет последний общий_ptr, указывающий на родителя. При установке родительского узла в родительский->родительский узел, по сути, умирает, не вызывая цепной реакции, поскольку наш узел теперь указывает на своего дедушку и так далее... Подходит ли этот подход или все еще проблематичен?

hubenhau 09.08.2024 14:41

Это ошибка, которую вы получаете:

Переполнение стека в потоке № 1: невозможно увеличить стек до 0x1ffe801000

Переполнение стека означает, что стек переполнен, поэтому давайте разберемся, что такое стек.

Стек — это структура данных типа LIFO, то есть элементы складываются друг на друга и всегда можно сначала выдвинуть элемент сверху.

В информатике есть раздел памяти, который называется стек вызовов.

В этом разделе вызываются вызовы подпрограмм. Итак, если вы вызываете функцию, которая вызывает функцию, которая вызывает функцию, то эти функции складываются в стек вызовов. Это работает до определенного предела, но если у вас слишком много вызовов функций без выполнения функций (и извлечения их из стека), тогда ваши незавершенные вызовы функций накапливаются и заполняют размер стека вызовов. На этом этапе, если есть еще один вызов функции, его пытаются поместить в стек вызовов, но это не удается из-за отсутствия достаточного места для хранения. Вот что произошло.

Как вы, наверное, уже поняли, вы можете увеличить размер стека вызовов, что устранит симптомы, но основная проблема все равно останется, просто предела стека будет труднее достичь.

Итак, решение состоит в том, чтобы убедиться, что ваш алгоритм будет итеративным, а не рекурсивным. Один из способов попытаться решить эту проблему — попытаться создать итеративный стек (на самом деле вы явно используете стек, а не полагаетесь на стек вызовов), и если вы хотите освободить Node, затем поместите все его ссылки в свой стек и установите их. обнулить, чтобы ничего подозрительного не произошло, освободите Node, на который теперь больше нет ссылок для запуска деструктора, извлеките свой узел из вершины стека, ведите себя аналогично, то есть поместите его ссылки в стек, удалите ссылки этого объекта, затем освободите его, затем продолжите работу с вершиной стека и так далее, пока не останется указателя, который нужно освободить.

Спасибо за ответ. Не уверен, что я действительно понимаю, как написать предложенное вами решение, но попробую.

hubenhau 08.08.2024 17:22

Теперь мне удалось избавиться от ошибки, вызвав эту функцию на каждом узле, прежде чем он умрет: void free(Node &node) { while(node.parent.use_count() == 1) { auto parent = node.parent; node.parent = nullptr; node = *parent; } } Можно ли сделать это так?

hubenhau 08.08.2024 17:39

@hubenhau Я предлагал использовать стек. См. programiz.com/cpp-programming/stack. Вы должны активно поместить все ссылки, которые имеет ваш объект, в стек, установить для них все нулевые значения внутри вашего объекта, удалить свой объект с помощью теперь безопасного деструктора, затем продолжить извлечение следующего элемента из стека, делать то же самое, пока все ссылки были успешно освобождены.

Lajos Arpad 08.08.2024 18:22

Другие вопросы по теме