【问题标题】:Two pointers alternative to C++ std::vector替代 C++ std::vector 的两个指针
【发布时间】:2021-04-08 12:16:17
【问题描述】:

一般std::vector 对象大小为 24 字节,因为它实现为 3 个指针(在 64 位 CPU 上每个指针大小为 8 字节)。这些指针是:

  • 向量的开始
  • 向量结束
  • 向量保留内存结束(向量容量)

我们是否有任何类似的容器提供相同的接口(容量特性除外)但仅使用两个指针(或者可能是指针 + 大小值)实现?

在某些情况下,例如我当前的项目,这很有意义。我在内存中保存了数百万个向量,但我不需要区分向量大小和容量,内存使用正在成为瓶颈。我考虑了几个选项:

  • std::string 的实现可能很棘手,包括短字符串优化之类的东西,这可能会更糟。
  • std::unique_ptr 分配给某个分配的缓冲区。该接口不如std::vector 方便,我最终会创建自己的类来包装指针和缓冲区的大小,这是我试图避免的。

那么有什么现成的替代品吗?接受基于 Boost 库的解决方案。

【问题讨论】:

  • 能否详细说明“我不需要区分向量大小和容量”?你的数据结构是否需要动态分配,可以改变它的大小吗?
  • 对于std::array,您需要在编译时分配其大小。正如我所说,我要寻找的是 std::vector 的替代品,它表现出相同的行为,但没有实现容量功能(即大小和容量应该是同一件事)。
  • @Bob__ 确实,我需要动态分配它。到目前为止,我不需要在动态分配后更改它的大小。但实际上即使容器仅使用两个指针实现也是可能的(但我们失去了保留内存指针的便利)。
  • std::string 是一个值得考虑的有趣替代方案。我快速检查了一下,std::vector<char> 的重量为 24 个字节,std::string“只有”32 个字节(其他系统的大小可能会有所不同)。并没有真正节省任何东西......

标签: c++ vector boost stdvector


【解决方案1】:

以下是将unique_ptr<T[]> 封装在模拟RandomAccessRange 的一流值类型中的绝对最小值:

#include <memory>
template <typename T>
struct dyn_array {
    explicit dyn_array(size_t n)
      : _n(n), _data(std::make_unique<T[]>(n)) { }

    auto begin() const { return _data.get(); }
    auto end()   const { return begin() + _n; }
    auto begin()       { return _data.get(); }
    auto end()         { return begin() + _n; }
    auto size()  const { return _n; }
private:
    size_t _n {};
    std::unique_ptr<T[]> _data;
};

那是 15 行代码。看到它Live

int main() {
    using std::begin;
    using std::end;

    for (int n; (n = size_gen(prng)) != 10;) {
        dyn_array<double> data(n);
        std::iota(begin(data), end(data), 0);

        static_assert(sizeof(data) == 2*sizeof(void*));

        fmt::print("Size {} data {}\n", n, data);
    }
}

打印例如

Size 8 data {0, 1, 2, 3, 4, 5, 6, 7}
Size 6 data {0, 1, 2, 3, 4, 5}
Size 7 data {0, 1, 2, 3, 4, 5, 6}
Size 6 data {0, 1, 2, 3, 4, 5}

我评论了两个版本

  • 添加带有初始化器live的构造器指南
  • 并添加复制/移动语义live

奖励:单指针大小

在上面的基础上,我意识到大小可以在分配使dyn_array的大小正好是1个指针。

Live Demo

#include <memory>
#include <cstdlib> // committing the sin of malloc for optimization

template <typename T>
struct dyn_array {
    dyn_array(std::initializer_list<T> init)
    : _imp(allocate(init.size(), true))
    { std::uninitialized_move(init.begin(), init.end(), begin()); }

    dyn_array(dyn_array const& rhs)
    : _imp(allocate(rhs.size(), true))
    { std::uninitialized_copy(rhs.begin(), rhs.end(), begin()); }

    dyn_array(dyn_array&& rhs)          { rhs.swap(*this); }
    dyn_array& operator=(dyn_array rhs) { rhs.swap(*this); } 

    explicit dyn_array(size_t n = 0)
    : _imp(allocate(n))
    { }

    auto size()  const { return _imp? _imp->_n : 0ull; }
    auto begin() const { return _imp? _imp->_data + 0 : nullptr; }
    auto begin()       { return _imp? _imp->_data + 0 : nullptr; }
    auto end()   const { return begin() + size(); }
    auto end()         { return begin() + size(); }
    auto empty() const { return size() == 0; }

    bool operator==(dyn_array const& rhs) const {
        return size() == rhs.size() &&
            std::equal(rhs.begin(), rhs.end(), begin());
    };

    void swap(dyn_array& rhs) {
        std::swap(_imp, rhs._imp);
    }
private:
    struct Impl {
        size_t _n;
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wpedantic"
        T _data[]; // C99 extension
#pragma GCC diagnostic pop
    };
    struct Deleter {
        void operator()(Impl* s) const { 
            while (s->_n) { s->_data[--(s->_n)].~T(); }
            std::free(s);
        }
    };
    using Ptr = std::unique_ptr<Impl, Deleter>;
    Ptr _imp;

    static Ptr allocate(size_t n, bool uninitialized = false) {
        if (!n)
            return {};

        auto p = std::malloc(sizeof(Impl) + n*sizeof(T)); // could be moreconservative
        auto s = Ptr(reinterpret_cast<Impl*>(p));
        s->_n = n;
        if (!uninitialized)
            std::uninitialized_default_construct_n(s->_data, n);
        return s;
    }
};

我们可以用作:

#include <fmt/ranges.h>

static size_t constructions = 0;
static size_t default_ctor, copy_ctor = 0;
static size_t destructions = 0;

struct Sensor final {
    Sensor()              { ++constructions; ++default_ctor; } 
    Sensor(Sensor const&) { ++constructions; ++copy_ctor; } 
    ~Sensor()             { ++destructions;  } 
};

int main() {
    fmt::print("With initializers: {}, {}\n",
        dyn_array{3.1415f},
        dyn_array{"one", "two", "three"});

    fmt::print("Without: {}, {}\n",
        dyn_array<std::string_view>{3}, 
        dyn_array<float>{1},
        dyn_array<int>{}); // empty by default, no allocation

    auto a = dyn_array{3,2,1};
    fmt::print("sizeof(a) == sizeof(void*)? {}\n", sizeof(a) == sizeof(void*));
    auto copy = a;
    fmt::print("copy: {} == {}? {}\n", copy, a, (copy == a));
    auto move = std::move(copy);
    fmt::print("move: {} == {}? {}\n", move, a, (move == a));
    fmt::print("copy now moved-from: {}, empty? {}\n", copy, copy.empty());

    dyn_array<Sensor>(4); // test destructors
    fmt::print("constructions({}) and destructions({}) match? {}\n",
            constructions, destructions, constructions == destructions);
    fmt::print("all default, no copy ctors? {}\n",
            (copy_ctor == 0) && (default_ctor == constructions));

    dyn_array { Sensor{}, Sensor{}, Sensor{} };
    fmt::print("constructions({}) and destructions({}) match? {}\n",
            constructions, destructions, constructions == destructions);
    fmt::print("initializers({}) were uninitialized-copied: {}\n",
            copy_ctor,
            (copy_ctor == 3) && (default_ctor + copy_ctor == constructions));
}

印刷:

With initializers: {3.1415}, {"one", "two", "three"}
Without: {"", "", ""}, {1}
sizeof(a) == sizeof(void*)? true
copy: {3, 2, 1} == {3, 2, 1}? true
move: {3, 2, 1} == {3, 2, 1}? true
copy now moved-from: {}, empty? true
constructions(4) and destructions(4) match? true
all default, no copy ctors? true
constructions(10) and destructions(10) match? true
initializers(3) were uninitialized-copied: true

当然你可以不使用C99 flexible array members 来做到这一点,但我现在不想手动干预对齐。

奖励:[] 索引、front、back、at 访问器

这些添加起来非常简单:

auto& operator[](size_t n) const { return *(begin() + n); }
auto& operator[](size_t n)       { return *(begin() + n); }
auto& at(size_t n) const { return n<size()?*(begin() + n): throw std::out_of_range("dyn_array::at"); }
auto& at(size_t n)       { return n<size()?*(begin() + n): throw std::out_of_range("dyn_array::at"); }
auto& front() const { return at(0); }
auto& front()       { return at(0); }
auto& back()  const { return at(size()-1); }
auto& back()        { return at(size()-1); }

当然 push_back/erase/etc 已经退出,因为构建后大小不会改变。

【讨论】:

  • 感谢您的方法。我所说的向量是一个类的数据成员,它被实例化了数百万次。我可以使用std::unique_ptr,但我仍然需要另一个单独的变量来保存缓冲区的大小。您提出的跨度解决方案可能有助于抽象我使用的任何实现。
  • 我的意思是为您提供构建模块。在这些原语上构建你的容器应该很简单,但我不会仅仅为了一个 SO 答案来实现一个类似矢量的接口:)
  • 如果您想要一些具有固定大小的现成产品,请考虑boost::container::static_vector,然后您可以通过简单的 unique_ptr 来引用它。它将具有大小和容量,但由于后者是静态固定的,因此不会被存储。
  • 我认为我没有很好地明确我的目的。实际上静态向量(或静态范围)是我不需要的。恰恰相反,所有分配都是动态的,并且向量的大小在编译时是未知的。当然,在 unique_ptr 上实现我自己的类似矢量的接口并不是那么困难。但我想知道 boost 或 STL 是否提供现成的解决方案。这不是关于实现这样的困难,而是关于能够使用一些已知的容器。这总是比介绍我自己的要好......
  • @sapito open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3532.html 提案已经过审核,但 TTBOMK 被拒绝(这未反映在 cplusplus.github.io/LWG/lwg-proposal-status.html 中,所以也许还有机会?)。是的,否则它是一个自定义容器。
【解决方案2】:

动态分配后我不需要更改它的大小(来自 cmets)

由于您不需要动态扩展矢量,std::valarray&lt;T&gt; 可能是一个不错的选择。它的大小在构造时是固定的,因此实现不需要第三个指针。

【讨论】:

  • 好点,但我仍然怀念它可以轻松替换std::vector,只要我不打算调用任何与容量相关的方法。例如,std::valarray 没有 data() 成员函数。
  • @sapito 好吧,&amp;v[0] 将产生指向 std::vector&lt;T&gt; 和 std::valarray&lt;T&gt; 的初始元素的指针。
  • 是的,这是可行的。只是想知道是否有什么东西可以替换std::vector 并立即编译而无需进一步更改代码。否则,是的,valarray 是一个替代方案。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-06-22
  • 2016-02-19
  • 2019-12-03
  • 1970-01-01
  • 2021-02-17
  • 2017-11-13
  • 2016-02-12
相关资源
最近更新 更多