адаптация интегрального значения, отличного от constexpr, к параметру шаблона, отличному от типа, и раздувание кода

Рассмотрим объект функции F, принимающий constexpr size_t аргумент I

struct F
{
    template <size_t I>
    constexpr size_t operator()(size <I>) const { return I; }
};

завернутый в тип size <I>, где (для краткости)

template <size_t N>
using size = std::integral_constant <size_t, N>;

Конечно, мы могли бы передать I напрямую, но я хочу подчеркнуть, что это constexpr, используя его в качестве аргумента шаблона. Функция F здесь является фиктивной, но на самом деле она может делать множество полезных вещей, таких как извлечение информации из Ith элемента кортежа. Предполагается, что F имеет один и тот же тип возврата независимо от I. I может быть любого целочисленного типа, но считается неотрицательным.

Проблема

Учитывая значение constexpr size_t I, мы можем вызвать F с помощью

F()(size <I>());

Теперь, что, если мы хотим вызвать F с не-constepr size_t значением i? Рассмотрим следующее:

constexpr size_t L = 10;
idx <F, L> f;
for (size_t i = 0; i < L; ++i)
    cout << f(i) << " ";

(Зачем мне это нужно? Чтобы дать некоторый контекст, я на самом деле пытаюсь встроить составной итератор в представление контейнера, которое представляет собой последовательность "соединенных" (конкатенированных) разнородных контейнеров. Это дало бы возможность сказать что-то вроде join(a, b) = c;, где массивы join(a, b) и c имеют одинаковую длину. Однако i является состоянием итератора, поэтому не может быть constexpr, но суб-итераторы хранятся в кортеже, и к ним нужно обращаться по индексу constexpr. Отдельные value_type примерно согласованы, так что объединенное представление может принимать их тип common_type, но вложенные контейнеры и, следовательно, вложенные итераторы имеют разные типы.)

Решение

Здесь я придумал структуру idx <F, L>, которая адаптирует функцию F для этой цели, предполагая, что входной аргумент меньше L. Это на самом деле прекрасно компилирует вывод

0 1 2 3 4 5 6 7 8 9 

и вот живой пример.

idx работает путем рекурсивной декомпозиции входных данных i в двоичное представление и реконструкции аналога constexpr N:

template <typename F, size_t L, size_t N = 0, bool = (N < L)>
struct idx
{
    template <size_t R = 1>
    inline constexpr decltype(F()(size <0>()))
    operator()(size_t I, size <R> = size <R>()) const
    {
        return I%2 ? idx <F, L, N+R>()(--I/2, size <2*R>()) :
               I   ? idx <F, L, N  >()(  I/2, size <2*R>()) :
               F()(size <N>());
    }
};

где R представляет степень 2 на текущей итерации. Чтобы избежать бесконечного создания экземпляров шаблона, для N >= L дается специализация, возвращающая F()(size <0>()) в качестве фиктивного значения:

template <typename F, size_t L, size_t N>
struct idx <F, L, N, false>
{
    template <size_t R>
    inline constexpr decltype(F()(size <0>()))
    operator()(size_t I, size <R>) const { return F()(size <0>()); }
};

По сути, этот метод является обобщением более распространенной идиомы с логическим аргументом:

bool b = true;
b ? f <true>() : f <false>();

где f — функция, принимающая bool в качестве аргумента шаблона. В этом случае очевидно, что реализованы все две возможные версии f.

Вопрос

Хотя это работает, и его сложность во время выполнения предположительно логарифмическая в i, меня беспокоят последствия во время компиляции, например:

  • сколько комбинаций idx и его template operator() создается для того, чтобы это работало во время выполнения для любого ввода i, который не известен во время компиляции? (я понимаю опять "все возможные", но сколько?)

  • действительно ли возможно встроить operator()?

  • есть ли какая-либо альтернативная стратегия или вариант, который «легче» скомпилировать?

  • должен ли я забыть об этой идее как о случае чистого раздувания кода?

Примечания

Вот время компиляции (в секундах) и размер исполняемого файла (в КБ), которые я измерил для разных значений L:

 L      Clang(s)    GCC(s)    Clang(KB)    GCC(KB)
 10       1.3       1.7          33         36
 20       2.1       3.0          48         65
 40       3.7       5.8          87        120
 80       7.0      11.2         160        222
160      13.9      23.4         306        430
320      28.1      47.8         578        850
640      56.5     103.0        1126       1753

Таким образом, хотя в L он кажется примерно линейным, он довольно длинный и разочаровывающе большой.

Попытка форсировать встроенный operator() не удалась: вероятно, Clang проигнорировал ее (исполняемый файл даже больше), в то время как GCC сообщает recursive inlining.

Запуск nm -C в исполняемом файле, например. для L = 160 показывает 511/1253 разные версии operator() (с Clang/GCC). Это все для N < L, поэтому похоже, что конечная специализация N >= L действительно встроена.

ПС. Я бы добавил тег code-bloat, но система не пускает.


person iavr    schedule 21.02.2014    source источник
comment
Ожидаете ли вы, что L когда-либо будет больше 5?   -  person ecatmur    schedule 21.02.2014
comment
В моем приложении (в контексте, который я даю) нет причин, по которым нельзя было бы конкатенировать, например. 105 массивов, особенно если рассматривать в итоге многомерные...   -  person iavr    schedule 21.02.2014


Ответы (4)


Я называю эту технику волшебным переключателем.

Самый эффективный известный мне способ сделать это — создать собственную таблицу прыжков.

// first, index list boilerplate.  Does log-depth creation as well
// needed for >1000 magic switches:
template<unsigned...Is> struct indexes {typedef indexes<Is...> type;};
template<class lhs, class rhs> struct concat_indexes;
template<unsigned...Is, unsigned...Ys> struct concat_indexes<indexes<Is...>, indexes<Ys...>>{
    typedef indexes<Is...,Ys...> type;
};
template<class lhs, class rhs>
using ConcatIndexes = typename concat_indexes<lhs, rhs>::type;

template<unsigned min, unsigned count> struct make_indexes:
    ConcatIndexes<
        typename make_indexes<min, count/2>::type,
        typename make_indexes<min+count/2, (count+1)/2>::type
    >
{};
template<unsigned min> struct make_indexes<min, 0>:
    indexes<>
{};
template<unsigned min> struct make_indexes<min, 1>:
    indexes<min>
{};
template<unsigned max, unsigned min=0>
using MakeIndexes = typename make_indexes<min, max-min>::type;

// This class exists simply because [](blah){code}... `...` expansion
// support is lacking in many compilers:
template< typename L, typename R, unsigned I >
struct helper_helper {
    static R func( L&& l ) { return std::forward<L>(l)(size<I>()); }
};
// the workhorse.  Creates an "manual" jump table, then invokes it:
template<typename L, unsigned... Is>
auto
dispatch_helper(indexes<Is...>, L&& l, unsigned i)
-> decltype( std::declval<L>()(size<0>()) )
{
  // R is return type:
  typedef decltype( std::declval<L>()(size<0>()) ) R;
  // F is the function pointer type for our jump table:
  typedef R(*F)(L&&);
  // the jump table:
  static const F table[] = {
    helper_helper<L, R, Is>::func...
  };
  // invoke at the jump spot:
  return table[i]( std::forward<L>(l) );
};
// create an index pack for each index, and invoke the helper to
// create the jump table:
template<unsigned Max, typename L>
auto
dispatch(L&& l, unsigned i)
-> decltype( std::declval<L>()(size<0>()) )
{
  return dispatch_helper( MakeIndexes<Max>(), std::forward<L>(l), i );
};

который требует некоторой статической настройки, но довольно быстр при запуске.

Утверждение, что i находится в границах, также может быть полезным.

живой пример

person Yakk - Adam Nevraumont    schedule 21.02.2014
comment
Спасибо, это действительно совсем другая альтернатива. Это было бы определенно быстрее во время выполнения за счет некоторого места для хранения. У GCC действительно есть проблема со строкой, содержащей лямбда, мне нужно настроить. И Clang компилируется за 4,7 секунды для L=1<<10 (1024), но выше этого при сбоях во время компиляции. Есть идеи? (при приближении моего собственного ответа мне удалось даже L=1<<14 (16K) за 44 секунды). - person iavr; 21.02.2014
comment
@iavr переписан. Глубина рекурсии уменьшена до разумной и должна компилироваться быстрее. helper_helper добавлен из-за того, что лямбда... редко поддерживается. Массив указателей на функции X с функциями X, поддерживающими их, кажется гораздо меньшим хранилищем, чем фактические функции X lg X. :) - person Yakk - Adam Nevraumont; 21.02.2014
comment
Еще раз спасибо, эта древовидная реализация make_indexes с глубиной журнала — это то, что я все равно собирался сделать в какой-то момент (но я всегда оставлял на потом). GCC работает сейчас. Для этой последней версии Clang/GCC дает 13,7/40,9 секунд для L=1<<14, так что это превосходит мое решение по времени компиляции :-) Однако двоичный файл составляет 1,9 МБ против 839 КБ для моего решения. - person iavr; 22.02.2014
comment
@iavr раздели символы? Аргументы одной из моих функций становятся довольно нелепыми. Может помочь создание dispatch_helper static и избегание встраивания этого символа? Кроме того, изменение make_indexes, чтобы всегда использовать typedef blah type вместо наследования иногда, также может уменьшить раздувание символов... не уверен. - person Yakk - Adam Nevraumont; 22.02.2014
comment
Ты прав, я забыл об этом. С зачисткой я получаю 746/694 КБ с вашим/моим решением. - person iavr; 22.02.2014
comment
Есть ли шансы, что лямбды будут встроены? Некоторое время назад я использовал аналогичный метод генератора таблицы переходов (для размеченного объединения, также известного как вариант), который эффективно создал класс с набором перегруженных функций-членов для каждого типа варианта, а в в этом случае компилятор не встроился. - person CouchDeveloper; 22.02.2014
comment
@CouchDeveloper есть компиляторы, которые могут inline вызывать указатель на функцию - я видел, как это делает gcc. Я не знаю, сможет ли он справиться с вышеизложенным, и почти наверняка только в том случае, если индекс массива был известен во время компиляции. - person Yakk - Adam Nevraumont; 22.02.2014
comment
@Yakk У меня самая первая версия интегрирована в мой итератор соединений, и она работает, так что еще раз спасибо! В моем случае индекс массива зависит от состояния итератора, поэтому он неизвестен во время компиляции, и я не ожидаю, что вызов будет встроен. Но мое решение также не было встроенным (как рекурсивное), плюс для получения индекса потребовалось вычисление логарифмического времени, поэтому поиск + вызов, я думаю, все же лучше. Самое неприятное, что вызываемые функции являются функциями-членами, поэтому мне пришлось приспосабливаться к дополнительным аргументам, везде передавать *this как t и использовать t.f<I>() вместо f<I>(). - person iavr; 22.02.2014

Если ваше решение имеет ограничение на максимально возможное значение (скажем, 256), вы можете использовать магию макросов и оператор switch, чтобы упростить его:

#define POS(i) case (i): return F<(i)>(); break;
#define POS_4(i) POS(i + 0) POS(i + 1) POS(i + 2) POS(i + 3)
#define POS_16(i) POS_4(i + 0) POS_4(i + 4) POS_4(i + 8) POS_4(i + 12)

int func(int i)
{
    switch(i)
    {
        POS_16(0)
    }
}

Другое возможное решение (с С++ 11) использовать вариативные шаблоны:

template<int I>
struct FF
{
    static int f() { return I; }
};


template<typename... I>
int func(int i)
{
    constexpr int (*Func[])() = { I::f... };
    return Func[i]();
}

int main(int argc, char** argv)
{
    func<FF<0>,FF<1>>(1);
}
person Yankes    schedule 21.02.2014
comment
Я бы предпочел держаться как можно дальше от магии макросов :-) Второе решение интересно и похоже (или то же самое) на решение @Yakk, верно? - person iavr; 22.02.2014
comment
@iavr да, мой constexpr int (*Func[])() = { I::f... }; - это тот же трюк, что и @Yakk: static const F table[] = { helper_helper<L, R, Is>::func... }. Отдых — это шум. - person Yankes; 22.02.2014

Я возьму здесь очевидную позицию и спрошу, стоит ли «я хочу подчеркнуть, что это constexpr, используя его в качестве аргумента шаблона» этих затрат, и если:

struct F
{
    constexpr size_t operator()(size_t i) const { return i; }
    template <size_t I>
    constexpr size_t operator()(size <I>) const { return (*this)(I); }
};

не было бы намного более простым решением.

person Casey    schedule 21.02.2014
comment
Это просто, но не решение. Мне все еще нужно адаптировать ввод, отличный от constexpr, к вызову constexpr. - person iavr; 22.02.2014

Это не совсем ответ, и мой вопрос остается в силе, но я нашел обходной путь, который дает впечатляющий прирост компиляции. Это небольшая подстройка решения, данного в вопросе, где параметр R перемещается из operator() снаружи в структуру idx, а условие завершения теперь включает как R, так и N:

template <
    typename F, size_t L,
    size_t R = 1, size_t N = 0, bool = (R < 2 * L && N < L)
>
struct idx //...

Весь код приведен в этом новом живом примере.

Этот подход, по-видимому, сокращает огромное количество ненужных комбинаций специализаций для R. Время компиляции и размеры исполняемых файлов резко сокращаются. Например, я смог скомпилировать за 10,7/18,7 секунд с помощью Clang/GCC для L = 1<<12 (4096), получив исполняемый файл размером 220/239 КБ. В этом случае nm -C показывает 546/250 версий operator().

person iavr    schedule 21.02.2014