Оболочка для std :: vector, которая делает Structure of Arrays похожим на Array of Structures - PullRequest
0 голосов
/ 24 февраля 2020

У меня есть вектор структуры с 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 ) );
  }
  ...
};

Это работает, но это грязно. Он также использует только 4 из 8 полей. Я хотел бы добавить больше полей позже, не меняя десятки строк кода для каждого поля. Есть ли более компактный / чистый способ сделать это? Некоторые волхвы c с макросами, шаблонами, C ++ 11, et c? Спасибо.

1 Ответ

0 голосов
/ 25 февраля 2020

Ну, маленькие биты включения (use_uv() и т. Д. 1078 *) интересны, и я уверен, что не существует обобщенной c версии аналогичной функции, поэтому я дал ей go .

Чтобы сделать данные обобщенными c, вы должны заменить часть "struct" и имена этих прекрасных полей на индексы std::tuple, структуры generi c. Вы можете восполнить это, расширив 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) { }

Теперь я попытался классифицировать каждый метод в вашем классе как некое обобщенное c действие:

  • select - что-то делает с первым активированным вектором и возвращает значение
  • apply - делает что-то с каждым активным вектором (или всеми векторами)
  • apply_join - архивирует кортеж с другим кортеж и что-то делает с каждой включенной парой
  • get_mtd - эту функцию я не смог преобразовать sh в другие типы

Функции apply* должны иметь версия, которая применяет его только к включенным массивам или ко всем массивам.

Если вы сопоставите весь интерфейс publi c с этими обобщенными c функциями:

  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 generi c lambdas).

Чтобы реализовать select, вы не можете просто написать al oop, потому что std::tuple может индексироваться только с аргументом времени компиляции . Так что вместо этого вам нужно go рекурсивно: начинать с нуля, и если [0] включено, применить лямбду к этому вектору ИЛИ повторить для следующего индекса:

  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_sequence и std::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, он будет применять лямбду независимо от того, что возвращает предикат.

Множество функций, таких как push_back и swap, принимают либо слайс, либо другой 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

...