A couple days ago I posted my first implementation of my data structure here, which implements a cache-optimized tensor representation. I have worked the ideas from the responses into my implementation and I would like to see how I could further improve it.
Code Explanation
I will provide a rough explanation of the operation of the code as there is quite a bit to it.
Tensor Class
The data is stored in a single std::vector
in the Tensor
class. An offsets
member variable is also used to determine how far to step to get to the next element of a specific dimension.
Tensor::Operator[]
I have overloaded Tensor::operator[]
with both const and non-const variants. In the non-const variant a return type of decltype(auto)
is used to make sure that in the case where DIM == 1
a double&
is returned, in the case where DIM != 1
an object of type Tensor::View
is returned with dimension one less. The const variant uses a return type of auto
and so instead returns double
in the case where DIM == 1
and an object of type Tensor::ConstView
with one dimension less otherwise.
Tensor::resize
A resize function is provided in Tensor
which reallocates the memory buffer for a given dimension of tensor. the offsets
member variable is also given values which is used to determine how far to step through the vector for a given dimension.
Tensor::View class
The Tensor::View
class has two member variables, data
which points to an area in memory owned by the data
member of some Tensor
class, and offsets
which points to the start of the member variable offsets
in a tensor class. This class also implements a single operator[]
overload which either returns a double&
or another Tensor::View
of one dimension less.
Tensor::ConstView class
The Tensor::ConstView
is similar to the Tensor::View
class, only that it only has constant pointer member variable equivalents to the Tensor::View
member variables. A single operator[]
overload is given which returns either a double
or a Tensor::ConstView
of one dimension less.
My Concerns
My main concern is the amount of repetition in the code. This is not too much of an issue at the moment as there are not many member variables provided, however as I add more they will need to be implemented three times, once in the main Tensor
class and then once in each of the Tensor::View
and Tensor::ConstView
classes. I do not know a way around this; however, I would really like to hear some of your suggestions on this.
I am also concerned about the potential for poor performance as I need to pass two separate pointers into the constructor of each Tensor::View
or Tensor::ConstView
. I am not sure whether or not the performance penalty from this is substantial, and so I am interested to see what others think. Thank you.
template<typename Ty, int DIM>
class Tensor
{
private:
std::vector<Ty> data;
std::vector<size_t> offsets;
public:
Tensor() :
offsets(DIM + 1)
{}
Tensor(const std::vector<size_t> &dims) :
offsets(DIM + 1)
{
resize(&dims.data()[0]);
}
decltype(auto) operator[] (size_t i)
{
if constexpr (DIM == 1)
{
return data[i];
}
else
{
return typename Tensor<Ty, DIM-1>::View(&data.data()[i*offsets[DIM-1]], &offsets.data()[0]);
}
}
auto operator[] (size_t i) const
{
if constexpr (DIM == 1)
{
return data[i];
}
else
{
return typename Tensor<Ty, DIM-1>::ConstView(&data.data()[i*offsets[DIM-1]], &offsets.data()[0]);
}
}
void resize(const size_t *dims)
{
offsets[0] = 1;
for (size_t i = 1; i <= DIM; ++i)
{
offsets[i] = offsets[i - 1] * dims[DIM - i];
}
data.resize(offsets[DIM]);
}
size_t size(int axis=0) const
{
return offsets[DIM-axis] / offsets[DIM-axis-1];
}
class View
{
private:
Ty *data;
size_t *offsets;
View() = delete;
View(Ty *data, size_t *offsets) :
data(data), offsets(offsets)
{}
public:
friend class Tensor<Ty, DIM+1>;
decltype(auto) operator[] (size_t i)
{
if constexpr (DIM == 1)
{
return data[i];
}
else
{
return typename Tensor<Ty, DIM-1>::View(&data[i*offsets[DIM-1]], offsets);
}
}
size_t size(int axis=0) const
{
return offsets[DIM-axis] / offsets[DIM-axis-1];
}
};
class ConstView
{
private:
const Ty *data;
const size_t *offsets;
ConstView() = delete;
ConstView(const Ty *data, const size_t *offsets) :
data(data), offsets(offsets)
{}
public:
friend class Tensor<Ty, DIM+1>;
auto operator[] (size_t i) const
{
if constexpr (DIM == 1)
{
return data[i];
}
else
{
return typename Tensor<Ty, DIM-1>::ConstView(&data[i*offsets[DIM-1]], offsets);
}
}
size_t size(int axis=0) const
{
return offsets[DIM-axis] / offsets[DIM-axis-1];
}
};
};
Ty& at(std::initializer_list<size_t> indices)
andconst Ty& at(/*ditto*/) const
. \$\endgroup\$