Разница в производительности развертывания цикла C++ (проект Euler)

У меня есть вопрос о вопросе Project Euler и оптимизации с использованием развертывания цикла.

Описание задачи: 2520 — это наименьшее число, которое можно разделить на каждое из чисел от 1 до 10 без остатка. Какое наименьшее положительное число делится без остатка на все числа от 1 до 20?

Решение:

#include <iostream>
#include <limits.h>
#include <stdio.h>
#include <time.h>

using namespace std;

int main() {

    clock_t startTime = clock();

    for (int i = 1; i < INT_MAX; i++)
    {
        bool isDivisible = true;

        //CODE BLOCK #1
        /*for (int j = 2; j <= 20; j++)
        {
                if ( i % j != 0)
                {
                        isDivisible = false;
                        break;
                {
        }*/

        //CODE BLOCK #2
        /*if (i % 2 != 0 || i % 3 != 0 ||
                i % 4 != 0 || i % 5 != 0 ||
                i % 6 != 0 || i % 7 != 0 ||
                i % 8 != 0 || i % 9 != 0 ||
                i % 10 != 0 || i % 11 != 0 ||
                i % 12 != 0 || i % 13 != 0 ||
                i % 14 != 0 || i % 15 != 0 ||
                i % 16 != 0 || i % 17 != 0 ||
                i % 18 != 0 || i % 19 != 0 ||
                i % 20 != 0 )
                isDivisible = false;*/

        if (isDivisible)
        {
                cout << "smallest: " << i << endl;

                cout << "Ran in: " << clock() -  startTime  << " cycles" << endl;
                break;
        }
    }

return 0;
}

Теперь, комментируя БЛОК КОДА № 1 или БЛОК КОДА № 2, я получаю правильный ответ (232792560). Однако БЛОК КОДА № 2 намного быстрее, чем БЛОК КОДА № 1.

БЛОК КОДА № 1: 3 580 000 циклов (я только что добавил разрыв в БЛОК КОДА № 1, и он работает намного быстрее. Однако все же значительно медленнее, чем составной оператор IF.)

КОДОВЫЙ БЛОК № 2: 970 000 циклов

Кто-нибудь знает, почему может возникнуть такая огромная разница в производительности?


person Blaine    schedule 08.11.2013    source источник


Ответы (2)


Использование || означает, что как только одно из них истинно, остальные условия не вычисляются. Это будет эквивалентно циклу:

    for (int j = 2; j <= 20; j++)
    {
        if ( i % j != 0){
            isDivisible = false;
            break;
        }
    }

Если вы попробуете это, вы можете обнаружить, что разрыв во времени работы сократился. Любые другие различия могут быть связаны с накладными расходами на цикл, но с включенной оптимизацией в вашем компиляторе я подозреваю, что они будут работать с одинаковой скоростью (или, по крайней мере, иметь гораздо более похожее время).

EDIT О новой разнице в производительности:
Существует множество оптимизированных способов проверки делимости чисел на константы, например, для N любую степень 2 можно заменить на i % N != 0 на i & (N-1), существуют и другие, и не столь очевидны.
Компилятор знает множество этих маленьких хитростей и во втором блоке кода, вероятно, способен оптимизировать большинство, если не все эти проверки делимости (поскольку они написаны непосредственно вами), в то время как в первом коде блоке, он должен решить сначала развернуть циклы, а затем заменить переменную цикла константами, прежде чем можно будет даже рассуждать о разных проверках.
Возможно, это различие способствует лучшей оптимизации кода в блоке 2, чем в блоке 1.

person SirGuy    schedule 08.11.2013
comment
Ах, это имеет смысл. Я только что добавил разрыв в БЛОК КОДА № 1, и он работает намного быстрее. Однако все еще значительно медленнее, чем составной оператор IF. С заявлением об остановке: 3 580 000 циклов - person Blaine; 09.11.2013

3 580 000 против 970 000 — это не только накладные расходы на цикл...

В вашем последнем ядре, похоже, вы намеревались сохранить блок Up, Down и Square между другим циклом, но эти блоки являются «краткими» локальными, поэтому данные, которые они содержат, не распределяются между ветвями. К сожалению, ваш подход не сработает, даже если они будут разделены между ветвями.

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

Вам, вероятно, следует решить эту проблему с помощью существующих параллельных примитивов, таких как суммы префиксов, которые уже были оптимизированы. Например, в CUB или Thrust.

person zluk in the flesh    schedule 08.11.2013
comment
Этот ответ относится к другому вопросу? Я не вижу никаких массивов или потоков в опубликованном коде. - person Blastfurnace; 09.11.2013
comment
Не уверен, что вы имеете в виду. Я отвечаю на пост Project Euler. - person zluk in the flesh; 09.11.2013
comment
Даже после вашего редактирования я не уверен, как этот ответ применим. Что такое блок вверх, вниз и квадрат? Кроме того, вместо того, чтобы пытаться распараллелить этот код, существует тривиальное (и быстрое) решение с использованием наименьших общих кратных. - person Blastfurnace; 09.11.2013
comment
Расскажите, пожалуйста, о методе наименьших общих кратных. Я думаю, что накладные расходы на цикл не могут объяснить 4-кратное ускорение в исходном вопросе плакатов. Мой пример — печально известный вездесущий алгоритм, работающий за Θ(n!) раз. Я изучил новую методологию LCM для понимания избыточности, и контрольные суммы не могут служить этой цели. Я не вижу причин не использовать блокировку эвристического цикла для управления поиском INT. - person zluk in the flesh; 09.11.2013
comment
Вы можете увидеть мое решение здесь, на ideone.com. Это просто и быстро. - person Blastfurnace; 09.11.2013
comment
ОП не запрашивает алгоритм решения проблемы. Он нашел двоих. Он спрашивает, почему производительность такая разная. - person zluk in the flesh; 09.11.2013
comment
Это правильно, у меня нет проблем с правильным ответом, я просто хочу знать, почему количество циклов, необходимых для запуска кода, так сильно отличается. Пожалуйста, перечитайте вопрос Blastfurnace. - person Blaine; 09.11.2013
comment
@user988398: user988398: У меня нет для вас окончательного ответа (именно поэтому я не опубликовал ответ). Я предполагаю, что это один или несколько обычных подозреваемых: качество генерации кода компилятора, эффективное использование конвейеров ЦП или предсказание ветвлений. - person Blastfurnace; 09.11.2013