Подтвердить что ты не робот

Шаблон для цикла 'for' на С++?

У меня есть фрагмент С++ ниже с циклом времени выполнения for,

for(int i = 0; i < I; i++)
  for (int j = 0; j < J; j++)
    A( row(i,j), column(i,j) ) = f(i,j);

Отсчетчик вызывается повторно. Границы цикла "I" и "J" известны во время компиляции (I/J - порядок от 2 до 10). Я хотел бы развернуть петли, используя шаблоны. Основным узким местом является функция row() и column() и f(). Я хотел бы заменить их эквивалентными метапрограммами, которые оцениваются во время компиляции, используя трюки row<i,j>::enum.

То, что я действительно люблю, - это то, что в конечном итоге разрешает цикл в последовательности операторов, таких как:

A(12,37) = 0.5;
A(15,23) = 0.25;
A(14,45) = 0.25;

Но я бы хотел сделать это, не слишком сильно разрушая структуру for - for. Что-то в духе:

TEMPLATE_FOR<i,0,I>
  TEMPLATE_FOR<j,0,J>
     A( row<i,j>::value, column<i,j>::value ) = f<i,j>::value

Может ли boost:: lambda (или что-то еще) помочь мне создать это?

4b9b3361

Ответ 1

Хороший компилятор должен развернуть для вас. Например, в gcc-компиляции с параметром -O2 включается разворот цикла.

Если вы попытаетесь сделать это самостоятельно вручную, если вы не тщательно оцениваете вещи и не знаете, что делаете, вы можете получить более медленный код. Например, в вашем случае с ручным развёртыванием вы можете не дать компилятору возможности выполнить перестановку циклов или оптимизацию полосы (посмотрите на -floop-exchange и -floop-strip-mine in gcc docs)

Ответ 2

Это способ сделать это напрямую:

template <int i, int j>
struct inner
{
  static void value()
  {
    A(row<i,j>::value, column<i,j>::value) = f<i,j>::value;
    inner<i, j+1>::value();
  }
};

template <int i> struct inner<i, J> { static void value() {} };

template <int i>
struct outer
{
  static void value()
  {
    inner<i, 0>::value();
    outer<i+1>::value();
  }
};

template <> struct outer<I> { static void value() {} };

void test()
{
  outer<0>::value();
}

Вы можете передать A через параметр для каждого из value, если это необходимо.

Вот путь с вариационными шаблонами, которые не требуют жесткого кодирования я и J:

#include <utility>

template <int j, class Columns>
struct Inner;

template <class Columns, class Rows>
struct Outer;

template <int j, int... i>
struct Inner<j, std::index_sequence<i...>>
{
  static void value() { (A(column<i, j>::value, row<i, j>::value), ...); }
};


template <int... j, class Columns>
struct Outer<std::index_sequence<j...>, Columns>
{
  static void value() { (Inner<j, Columns>::value(), ...); }
};

template <int I, int J>
void expand()
{
  Outer<std::make_index_sequence<I>, std::make_index_sequence<J>>::value();
}

void test()
{
  expand<3, 5>();
}

(фрагмент с сгенерированной сборкой: https://godbolt.org/g/DlgmEl)

Ответ 4

Вы можете использовать Boost MPL.

Пример разворачивания цикла на этой странице mpl:: for_each.

for_each< range_c<int,0,10> >( value_printer() );

Кажется, что все это не оценивалось во время компиляции, но это может быть хорошей отправной точкой.

Ответ 5

Я бы сказал, что это ложная хорошая идея.

В С++ это: row<i,j>::value
означает, что у вас будет столько разных функций row<>(), сколько у вас есть я * j. Вы не хотите этого, потому что это увеличит размер кода и пропустит много промахов кэша команд.

Я наблюдал это, когда делал функции шаблона, чтобы избежать одной логической проверки.

Если это короткая функция, просто введите ее.

Ответ 6

Вы можете использовать Boost.Mpl, чтобы реализовать все это во время компиляции, но я не уверен, что это будет быстрее. (Mpl по существу повторно реализует все алгоритмы STL в качестве шаблонов метапрограммирования времени компиляции)

Проблема с этим подходом заключается в том, что вы в конечном итоге разворачиваете и вставляете много кода, что может привести к краху кэша команд и съесть пропускную способность памяти, которая могла быть сохранена. Это может вызвать огромный, раздутый и медленный код.

Вероятно, я, вероятно, скорее доверяю компилятору встроить функции, которые имеют смысл. Пока определения цикла row и column видны из цикла, компилятор может тривиально встроить вызовы и развернуть столько итераций, сколько он считает полезным.

Ответ 7

f нужно будет вернуть double, который не может быть выполнен во время компиляции.

Ответ 8

Если вы хотите немного изменить синтаксис, вы можете сделать что-то вроде этого:

template <int i, int ubound>
struct OuterFor {
    void operator()() {
        InnerFor<i, 0, J>()();
        OuterFor<i + 1, ubound>()();
    }
};

template <int ubound>
struct OuterFor <ubound, ubound> {
    void operator()() {
    }
};

In InnerFor, я - счетчик внешних циклов (константа времени компиляции), j - счетчик внутренних циклов (изначально 0 - также константа времени компиляции), поэтому вы можете оценить строку как шаблон времени компиляции.

Это немного сложнее, но, как вы говорите, row(), col() и f() - ваши сложные части. По крайней мере, попробуйте и посмотрите, стоит ли производительность. Возможно, стоит изучить другие варианты, чтобы упростить функции row() и т.д.

Ответ 9

Я никогда не пытался это сделать, поэтому возьмите эту идею с солью...

Кажется, вы могли бы использовать Boost.Preprocessor для выполнения разворачивания цикла (в частности, BOOST_PP_FOR и BOOST_PP_FOR_r), а затем использовать шаблоны для генерации фактического постоянного выражения.

Ответ 10

Я не поклонник мета-программирования шаблонов, поэтому вы можете взять этот ответ со щепоткой соли. Но прежде чем я вложил в это время какое-либо время, я задал себе вопрос:

  • Является ли мой цикл for узким местом?
  • Развертывает ли это разницу? Легко проверяется ручным разворачиванием и измерением.

Во многих компиляторах /cpus "зацикленная" версия может обеспечить лучшую производительность из-за эффектов кеша.

Помните: сначала измерьте, оптимизируйте позже - если вообще.