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

Posted

技术标签:

【中文标题】连续数组多维表示的快速元素访问【英文标题】:Fast element access for multi-dimensional representation of contiguous array 【发布时间】:2018-05-21 00:17:51 【问题描述】:

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

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

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

我只是对模板有点熟悉,我认为应该有某种方法可以利用 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);

我该如何使用模板来做这件事?这甚至可能吗?

【问题讨论】:

我建议使用boost::multi_array_ref 我最近回答了一个密切相关的问题here,它是关于一个允许重塑并提供元素访问的动态多维数组。 @bnaecker 这是一个有吸引力的选择,我喜欢重塑的便利性,并且使用索引引用向量(我可以在外部更改)可以解决速度问题!感谢您的回复! 【参考方案1】:
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&lt;T&gt; 的数据和一个std::vector&lt;std::size_t&gt; 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(); 
;

c++14。 Live example.

如果您使用的[]s 不够,它指的是相关子数组的第一个元素。如果你使用太多它会 UB。它会在维度计数和大小方面进行零维度检查。

两者都可以添加,但会性价比。

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

【讨论】:

我尝试为您编辑此内容,但更改在同行评审中被拒绝。这里是 cmets:向 strides 数组添加了额外的单位值。更正了缓冲区 get() 访问器上的跨步数组偏移量。最高步幅不应该是索引计算的一部分。否则,后续增量会跳等于最后一个步幅条目的量。还更改了 for 循环中的大写拼写错误 'I'->'i' 这里是变化:template struct buffer std::vector data; std::vector<:size_t> 跨步;缓冲区(std::vector<:size_t> 大小,std::vector d):数据(std::move(d)),strides(大小) std::size_t scale = 1; for (std::size_t i = 0; i get() return data.data(), strides.data() + 1 ; slice operator[](std::size_t I) return get()[I]; ; @chrisg 你的设计有错误;但我的有一个错误。修复了两者。 emplace_back 修补了我的错误,但没有修复它。 @user5915738 重新编辑:slice&lt;T&gt; 遵循 const 的指针规则。你想要的slice&lt;T&gt; const 真的是slice&lt;T const&gt;。指针上的 []* 是 const,即使指向的对象不是 const。【参考方案2】:

如果你可以使用 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[])接收无符号值来看到它。

无论对错,该语言本身旨在从旧的sizeof() 和最近的可变参数sizeof...() 返回一个std::size_t。同一个类std::index_sequencestd::integer_sequence 的别名,具有固定的无符号类型,同样是std::size_t

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

【讨论】:

关于signed vs unsigned:这个问题并不像你所说的那么清楚:计算与unsigneds 的差异会产生令人惊讶的结果,而且编译器可以更加优化signed 计算因为未定义的溢出。 @Julius - 我承认我什至没有考虑过表演。而且我不会太重视它,因为(我想)严重依赖于特定的硬件和编译器优化。我的建议来自观察到所有 STL 都将大小(size() 方法)和基于索引的方法(operator[]at())视为基于无符号类型,通常是 std::size_t。混合有符号和无符号类型可能很危险,非常非常危险(“我见过你们人们不会相信的事情......”)。 感谢您的解释。我了解signedunsigned 讨论中有这些不同的立场,我当然接受您的意见。我的意图只是指出关于这个问题存在不同的意见。因此,我认为这个 OTPS 会使您的答案恶化:[quote start] 如果我理解正确,您的 MaxDimX 是维度向量;所以应该是一个 unsigned 整数,而不是一个有符号的int [quote end]。它只是没有你在这里展示的那么清楚:在我看来,int 可能是完美的选择(当然取决于具体的要求)。 @Julius - 我知道存在不同的意见,我尊重他们;但是,根据我的经验,使用有符号整数确实很容易出错;无论如何,您让我相信我的建议应该在答案中得到更好的解释,而不是在评论中。答案已修改。【参考方案3】:

在创建具有可变维度的矩阵类的类模板时,我已经多次使用过这种模式。

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>

ma​​in.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 &lt;T&gt;的简单可变多维矩阵类

您可以创建不同大小的浮点数、整数、字符等矩阵,例如2x22x35x3x74x9x8x12x2x19。这是一个非常简单但用途广泛的类。

它使用std::vector&lt;&gt;,所以搜索时间是线性的。多维矩阵在维度上增长越大,内部容器将根据每个维度的大小而增长;如果每个单独的维度都具有很大的维度,这可能会很快“爆炸”,例如:9x9x9 只是一个3 dimensional volumetric matrix,它比2x2x2x2x2 具有更多的元素,5 dimensional volumetric matrix。第一个矩阵有729 元素,而第二个矩阵只有32 元素。

我没有包含默认构造函数、复制构造函数、移动构造函数,也没有包含任何可以接受std::container&lt;T&gt; 或另一个Matrix&lt;T,...&gt; 的重载构造函数。这可以作为 OP 的练习来完成。

我也没有包含任何简单的函数来给出主容器中总元素的大小,也没有包含与strides 容器大小相同的总维度数。 OP 应该能够非常简单地实现这些。

至于strides 和多维坐标索引,OP 需要使用stride 值再次计算适当的索引,我将其作为主要练习。

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

您甚至可以使用用户 Yakk 的“步幅和切片”算法来回答,并且应该能够轻松地将其直接插入到此类中,从而为您提供所需的全部功能。

【讨论】:

【参考方案4】:

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

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

【讨论】:

以上是关于连续数组多维表示的快速元素访问的主要内容,如果未能解决你的问题,请参考以下文章

C语言多维数组声明元素初始化规则

连续空间的多维数组实现

连续空间的多维数组实现

多维数组

多维数组

pytorch笔记:contiguous &tensor 存储知识