std::vector的包装器,使数组的结构看起来像结构的数组

Wrapper for std::vector that makes Structure of Arrays look like Array of Structures

本文关键字:数组 结构 看起来 vector 包装 std      更新时间:2023-10-16

我有一个结构的向量,它有8个不同的字段(整数和指针),用作我的程序的数据库。通常只有其中的几个字段被实际使用(通常只有一个)。它最初很好,但现在在存储数十亿元素时内存不足。我想稀疏地存储这些数据,而不需要为每个对象中未使用的字段设置零/空条目。然而,这在代码库中被广泛使用,并且很难更改。

我决定将各个字段存储为单独的向量,并创建一个封装这些向量的类,使SoA对调用方显示为AoS。在创建数据库期间,所使用的字段集在运行时是已知的。它需要有大量的std::vector成员函数。我能想到的最好的是一些宏和大量的复制粘贴代码行来处理各个字段向量:

#define SELECT_FIELD_VECT(FUNC) (use_uv() ? uv.FUNC : (use_dv() ? dv.FUNC : (use_sv() ? sv.FUNC : rv.FUNC)))
#define APPLY_FIELD_VECT(FUNC) { if(use_uv()) {uv.FUNC;} if(use_dv()) {dv.FUNC;} if(use_sv()) {sv.FUNC;} if(use_rv()) {rv.FUNC;} }
class md_tracker_t {
vector< match_track_data_uints_t > uv;
vector< delta_pair  const * > dv;
vector< std::string const * > sv, rv;
public:
bool  empty( void ) const { return SELECT_FIELD_VECT(empty()); }
size_t size( void ) const { return SELECT_FIELD_VECT(size ()); }
size_t capacity( void ) const { return SELECT_FIELD_VECT(capacity()); }
void  clear( void ) { uv.clear(); dv.clear(); sv.clear(); rv.clear(); }
void shrink_to_fit( void ) { APPLY_FIELD_VECT(shrink_to_fit()); }
void reserve( size_t const sz ) { APPLY_FIELD_VECT(reserve(sz)); }
void resize ( size_t const sz ) { APPLY_FIELD_VECT(resize(sz)); }
void swap( md_tracker_t &mt ) { uv.swap( mt.uv ); dv.swap( mt.dv ); sv.swap( mt.sv ); rv.swap( mt.rv ); }
void push_back( match_track_data_t const &md ) {
if( use_uv() ) { uv.push_back( md.uints ); }
if( use_dv() ) { dv.push_back( md.deltas ); }
if( use_sv() ) { sv.push_back( md.signature ); }
if( use_rv() ) { rv.push_back( md.rulename ); }
}
void copy_from( size_t const from_ix, size_t const to_ix ) {
if( use_uv() ) { uv[to_ix] = uv[from_ix]; }
if( use_dv() ) { dv[to_ix] = dv[from_ix]; }
if( use_sv() ) { sv[to_ix] = sv[from_ix]; }
if( use_rv() ) { rv[to_ix] = rv[from_ix]; }
}
void add_from( md_tracker_t const &mt, size_t const ix ) {
if( use_uv() ) { uv.push_back( mt.uv[ix] ); }
if( use_dv() ) { dv.push_back( mt.dv[ix] ); }
if( use_sv() ) { sv.push_back( mt.sv[ix] ); }
if( use_rv() ) { rv.push_back( mt.rv[ix] ); }
}
match_track_data_t get_mtd( size_t const ix ) const {
assert( ix < size() );
return match_track_data_t( ( use_uv() ? uv[ix] : match_track_data_uints_t() ),
( use_dv() ? dv[ix] : 0 ),
( use_sv() ? sv[ix] : 0 ),
( use_rv() ? rv[ix] : 0 ) );
}
...
};

这很管用,但很乱。它也只使用8个字段中的4个。稍后我想添加更多的字段,而不必为每个字段更改数十行代码。有没有更紧凑/更干净的方法可以做到这一点?宏、模板、C++11等的神奇之处?非常感谢。

好吧,您拥有的小启用位(use_uv()等)很有趣,我很确定没有类似函数的通用版本,所以我尝试了一下。

为了使数据具有通用性,您必须将"struct"部分和那些可爱的字段名替换为std::tuple(通用结构)的索引。您可以通过扩展std::tuple来添加访问者来弥补它

struct match_track_data_t : std::tuple<match_track_data_uints_t, delta_pair, std::string, std::string> {
using std::tuple<match_track_data_uints_t, delta_pair, std::string, std::string>::tuple;
match_track_data_uints_t& uv() { return std::get<0>(*this); }
match_track_data_uints_t& uv() const { return std::get<0>(*this); }
delta_pair& dv() { return std::get<1>(*this); }
delta_pair& dv() const { return std::get<1>(*this); }
/* etc... */
};

对于向量,数据是因为向量的元组。use_*标志变成一个谓词数组:

template <typename... Ts>
class md_tracker_t {
std::tuple<std::vector<Ts>...> data;
std::array<bool(*)(), sizeof...(Ts)> use_ix;
public:
md_tracker_t(std::array<bool(*)(), sizeof...(Ts)> use_ix) : use_ix(use_ix) { }

现在,我尝试将类中的每个方法分类为一些通用操作:

  • select-对第一个启用的矢量执行某些操作并返回值
  • apply-对每个启用的矢量(或所有矢量)执行某些操作
  • apply_join-将元组与其他一些元组压缩,并对每个启用的元组执行某些操作
  • get_mtd-这个函数我无法与其他类型混合

apply*函数的版本应仅适用于已启用的数组或所有数组。

如果您将整个公共接口映射到这些通用函数:

bool empty() const { return select([](auto& v) { return v.empty(); }); }
bool size() const { return select([](auto& v) { return v.size(); }); }
bool capacity() const { return select([](auto& v) { return v.capacity(); }); }
void clear() { apply<false>([](auto& v) { v.clear(); }); }
void shrink_to_fit() { apply([](auto& v) { v.shrink_to_fit(); }); }
void reserve( size_t const sz ) { return apply([&sz](auto& v) { v.reserve(sz); }); }
void resize ( size_t const sz ) { return apply([&sz](auto& v) { v.resize(sz); }); }
void swap( md_tracker_t &mt ) { apply_join<false>(mt, [](auto& v, auto& w) { v.swap(w); }); }
void push_back( std::tuple<Ts...> const &md ) { apply_join(md, [](auto& v, auto& e) { v.push_back(e); }); }
void copy_from( size_t const from_ix, size_t const to_ix ) { apply([&from_ix, &to_ix](auto& v) { v[to_ix] = v[from_ix]; }); }
void add_from( md_tracker_t const &mt, size_t const ix ) { apply_join(mt, [&ix](auto& v, auto& w) { v.push_back(w[ix]); }); }
std::tuple<Ts...> get_mtd( size_t const ix ) const { return get_mtd_impl(ix, std::index_sequence_for<Ts...>{}); }

你所要做的就是执行它们!(我在这里使用C++14通用lambdas跳过了很多样板)。

要实现select,不能只写一个循环,因为std::tuple只能用编译时参数进行索引。因此,您必须改为递归:从零开始,如果启用[0],则将lambda应用于该向量,或者重复下一个索引:

template <typename Functor, size_t Index = 0, typename std::enable_if<Index != sizeof...(Ts)>::type* = nullptr>
decltype(auto) select(Functor&& functor) const {
return use_ix[Index]() 
? functor(std::get<Index>(data))
: select<Functor, Index + 1>(std::forward<Functor>(functor));
}
template <typename Functor, size_t Index, typename std::enable_if<Index == sizeof...(Ts)>::type* = nullptr>
decltype(auto) select(Functor&& functor) const  { return decltype(functor(std::get<0>(data))){}; }

apply稍微简单一点,因为您只需使用std::index_sequencestd::get将整个元组扩展为一次性布尔数组。逗号运算符(总是返回右手边)是一种欺骗代码,用于将void函数转换为表达式:

template <bool conditional = true, typename Functor, size_t... Is>
void apply(Functor&& functor, std::index_sequence<Is...>) {
std::initializer_list<bool> { (!conditional || use_ix[Is]() ? (functor(std::get<Is>(data)), false) : false)... };
}
template <bool conditional, typename Functor>
void apply(Functor&& functor) {
return apply(std::forward<Functor>(functor), std::index_sequence_for<Ts...>{});
}

布尔conditional模板参数基本上是一个重写。如果是false,那么无论谓词返回什么,它都将应用lambda。

很多函数,比如push_backswap,要么取一个切片,要么取另一个SoA,并将其交叉连接。对于这些函数,我们有apply_join,它几乎与apply相同,只是它处理额外的参数:

template <bool conditional = true, typename Functor, typename Arg, size_t... Is>
void apply_join(Functor&& functor, Arg& arg, std::index_sequence<Is...>) {
std::initializer_list<bool> { (!conditional || use_ix[Is]() ? (functor(std::get<Is>(data), std::get<Is>(arg)), false) : false)... };
}
template <bool conditional = true, typename Functor, typename Arg>
void apply_join(Functor&& functor, Arg& arg) {
return apply_join(std::forward<Functor>(functor), arg, std::index_sequence_for<Ts...>{});
}

最后,get_mtd只是扩展元组,将索引运算符应用于每个元组,然后将其传递给std::tuple:

template <size_t... Is>
std::tuple<Ts...> get_mtd_impl( size_t const ix, std::index_sequence<Is...>) const {
assert(ix < sizeof...(Ts));
return std::tuple<Ts...>(std::get<Is>(data)[ix]...);
}

就这样!

};

可能比手工编写的代码更多,但您可以全天添加字段,而不需要不必要的样板文件。

用法:

using md_tracker = md_tracker_t<match_track_data_uints_t, delta_pair, std::string, std::string>;

演示:https://godbolt.org/z/p5t6wu

Intel为此创建了一个模板库。参见SDLT(SIMD数据布局模板)

SIMD数据布局模板(SDLT)是一个C++11模板库提供表示"阵列"的容器;普通老数据";对象(数据成员没有任何指针/引用的结构,以及没有虚拟功能),使用能够生成高效的SIMD(单指令多数据)矢量代码。

SDLT使用标准ISO C++11代码。它不需要特殊语言或编译器是功能性的,但要利用性能功能(如OpenMP*SIMD扩展和pragma ivdep)并非所有编译器都可以使用。它旨在促进可扩展性SIMD矢量编程。要使用库,请指定SIMD循环和使用显式矢量规划模型和SDLT的数据布局容器,并让编译器在高效的方式。许多库接口使用泛型编程,其中接口由类型上的需求定义而不是特定类型。C++标准模板库(STL)是一个通用编程的示例。通用编程使SDLT灵活而高效。通用接口使您能够自定义组件以满足您的特定需求。

最终结果是,SDLT使您能够更方便地指定首选SIMD数据布局,而不是使用新的数据结构完全重新构建代码有效的矢量化,同时可以提高性能。

我自己没有这方面的经验,所以欢迎任何尝试过的人发表评论。