【问题标题】:C++ error: terminate called after throwing an instance of 'std::bad_alloc' when running a function twiceC++ 错误:在运行函数两次时抛出“std::bad_alloc”实例后调用终止
【发布时间】:2020-12-21 13:40:45
【问题描述】:

我有以下功能

void ParticleFilter::resample() {
  /**
   * TODO: Resample particles with replacement with probability proportional 
   *   to their weight. 
   */
  // First put all the weights in a vector
  std::cout<<"Weights: ";
  for (auto const & p: particles){
    std::cout<<p.weight<<"  ";
    weights.push_back(p.weight);
  }
  std::cout<<std::endl;

  std::cout<<"Preparing distribution"<<std::endl;

  std::discrete_distribution<size_t> distr(weights.begin(), weights.end());
  std::random_device rd;

  std::cout<<"distribution prepared"<<std::endl;
  // From here we are going to get the index for the particles
  // as distr(rd)
     
  
  //std::cout<<"New Particles: ";   //<=====HERE it breaks!
  std::vector<Particle> new_particles; 
  for(int i=0;i<num_particles;i++){
    int indice = distr(rd);
    std::cout<< indice <<" ";
    new_particles.push_back(particles[indice]);
  }   
  std::cout<<std::endl;  

  particles = new_particles;
}

这是更大代码的一部分。当我运行代码并执行此函数时,它按预期运行,但是当它被 第二次调用时,它会抛出标题异常。

它失败的部分在代码中被标记,这意味着“分发准备”被打印但奇怪的是下一个“新粒子”(当它没有被评论时)没有被打印。 (想知道为什么它们只是打印语句)

在此之后发生异常。我读到的这通常与内存使用不当有关,所以

不知我是不是用std::vector&lt;Particle&gt; new_particles错了?

附件

了解使用的类型

 std::vector<double> weights; 

struct Particle {
  int id;
  double x;
  double y;
  double theta;
  double weight;
  std::vector<int> associations;
  std::vector<double> sense_x;
  std::vector<double> sense_y;
};
std::vector<Particle> particles;

【问题讨论】:

  • 注释掉的代码是否相关?如果没有,请删除它以留下minimal reproducible example
  • 在您的“新粒子:”打印行中没有明确刷新到标准输出,这可能是您在崩溃之前看不到它打印的原因。在迭代中的每个 std::cout 行的末尾添加
  • 只是猜测,因为我们没有最小的示例:您将更多值附加到您的 weights 向量,现在它比您的 particles 向量大。这意味着particles[indice] 可能超出范围,因为您的分布基于weights 而不是particles 的大小。
  • 如果您在调试器中运行代码,则无需猜测。
  • 一次删除一行代码,直到它不再崩溃...并提供最少的main 重现问题的需要。

标签: c++ memory


【解决方案1】:

您可能内存不足,bad_alloc 异常告诉您同样多。所以我不太明白混淆是什么:内存分配失败,抛出异常,如果分配失败,您的代码似乎没有多大意义,因此您没有捕获异常(因为那里没有合理的恢复操作),程序终止 - 一切都很好。分配更少的内存就不会出现这个问题,或者编译为 64 位。

还有其他效率低下的地方,所以让我们一一解决。

单独存储 sense_xsense_y 值不是一个好主意:每个向量都会分配,因此每个粒子的分配量是两倍 并且它们在内存中不一定靠得很近,因此访问它们的缓存压力和延迟更高。将它们放在一个结构中,例如Point

#include <iostream>
#include <random>
#include <type_traits>
#include <vector>

struct Point {
    double x, y;
};

struct Particle {
    int id;
    Point pos;
    double theta;
    double weight;
    std::vector<int> associations;
    std::vector<Point> sense;
};

然后,您不需要复制权重:它们已经存在,因此您不妨提供一个“查看”它们的迭代器:

class const_weight_iterator : public std::vector<Particle>::const_iterator {
    using It = std::vector<Particle>::const_iterator;
    inline It &base() { return *this; }
    inline const It &base() const { return *this; }
public:
    using value_type = const double;
    using pointer = value_type*;
    using reference = value_type&;
    reference operator*() const { return base()->weight; }
    pointer operator&() const { return &(base()->weight); }
    const_weight_iterator(const_weight_iterator &) = default;
    const_weight_iterator(const It &iter) : It(iter) {}
};

我们使用它来创建粒子向量中权重的视图:

class as_const_weights {
    const const_weight_iterator m_begin, m_end;
public:
    as_const_weights(const std::vector<Particle> &v) :
        m_begin(v.begin()), m_end(v.end()) {}
    const_weight_iterator begin() const { return m_begin; }
    const_weight_iterator end() const { return m_end; }
};

编写这样的代码很乏味,您可能想改用boost::make_transform_iterator - 这样您就不需要编写自己的迭代器:

class as_const_weights {
    static auto get_weight(const Particle &particle) { return particle.weight; }
    const std::vector<Particle>::const_iterator m_begin, m_end;
public:
    as_const_weights(const std::vector<Particle> &v) :
        m_begin(v.begin()), m_end(v.end()) {}
    auto begin() const { return boost::make_transform_iterator(m_begin, get_weight); }
    auto end() const { return boost::make_transform_iterator(m_end, get_weight); }
};

另一种选择可能是重用member_access_iterator from the lug project

现在不需要复制权重了:

void ParticleFilter::resample() {
    auto const_weights_view = as_const_weights(m_particles);

    std::cout << "Weights: ";
    for (auto weight : const_weights_view)
        std::cout << weight << "  ";
    std::cout << std::endl;

    std::discrete_distribution<size_t> distr(
        const_weights_view.begin(), const_weights_view.end()
    );
    std::random_device rd;

    auto const num_particles = m_particles.size();
    std::cout << "New Particles: ";
    std::vector<Particle> new_particles; 
    new_particles.reserve(num_particles);
    for (int i=0; i<num_particles; i++){
        int indice = distr(rd);
        std::cout << indice << " ";
        new_particles.push_back(m_particles[indice]); //*note
    }   
    std::cout << std::endl;  

    m_particles = std::move(new_particles);
}

我们确切知道会有多少new_particles,因此reserved 可以有足够的存储空间,从而避免重复的内存重新分配以及由于new_particles 向量必须增长而导致的内存和复制开销。

由于new_particles 在方法返回后不会被使用,我们将它们移动到m_particles - 与m_particles = new_particles 所暗示的副本相比,这是一个便宜的操作。

*注意:我假设索引可以重复。因此,我们不能将粒子移动到new_particles 向量中,即new_particles.push_back(std::move(m_particles[indice])); 是错误的。但是,如果索引是唯一的(不要重复),那么移动将是合适的 - 但你真的想断言这个事实:

    auto const num_particles = m_particles.size();

    std::vector<Particle> new_particles; 
    new_particles.reserve(num_particles);
    std::set<int> new_indices;

    for (int i=0; i<num_particles; i++){
        int indice = distr(rd);
        std::cout << indice << " ";
        assert(new_indices.find(indice) == new_indices.end());
        // the index is unique
        new_particles.push_back(std::move(m_particles[indice]));
        new_indices.insert(indice); 
    }   

我不确定您的确切应用程序是什么,以及您需要多久访问一次m_particles 数组,但我们还可以进一步做一步。请注意,resample 所做的只是创建索引映射。如果这个重采样的粒子数组不经常被访问,那么我们就不会意识到顺序访问的好处——它们会被复制粒子向量的成本所掩盖。

在这种情况下,我们只需要生成新索引的向量(而不是粒子),并拥有一个使用它的视图:

template <class Container, class Index> class indexed_view {
    Container &m_container;
    const Index &m_index;
    using value_iter = typename Container::iterator;
    using index_iter = typename Index::const_iterator;
    using index_value = typename Index::value_type;
public:
    using value_type = typename Container::value_type;
    using size_type = typename Container::size_type;

    class iterator {
        Container &m_container;
        index_iter m_it_index, m_end_index;
        index_value m_val_index;
        auto get_index() const { return m_it_index != m_end_index ? *m_it_index : index_value{}; }
    public:
        using value_type = typename value_iter::value_type;
        using reference = typename value_iter::reference;
        using pointer = typename value_iter::pointer;
        iterator(Container &container, index_iter iter, index_iter end) : 
            m_container(container),
            m_it_index(iter), m_end_index(end),
            m_val_index(get_index())
        {}
        iterator(const iterator &) = default;
        reference operator*() const { return m_container[m_val_index]; }
        pointer operator&() const { return &(m_container[m_val_index]); }
        auto &operator++() { ++m_it_index; m_val_index = get_index(); return *this; }
        auto &operator--() { --m_it_index; m_val_index = get_index(); return *this; }
        auto operator++(int) = delete;
        auto operator--(int) = delete;
        bool operator!=(const iterator &o) const { return m_it_index != o.m_it_index; }
    };

    indexed_view(Container &container, Index &index) :
        m_container(container), m_index(index)
    {}
    auto size() const { return m_index.size(); }
    iterator begin() { return iterator(m_container, m_index.begin(), m_index.end()); }
    iterator end() { return iterator(m_container, m_index.end(), m_index.end()); }
    auto &operator[](size_type index) { return m_container[m_index(index)]; }
};

template <class T1, class T2>
auto make_indexed_view(T1 &&container, T2 &&view) {
    return indexed_view<std::decay_t<T1>, std::decay_t<T2>>(
        std::forward<T1>(container), std::forward<T2>(view)
    );
}

我认为 Boost 不提供类似的功能,当然 Ranges library 也不提供。

ParticleFilter 类现在如下所示:

class ParticleFilter {
    using size_type = std::vector<Particle>::size_type;
    std::vector<Particle> m_particles;
    std::vector<size_type> m_sampledIndex;

public:
    void resample();
    void generateSampledIndex();
    void processSampledParticles();
};

使用与粒子采样循环非常相似的循环生成采样索引,不同之处在于我们只存储新索引而不是存储粒子:

void ParticleFilter::generateSampledIndex() {
    auto const_weights_view = as_const_weights(m_particles);

    std::discrete_distribution<size_t> distr(
        const_weights_view.begin(), const_weights_view.end()
    );
    std::random_device rd;

    auto const num_particles = m_particles.size();
    m_sampledIndex.clear();
    m_sampledIndex.reserve(num_particles);
    for (int i=0; i<num_particles; i++) {
        int indice = distr(rd);
        m_sampledIndex.push_back(indice);
    }
}

现在可以轻松使用采样视图 - 原始粒子矢量不受干扰。

void ParticleFilter::processSampledParticles()
{
    for (auto &particle : make_indexed_view(m_particles, m_sampledIndex)) {
        // ...
    }
}

在 C++20 中,make_indexed_view 不是必需的(感谢 HTNW!):

auto ParticleFilter::sampled_view() {
  return
    m_sampledIndex
    | std::views::transform([&](auto i) -> auto& { return m_particles[i]; });
}

void ParticleFilter::processSampledParticles()
{
    for (auto &particle : make_indexed_view(m_particles, m_sampledIndex)) {
        // ...
    }
}

当然,您必须执行基准测试以确定索引视图是否有利。此类基准测试必须使用真实数据并在发布版本中完成 - 对调试版本进行基准测试几乎无法了解发布版本的性能。

【讨论】:

    猜你喜欢
    • 2021-01-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多