Саморазворачивающийся макроцикл в C/C++

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

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

То, что я представляю, выглядит примерно так:

#define LOOP_N_TIMES(N, CODE) <insert magic here>

Чтобы я мог заменить for (int i = 0; i < N, ++i) { do_stuff(); } на:

#define INNER_LOOP_COUNT 4
LOOP_N_TIMES(INNER_LOOP_COUNT, do_stuff();)

И разворачивается в:

do_stuff(); do_stuff(); do_stuff(); do_stuff();

Поскольку препроцессор C все еще остается для меня загадкой большую часть времени, я понятия не имею, как это сделать, но я знаю, что это должно быть возможно, потому что Boost, похоже, имеет BOOST_PP_REPEAT макросов. К сожалению, я не могу использовать Boost для этого проекта.


person Karsten    schedule 30.01.2015    source источник
comment
Я использую модифицированную версию GCC для архитектуры, над которой работаю. Так что думаю технически да.   -  person Karsten    schedule 30.01.2015
comment
Вы смотрели -funroll-loops ?   -  person dmg    schedule 30.01.2015
comment
Компилятор не разворачивает этот цикл независимо от того, для чего я его настроил. Примечание: я всегда хочу знать, как это можно сделать в образовательных целях, а не только для этого конкретного случая.   -  person Karsten    schedule 30.01.2015
comment
Почему вы не можете использовать Boost для этого? Если это по техническим причинам (что кажется маловероятным), то я сомневаюсь, что вы вообще сможете это сделать. В конце концов, Boost PP — это библиотека только для заголовков, если я правильно понял. По крайней мере, вы сможете посмотреть в Boost, как это можно сделать самостоятельно.   -  person user694733    schedule 30.01.2015
comment
@ user694733: Я не могу использовать Boost, потому что у проекта не должно быть никаких зависимостей. Я посмотрел исходный код BOOST_PP_REPEAT, и похоже, что он примерно такой же, как и большинство предлагаемых решений. Я надеялся, что будет более общее решение, но я полагаю, что это невозможно, потому что вы не можете писать рекурсивные макросы...   -  person Karsten    schedule 30.01.2015
comment
Возможный дубликат Запись цикла while в препроцессоре C   -  person Cristian Ciupitu    schedule 27.04.2018


Ответы (5)


Вы можете использовать шаблоны для развертывания. См. разборку для примера Live on Godbolt

введите здесь описание изображения

Но -funroll-loops имеет тот же эффект для этого образца.


Жить на Coliru

template <unsigned N> struct faux_unroll {
    template <typename F> static void call(F const& f) {
        f();
        faux_unroll<N-1>::call(f);
    }
};

template <> struct faux_unroll<0u> {
    template <typename F> static void call(F const&) {}
};

#include <iostream>
#include <cstdlib>

int main() {
    srand(time(0));

    double r = 0;
    faux_unroll<10>::call([&] { r += 1.0/rand(); });

    std::cout << r;
}
person sehe    schedule 30.01.2015
comment
Показывая, как это сделать без злых МАКРОСОВ. И показать, что -funroll-loops способен на то же самое (в зависимости от кода) - person sehe; 30.01.2015
comment
Определенно интересный подход, но я сомневаюсь, что мой компилятор его поддерживает. Я отчитаюсь, когда попробую. - person Karsten; 30.01.2015
comment
Только что проверил: получил ошибку компилятора «ожидаемое первичное выражение перед токеном [». Поэтому я думаю, что не могу использовать это, хотя это кажется наиболее общим подходом. - person Karsten; 30.01.2015
comment
@Karsten включите c++11 (или c++0x), если он есть в вашем компиляторе. Все компиляторы с ~ 2010 года должны делать - person sehe; 30.01.2015
comment
К сожалению, я привязан к компилятору, который сейчас использую, и он не поддерживает С++ 11. - person Karsten; 30.01.2015
comment
Тем не менее я принимаю этот ответ, потому что в общем случае это кажется лучшим решением. - person Karsten; 30.01.2015
comment
@Karsten добавьте -ftree-vectorize, и вы также можете получить векторизацию! - person Mgetz; 30.01.2015
comment
Хм, производительность достаточно критична, чтобы опускаться до макрохаков, но недостаточно критична, чтобы компилировать даже один исходный код более новым компилятором. - person Potatoswatter; 30.01.2015
comment
многие процессоры имеют программный кеш, если вы сохраняете область исполняемого цикла в пределах кеша (даже лучше, в пределах одной строки кеша), то вы устраняете любые выборки памяти инструкций (и с осторожностью можете даже иметь возможность исключить любые обращения к памяти данных) Это (в общем) приведет к самому быстрому выполнению кода. затем некоторое развертывание цикла (с учетом вышеуказанных целей) устранит (или сведет к минимуму) код, выполняемый для проверки выхода из цикла и возврата к началу цикла. - person user3629249; 01.02.2015
comment
продолжение .. некоторые процессоры имеют достаточно большой конвейер, чтобы весь цикл мог содержаться в конвейере. Тогда даже к кешу не обращаются. Это может привести к отсутствию состояний ожидания и остановок конвейера. Результатом является максимально возможная скорость выполнения для процессора. - person user3629249; 01.02.2015
comment
@ user3629249: Я знаю, что это верно для большинства процессоров (особенно x86), но процессор, с которым я работаю, представляет собой специальный ASIP без предсказания ветвлений. Поэтому КАЖДАЯ итерация требует как минимум 3 дополнительных цикла (приращение, ветвление и 1 остановка конвейера из-за ветки). Если, как в моем случае, цикл содержит только от 1 до 8 инструкций, это значительное замедление. - person Karsten; 05.02.2015
comment
@Potatoswatter: я не могу использовать более новый компилятор, потому что для моего процессора его нет. - person Karsten; 05.02.2015

Вы можете использовать препроцессор и поэкспериментировать с конкатенацией токенов и расширением нескольких макросов, но вам придется жестко закодировать все возможности:

#define M_REPEAT_1(X) X
#define M_REPEAT_2(X) X X
#define M_REPEAT_3(X) X X X
#define M_REPEAT_4(X) X X X X
#define M_REPEAT_5(X) X M_REPEAT_4(X)
#define M_REPEAT_6(X) M_REPEAT_3(X) M_REPEAT_3(X)

#define M_EXPAND(...) __VA_ARGS__

#define M_REPEAT__(N, X) M_EXPAND(M_REPEAT_ ## N)(X)
#define M_REPEAT_(N, X) M_REPEAT__(N, X)
#define M_REPEAT(N, X) M_REPEAT_(M_EXPAND(N), X)

А затем расширить его следующим образом:

#define THREE 3

M_REPEAT(THREE, three();)
M_REPEAT(4, four();)
M_REPEAT(5, five();)
M_REPEAT(6, six();)

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

#define COUNT (N + 1)

M_REPEAT(COUNT, stuff();)
person M Oehm    schedule 30.01.2015
comment
Какая польза от макросов THREE и COUNT? - person harper; 30.01.2015
comment
@harper: Если у вас есть много циклов, которые нужно развернуть таким образом, и все они используют одно и то же количество повторений, у вас может быть одно глобальное определение. Когда вы настраиваете свою производительность, вам просто нужно изменить счет в одном месте. Это место может быть даже внешним, то есть параметрами компилятора или Makefile. Но в основном я включил его, потому что ОП использовал эту настройку в своем примере. - person M Oehm; 30.01.2015
comment
Ах я вижу. Это не имеет ничего общего с конкретными макросами, но всегда следует сокращать использование числовых литералов и заменять их символами. - person harper; 30.01.2015
comment
Вы используете многочисленные расширения. Достаточно двух расширений: #define M_REPEAT_(N, X) M_REPEAT ## N(X) + #define M_REPEAT(N, X) M_REPEAT_(N, X), если вы хотите использовать другой макрос в качестве счетчика. - person Knut; 28.04.2016

Стандартного способа сделать это нет.

Вот немного чокнутый подход:

#define DO_THING printf("Shake it, Baby\n")
#define DO_THING_2 DO_THING; DO_THING
#define DO_THING_4 DO_THING_2; DO_THING_2
#define DO_THING_8 DO_THING_4; DO_THING_4
#define DO_THING_16 DO_THING_8; DO_THING_8
//And so on. Max loop size increases exponentially. But so does code size if you use them. 

void do_thing_25_times(void){
    //Binary for 25 is 11001
    DO_THING_16;//ONE
    DO_THING_8;//ONE
    //ZERO
    //ZERO
    DO_THING;//ONE
}

Не так уж много можно попросить оптимизатора устранить мертвый код. В таком случае:

#define DO_THING_N(N) if(((N)&1)!=0){DO_THING;}\
    if(((N)&2)!=0){DO_THING_2;}\
    if(((N)&4)!=0){DO_THING_4;}\
    if(((N)&8)!=0){DO_THING_8;}\
    if(((N)&16)!=0){DO_THING_16;}
person Persixty    schedule 30.01.2015

Вы не можете использовать конструкцию #define для вычисления «счетчика развертывания». Но с достаточным количеством макросов вы можете определить это:

#define LOOP1(a) a
#define LOOP2(a) a LOOP1(a)
#define LOOP3(a) a LOOP2(a)

#define LOOPN(n,a) LOOP##n(a)

int main(void)
{
    LOOPN(3,printf("hello,world"););
}

Протестировано с VC2012

person harper    schedule 30.01.2015

Вы не можете писать настоящие рекурсивные инструкции с макросами, и я почти уверен, что вы не можете иметь настоящую итерацию в макросах.

Однако вы можете взглянуть на Порядок. Хотя он полностью построен на основе препроцессора C, он «реализует» функции, подобные итерациям. На самом деле он может иметь до N итераций, где N — некоторое большое число. Я предполагаю, что это похоже на «рекурсивные» макросы. В любом случае, это настолько пограничный случай, что немногие компиляторы его поддерживают (хотя GCC — один из них).

person dmg    schedule 30.01.2015