快速元素访问用于连续数组的多维表示

时间:2017-12-07 00:37:53

标签: c++ c++11 templates optimization variadic-templates

我有一个在内存中连续表示的多维数组。我希望隐藏这种表示,让用户访问数组元素就像它是多维的一样:例如my_array[0][3][5]my_array(0,3,5)或类似内容。直到运行时才确定对象的尺寸,但是使用指定它具有多少维度的类型创建对象。这个元素查找需要被调用数十亿次,因此希望每次调用都需要最小的开销。

我看过类似的问题,但没有真正找到一个好的解决方案。使用[]运算符需要创建N-1维对象,这对于像向量矢量这样的多维结构很好,因为对象已经存在,但对于连续数组,它似乎会很快就会卷曲,并需要通过原始数组进行某种切片。

我还研究了重载(),这似乎更有希望,但需要指定参数的数量,这将根据数组的维数而变化。我曾考虑使用列表初始化或向量,但希望避免实例化对象。

我对模板和图表有点熟悉,应该有一些方法可以使用C ++的宏伟模板功能来为不同类型的数组指定()的唯一重载(例如,不同的尺寸数量)。但是我只在非常基本的通用案例中使用了模板,比如使用floatdouble来创建函数。

我想象的是这样的事情:

template<typename TDim>
class MultiArray {
public:
  MultiArray() {} //build some things
  ~MultiArray() {} //destroy some things

  // The number of arguments would be == to TDim for the instantiated class
  float& operator() (int dim1, int dim2, ...) {
    //convert to contiguous index and return ref to element
    // I believe the conversion equation is something like:
    // dim1 + Maxdim1 * ( dim2 + MaxDim2 * ( dim3 + MaxDim3 * (...)))
  }

private:
  vector<float> internal_array;
  vector<int> MaxDimX; // Each element says how large each corresponding dim is.
};

因此,如果我初始化此类并尝试访问元素,它将如下所示:

my_array = MultiArray<4>();
element = my_array(2,5,4,1);

我如何使用模板进行此操作?这甚至可能吗?

4 个答案:

答案 0 :(得分:6)

template<class T>
struct slice {
    T* data = 0;
    std::size_t const* stride = 0;
    slice operator[](std::size_t I)const {
        return{ data + I* *stride, stride + 1 };
    }
    operator T&()const {
      return *data;
    }
    T& operator=(typename std::remove_const<T>::type in)const {
      *data = std::move(in); return *data;
    }
};

存储vector<T>个数据和std::vector<std::size_t> stride步幅,其中stride[0]是第一个索引所需的元素步幅。

template<class T>
struct buffer {
  std::vector<T> data;
  std::vector<std::size_t> strides;

  buffer( std::vector<std::size_t> sizes, std::vector<T> d ):
    data(std::move(d)),
    strides(sizes)
  {
    std::size_t scale = 1;
    for (std::size_t i = 0; i<sizes.size(); ++i){
      auto next = scale*strides[sizes.size()-1-i];
      strides[sizes.size()-1-i] = scale;
      scale=next;
    }
  }
  slice<T> get(){ return {data.data(), strides.data()}; }
  slice<T const> get()const{ return {data.data(), strides.data()}; }
};

Live example

如果你使用的[] s不够,它会引用所讨论的子阵列的第一个元素。如果你使用太多,那就是UB。无论是尺寸还是尺寸,它都会进行零尺寸检查。

两者都可以添加,但会降低性能。

维度数量是动态的。您可以将buffer拆分为两种类型,一种拥有缓冲区,另一种提供尺寸视图。

答案 1 :(得分:3)

在我看来,您可以使用Boost.MultiArrayboost::multi_array_ref来更具体。 boost::multi_array_ref完全符合您的要求:它将连续数据数组包装到一个可被视为多维数组的对象中。您也可以使用boost::multi_array_ref::array_view进行切片。

我无法向您提供任何基准测试结果,但根据我的经验,我可以说boost::multi_array_ref工作得非常快。

答案 2 :(得分:1)

如果你可以使用C ++ 17,那么可变参数模板折叠和row major order,我想你可以编写类似的东西(警告:未经测试)

template <template ... Args>
float & operator() (Args ... dims)
 {
   static_assert( sizeof...(Args) == TDim , "wrong number of indexes" );
   // or SFINAE enable instead of static_assert()

   std::size_t pos { 0U };
   std::size_t i   { 0U };

   ( pos *= MaxDimX[i++], pos += dims, ... );

   return internal_array[pos];
 }

OTPS(Off Topic Post Scriptum):如果我理解正确,你的MaxDimX是一个维度向量;所以应该是无符号整数,非签名int;通常,对于索引,使用std::size_t [见注1]。

OTPS 2:如果你知道编译时的维度数(TDim,对吗?)而不是std::vector,我建议使用std::array;我的意思是

std::array<std::size_t, TDim>  MaxDimX;

- 编辑 -

如果你不能使用C ++ 17,你可以使用未使用的数组初始化技巧来获得类似的东西。

我的意思是

template <template ... Args>
float & operator() (Args ... dims)
 {
   using unused = int[];

   static_assert( sizeof...(Args) == TDim , "wrong number of indexes" );
   // or SFINAE enable instead of static_assert()

   std::size_t pos { 0U };
   std::size_t i   { 0U };

   (void)unused { (pos *= MaxDimX[i++], pos += dims, 0) ... };

   return internal_array[pos];
 }

注1:正如Julius指出的那样,对索引使用有符号或无符号整数是有争议的。

所以我试着更好地解释为什么我建议使用无符号值(例如std::size_t)。

关键是(据我所知)所有标准模板库都设计为使用无符号整数作为索引值。您可以通过size()方法返回的值以及接收索引的访问方法(at()operator[])接收无符号值来查看它。

对或错,语言本身旨在从旧std::size_t和最近的可变参数sizeof()返回sizeof...()。同一个班级std::index_sequencestd::integer_sequence的别名,其中包含固定的无符号std::size_t类型。

在一个设计为索引使用无符号整数的世界中,使用有符号整数可能,但恕我直言,危险(因为容易出错)。

答案 3 :(得分:1)

在创建具有可变尺寸的矩阵类的类模板时,我已多次使用此模式。

<强> Matrix.h

#ifndef MATRIX_H

template<typename Type, size_t... Dims>
class Matrix {
public:
    static const size_t numDims_ = sizeof...(Dims);

private:
    size_t numElements_;

    std::vector<Type> elements_;
    std::vector<size_t> strides_; // Technically this vector contains the size of each dimension... (its shape)
    // actual strides would be the width in memory of each element to that dimension of the container.
    // A better name for this container would be dimensionSizes_ or shape_ 

public:
    Matrix() noexcept;

    template<typename... Arg>
    Matrix( Arg&&... as  ) noexcept;

    const Type& operator[]( size_t idx ) const;

    size_t numElements() const {
        return elements_.size();
    }

    const std::vector<size_t>& strides() const {
        return strides_;
    }

    const std::vector<Type>& elements() const {
        return elements_;
    }

}; // matrix

#include "Matrix.inl"

#endif // MATRIX_H

<强> Matrix.inl

template<typename Type, size_t... Dims>
Matrix<Type, Dims...>::Matrix() noexcept :
strides_( { Dims... } ) {
    using std::begin;
    using std::end;
    auto mult = std::accumulate( begin( strides_ ), end( strides_ ), 1, std::multiplies<>() );
    numElements_ = mult;
    elements_.resize( numElements_ );
} // Matrix


template<typename Type, size_t... Dims>
template<typename... Arg>
Matrix<Type, Dims...>::Matrix( Arg&&... as ) noexcept   : 
elements_( { as... } ),
strides_( { Dims... } ){
    numElements_ = elements_.size();
} // Matrix

template<typename T, size_t... d>
const T& Matrix<T,d...>::operator[]( size_t idx ) const {
    return elements_[idx];
} // Operator[]

<强> Matrix.cpp

#include "Matrix.h"
#include <vector>
#include <numeric>
#include <functional>
#include <algorithm>

<强>的main.cpp

#include <vector>
#include <iostream>
#include "matrix.h"

int main() {

    Matrix<int, 3, 3> mat3x3( 1, 2, 3, 4, 5, 6, 7, 8, 9 );

    for ( size_t idx = 0; idx < mat3x3.numElements(); idx++ ) {
        std::cout << mat3x3.elements()[idx] << " ";
    }

    std::cout << "\n\nnow using array index operator\n\n";

    for ( size_t idx = 0; idx < mat3x3.numElements(); idx++ ) {
        std::cout << mat3x3[idx] << " ";
    }

    std::cout << "\n\ncheck the strides\n\n";
    for ( size_t idx = 0; idx < mat3x3.numDims_; idx++ ) {
        std::cout << mat3x3.strides()[idx] << " ";
    }
    std::cout << "\n\n";

    std::cout << "=================================\n\n";

    Matrix<float, 5, 2, 9, 7> mf5x2x9x7;
    // Check Strides
    // Num Elements
    // Total Size
    std::cout << "The total number of dimensions are: " << mf5x2x9x7.numDims_ << "\n";
    std::cout << "The total number of elements are: " << mf5x2x9x7.numElements() << "\n";
    std::cout << "These are the strides: \n";
    for ( size_t n = 0; n < mf5x2x9x7.numDims_; n++ ) {
        std::cout << mf5x2x9x7.strides()[n] << " ";
    }
    std::cout << "\n";

    std::cout << "The elements are: ";
    for ( size_t n = 0; n < mf5x2x9x7.numElements(); n++ ) {
        std::cout << mf5x2x9x7[n] << " ";
    }
    std::cout << "\n";      

    std::cout << "\nPress any key and enter to quit." << std::endl;
    char c;
    std::cin >> c;

    return 0;

} // main

这是Same Type <T>

的简单变量多维矩阵类

您可以创建不同大小的浮点数,整数,字符等矩阵,例如2x22x35x3x74x9x8x12x2x19。这是一个非常简单但功能多样的课程。

正在使用std::vector<>,因此搜索时间是线性的。多维矩阵尺寸越大,内部容器越大,每个尺寸的大小越大;如果每个单独的维度具有较大的维度大小,则可以相当快速地“爆炸”:例如9x9x9只有3 dimensional volumetric matrix,其元素多于2x2x2x2x2,而5 dimensional volumetric matrix是{ {1}}。第一个矩阵具有729个元素,其中第二个矩阵只有32个元素。

我没有包含默认构造函数,复制构造函数,移动构造函数,也没有包含std::container<T>或其他Matrix<T,...>的任何重载构造函数。这可以作为OP的练习来完成。

我也没有包含任何能够提供主容器中总元素大小的简单函数,也不包括strides容器大小的总维度数。 OP应该能够非常简单地实现这些。

对于strides以及使用多维坐标进行索引,OP需要使用stride值来再次计算适当的索引。我将此作为主要练习。

编辑 - 我继续添加了默认构造函数,将一些成员移动到了类的私有部分,并添加了一些访问函数。我这样做是因为我只想在main函数中展示这个类的强大功能,即使在创建一个类型的空容器时也是如此。

更多的是你可以用他的“步幅和切片”算法来获取用户Yakk的答案,并且应该能够轻松地将其插入到本课程中,为您提供所需内容的全部功能。