连续数组多维表示的快速元素访问
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++ 的强大模板功能为具有不同类型(例如不同数量的维度)的数组指定 ()
的唯一重载。但我只在非常基本的通用情况下使用模板,比如让函数同时使用float
和double
。
我在想象这样的事情:
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<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();
;
c++14。 Live example.
如果您使用的[]
s 不够,它指的是相关子数组的第一个元素。如果你使用太多它会 UB。它会在维度计数和大小方面进行零维度检查。
两者都可以添加,但会性价比。
维数是动态的。您可以将buffer
拆分为两种类型,一种拥有缓冲区,另一种提供它的尺寸视图。
【讨论】:
我尝试为您编辑此内容,但更改在同行评审中被拒绝。这里是 cmets:向 strides 数组添加了额外的单位值。更正了缓冲区 get() 访问器上的跨步数组偏移量。最高步幅不应该是索引计算的一部分。否则,后续增量会跳等于最后一个步幅条目的量。还更改了 for 循环中的大写拼写错误 'I'->'i' 这里是变化:templateemplace_back
修补了我的错误,但没有修复它。
@user5915738 重新编辑:slice<T>
遵循 const 的指针规则。你想要的slice<T> const
真的是slice<T const>
。指针上的 []
和 *
是 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_sequence
是std::integer_sequence
的别名,具有固定的无符号类型,同样是std::size_t
。
在设计为使用无符号整数作为索引的世界中,使用有符号整数是可能的,但恕我直言,危险(因为容易出错)。
【讨论】:
关于signed
vs unsigned
:这个问题并不像你所说的那么清楚:计算与unsigned
s 的差异会产生令人惊讶的结果,而且编译器可以更加优化signed
计算因为未定义的溢出。
@Julius - 我承认我什至没有考虑过表演。而且我不会太重视它,因为(我想)严重依赖于特定的硬件和编译器优化。我的建议来自观察到所有 STL 都将大小(size()
方法)和基于索引的方法(operator[]
和 at()
)视为基于无符号类型,通常是 std::size_t
。混合有符号和无符号类型可能很危险,非常非常危险(“我见过你们人们不会相信的事情......”)。
感谢您的解释。我了解signed
与unsigned
讨论中有这些不同的立场,我当然接受您的意见。我的意图只是指出关于这个问题存在不同的意见。因此,我认为这个 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>
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>
的简单可变多维矩阵类
您可以创建不同大小的浮点数、整数、字符等矩阵,例如2x2
、2x3
、5x3x7
、4x9x8x12x2x19
。这是一个非常简单但用途广泛的类。
它使用std::vector<>
,所以搜索时间是线性的。多维矩阵在维度上增长越大,内部容器将根据每个维度的大小而增长;如果每个单独的维度都具有很大的维度,这可能会很快“爆炸”,例如:9x9x9
只是一个3 dimensional volumetric matrix
,它比2x2x2x2x2
具有更多的元素,5 dimensional volumetric matrix
。第一个矩阵有729
元素,而第二个矩阵只有32
元素。
我没有包含默认构造函数、复制构造函数、移动构造函数,也没有包含任何可以接受std::container<T>
或另一个Matrix<T,...>
的重载构造函数。这可以作为 OP 的练习来完成。
我也没有包含任何简单的函数来给出主容器中总元素的大小,也没有包含与strides
容器大小相同的总维度数。 OP 应该能够非常简单地实现这些。
至于strides
和多维坐标索引,OP 需要使用stride
值再次计算适当的索引,我将其作为主要练习。
编辑 - 我继续添加了一个默认构造函数,将一些成员移动到类的私有部分,并添加了一些访问函数。我这样做是因为我只是想在 main 函数中展示这个类的强大功能,即使在创建它的类型的空容器时也是如此。
您甚至可以使用用户 Yakk 的“步幅和切片”算法来回答,并且应该能够轻松地将其直接插入到此类中,从而为您提供所需的全部功能。
【讨论】:
【参考方案4】:在我看来,您可以更具体地使用Boost.MultiArray、boost::multi_array_ref
。 boost::multi_array_ref
完全符合您的要求:它将连续数据数组包装到一个可以被视为多维数组的对象中。您也可以使用boost::multi_array_ref::array_view
进行切片。
我无法为您提供任何基准测试结果,但根据我的经验,我可以说boost::multi_array_ref
工作得非常快。
【讨论】:
以上是关于连续数组多维表示的快速元素访问的主要内容,如果未能解决你的问题,请参考以下文章