Правило ограничения в циклах

Большая часть времени исполнения программы приходится на циклы: это могут быть вычисления, прием и обработка информации и т.д. Правильное применение техник оптимизации циклов позволит увеличить скорость работы программы. Но прежде, чем приступать к оптимизациям необходимо выделить «узкие» места программы и попытаться найти причины падения быстродействия.
Время исполнения кода в циклах зависит от организации памяти, архитектуры процессора, в том числе, поддерживаемого набора инструкций, конвейеров, кэшей и опыта программиста.
Рассмотрим некоторые методы оптимизаций циклов: развертка циклов (loop unrolling), объединение циклов (loop fusion), разрезание циклов (loop distribution), выравнивание циклов (loop alignment), перестановка циклов (loop interchange), разделение на блоки (loop blocking).
Перед применением какой-либо оптимизации сделайте самое простое: вынесите из цикла все переменные, которые в нем не изменяются.
Какие причины могут привести к уменьшению скорости работы программы в циклах?
- Итерации цикла зависимы и не могут исполняться параллельно.
- Тело цикла большое и требуется слишком много регистров.
- Тело цикла или количество итераций мало и выгоднее совсем отказаться от использования цикла.
- Цикл содержит вызовы функций и процедур из сторонних библиотек.
- Цикл интенсивно использует какое-то одно исполняющее устройство процессора.
- В цикле имеются условные переходы.
Развертка циклов
Такая оптимизация выполняется, когда тело цикла мало. Необходимо более эффективно использовать исполняющие устройства на каждой итерации. Поэтому многократно дублируют тело цикла в зависимости от количества исполняющих устройств. Но такая оптимизация может вызвать зависимость по данным, чтобы от нее избавиться вводятся дополнительные переменные.
До | После | После №2 |
for (int i = 0; i < iN; i++){ res *= a[i]; } | for (int i = 0; i < iN; i+=3){ res *= a[i]; res *= a[i+1]; res *= a[i+2]; } | for (int i = 0; i < iN; i+=3){ res1 *= a[i]; res2 *= a[i+1]; res3 *= a[i+2]; } res = res1 * res2 * res3; |
В gcc можно применить следующие ключи: -funroll-all-loops -funroll-loops.
Объединение циклов
В цикле может быть долго выполняющиеся инструкции, например, извлечение квадратных корней. Или есть несколько циклов, которые выполняются по одинаковому интервалу индексов. Поэтому целесообразно объединить циклы для более сбалансированной нагрузки исполняющих устройств.
До | После |
for(int i = 0; i < iN; i++){ a[i] = b[i] – 5; } for(int i = 0; i < iN-1; i++){ d[i] = e[i] * 3; } | for(int i = 0; i < iN-1; i++){ a[i] = b[i] – 5; d[i] = e[i] * 3; } a[iN-1] = b[iN-1] – 5; |
Разрезание циклов
Данная оптимизация применяется, когда тело цикла большое и переменным не хватает регистров. Поэтому данные сначала вытесняются в кэш, а если совсем все плохо, то и в оперативную память. А доступ к оперативной памяти занимает ~300 тактов процессора, а доступ к L2 всего ~10. Доступ к памяти с большим шагом еще больше замедляет программу. Оптимально «ходить» по памяти с шагом 2n, где n – достаточно маленькое число (<7).
До | После |
for (int j = 0; j < jN; j++){ for (int k = 0; k < kN; k++){ for (int m = 0; m < mN; m++){ i = j * k + m; a[i] = b[i] * c[i] + f[i]/e[i]+ x[i] – y[i] + z[i]/r[i] + d[i] * x[i]; } } } | for (int j = 0; j < jN; j++){ for (int k = 0; k < kN; k++){ double tmp; for (int m = 0; m < mN; m++){ i = j * k + m; tmp = b[i] * c[i] + f[i]/e[i]; a[i] = tmp – y[i] + z[i]/r[i] + (d[i] + 1) * x[i]; } } } |
Перестановка циклов
Во вложенных циклах важен порядок вложения. Поэтому необходимо помнить как хранятся массивы в памяти. Классический пример: c/c++ хранят матрицы построчно, а fortran – по столбцам.
До | После |
for(int i = 0; i < iN; i++){ for(int j = 0; j < jN; j++){ for(int k = 0; k < kN; k++){ c[i][j] = c[i][j] + a[i][k] * b[k][j]; } } } | for(int i = 0; i < iN; i++){ for(int k = 0; k < kN; k++){ for(int j = 0; j < jN; j++){ c[i][j] = c[i][j] + a[i][k] * b[k][j]; } } } |
Теперь обращения к массиву a идут последовательно.
Разделение циклов на блоки
Если тело цикла сложное, то можно применить эту оптимизацию для более лучшего расположения данных в памяти и улучшения использования кэшей. Результат оптимизации сильно зависит от архитектуры процессора.
До | После |
for(int i = 0; i < iN; i++){ for(int j = 0; j < jN; j++){ a[i][j] = a[i][j] + b[i][j]; } } | // размер блоков зависит от размера исходных массивов int iBlk, jBlk; for(int k = 0; k < iN/iBlk; k++){ for(int m = 0; m < jN/jBlk; m++){ for(int i = k * iBlk; i < ((k + 1) * iBlk); i++){ for(int j = m * jBlk; j < ((m + 1) * jBlk); j++){ a[i][j] = a[i][j] + b[i][j]; } } } } |
Примерно по такому принципу работает технология MPI: делит большие массивы на блоки и рассылает отдельным процессорам.
Разрешение зависимостей
Лучшее решение – избавиться. Но не со всеми зависимостями это получится.
for (int i = 1; i < N; i++){
a[i] = a[i-1] + 1;
}
Для этого примера лучше применить развертку, т.к. результат вычислений будет оставаться на регистрах. Но большинство таких циклов не могут быть полностью оптимизированы (или распараллелены), результат все равно зависит от предыдущего витка цикла.
Чтобы проверить цикл на независимость, измените направление движения в цикле на обратное. Если результат вычислений не изменился, то итерации цикла – независимы.
Относительно условных переходов
Потери времени возникают из-за ошибок в предсказании переходов, т. к. приходиться откатывать конвейер. Поэтому лучше всего отказаться от условных конструкций. Но, если это невозможно нужно постараться облегчить работу модулю предсказания переходов. Для этого разместите наиболее вероятные ветви в начале ветвления и, если возможно, вынесите условные конструкции за пределы цикла.
Вместо заключения
Если Вы создаете сложную программу, которая будет занимать много процессорного времени, то
- Ознакомтесь с архитектурой процессора (узнайте сколько и каких исполняющих устройств у него есть, сколько конвейеров, размеры кэшей L1 и L2).
- Попробуйте компилировать программу разными компиляторами и с различными ключами.
- Учитывайте влияние операционной системы.
Также советую ознакомиться с этой статьей.
По своему опыту могу сказать, что грамотное применение оптимизаций может улучшить быстродействие программы в разы.
Если хотите сами потренироваться в оптимизации, то попробуйте вычислить число Пи:
Ниже приведен «плохой» код.
long N = 10000000;
double dx, sum, x;
sum = 0.0;
x = 0.0;
dx = 1.0 / (double) N;
for (long i = 0; i < N; i++){
sum += 4.0 / (1.0 + x * x);
x += dx;
}
double pi = dx * sum;
О чем я не рассказал: о векторизации вычислений (инструкции SSE); о prefetch’ах, облегчающих работу с памятью. Если будут люди «которым интересно», то напишу отдельную статью про векторизацию циклов.
Подсветка исходных кодов Source Code Highlighter.
Источник
|
Источник
1. Участникам с незаполненным профилем необходимо сначала заполнить профиль!
Перед вами игровой список.
Список поделен на части: в первой части – циклы, во второй издательские и авторские серии.
Под Циклом будем подразумевать собрание трех или более книг, связанных одними героями, или сюжетом, или какими-либо общими реалиями, и написанных одним автором или группой авторов.
Под Сериями будем подразумевать реальные издательские и авторские серии, объединенные одной темой и общим дизайном (оформление, обложка).
2. Каждый желающий подаёт заявку в специальной теме
Заявки. Участник имеет право выбрать, по какому типу игры он хочет читать книги в “Цикломании”.
2.1. Первый Тип – для самых смелых и азартных!
2.1.1 Участник читает только циклы или только серии (не меньше трех)
Если участник берет три цикла (серии), то все они выбираются генератором случайных чисел.
Пример заявки:
1. Первый тип игры
2. Читаю циклы (серии)
3. Прошу разыграть три пункта
Если участник берет от пяти циклов (серий) и более, то у него есть право на самостоятельный выбор пунктов, по своему желанию:
2-х пунктов из 5-ти , или 3-х пунктов из 10-ти, или 4-х пунктов из 15-ти, или, соответственно, 5-ти пунктов из 20-ти, и так далее.
Максимальное количество циклов (серий) – 20
Остальные пункты выбираются генератором случайных чисел.
Участник имеет право указать ограничение по жанрам (не больше одного, если читается от 5 до 10 пунктов, два – если берется больше 10 пунктов) или написать, что играет без ограничений.
Правило ограничения по жанру действует только на те циклы и серии, где жанровая принадлежность точно определена. Для серий, куда входят книги смешанных жанров – правило не действует.
Если участнику выпадет пункт, попадающий под ограничение, то его можно заменить на любой другой. Если выпадают несколько или все пункты, попадающие под ограничение – что ж, тогда участнику повезло – он читает только те циклы или серии, которые выбирает сам.
Если выпадает цикл (серия), который участник уже полностью прочитал, то производится замена пункта на выбор участника.
Участник имеет также право на один необоснованный отказ – то есть если выпавший пункт совсем не хочется читать, можно заменить его любым другим, но это правило действует только в том случае, если участник берет 10 и более пунктов.
Примеры подачи заявки:
1. Первый тип игры
2. Читаю циклы (серии)
3. Беру 5 пунктов
4. Самостоятельно выбираю:
4.1. номер и название пункта
4.2 номер и название пункта
5. Ограничение по жанрам: например, фантастика (или – играю без ограничений)
Куратор публикует скриншот в ответ на заявку участника. Вы находите в списке соответствующие числам пункты.
2.1.2 Если участник хочет читать и циклы и серии нужно подать две заявки
2.2 Второй тип игры – для тех, кто не любит рисковать и не доверяет генератору случайных чисел!
Участник в этом типе игры сам выбирает себе пункты для чтения. На каждый пункт читается одна книга.
Основное условие – взять нужно не менее пяти пунктов, то есть прочитать не менее пяти книг.
Пример подачи заявки
1. Второй тип игры
2. Читаю циклы (серии)
3. Беру 5 пунктов (и более) самостоятельно:
3.1 номер и название пункта 3.2…и так далее
Если участник хочет читать и циклы и серии – подается две заявки
2.3. Третий тип игры – для тех, кто хочет читать циклы (серии) от начала до конца
Участник в этом типе игры выбирает себе самостоятельно цикл или серию и читает книги именно из данного цикла(серии)
Условие одно – прочитанных по такому типу игры книг должно быть не меньше трех от каждого цикла или серии.
Количество циклов (серий), которые участник будет читать – не ограничено.
Пример подачи заявки
1. Тип игры третий
2. Читаю 1 (или 2, 3 и так далее) циклов (серий)
3. Беру …номер и название пункта
Если участник хочет читать и циклы и серии – подается две заявки
Когда участник прочитал то количество книг, которое хотел (но не меньше трех по каждому выбранному циклу или серии), он может при подаче отчета указать, что игру по данной заявке закончил. Когда остановиться в этом типе Цикломании участник решает сам.
3. Условный срок чтения книг по заявке – один год.
4. Подавать новую заявку можно только после закрытия (т.е. прочтения книг и отчетов) по предыдущей.
5. Книги, читаемые в Цикломании, участники могут совмещать с другими проектами сайта.
6. Отчеты подаются в специальной теме
Прочитанное (цикломания)
Пример подачи отчета:
1. Номер участника (смотреть в списке участников)
2. Тип игры
2. Пункт, под который прочитана книга (название цикла(серии))
3. Автор (имя, фамилия) “Название книги,”
4. Рецензия/Отзыв: (ссылка)
7. Участники могут одновременно играть сразу во все типы игры
Если участник подает несколько заявок, их нужно пронумеровать: Заявка №1, Заявка №2 и так далее…
Если участник закончил игру по заявке (по выбранному типу игры) и хочет играть дальше, он при подаче новой заявки должен указать, что заходит на второй круг
Повторять пункты самостоятельного выбора при подаче заявок по разным типам игры – нельзя!
Для вопросов, обсуждений, пожеланий для вас открыта Флудилка цикломанов!
Источник
Я иду на риск, публикуя это, но я думаю, что ответ:
между 550 и 575
с настройками по умолчанию в Visual Studio 2015
я создал небольшую программу, которая генерирует вложенные for петли…
for (int i0=0; i0<10; i0++) { for (int i1=0; i1<10; i1++) { … … for (int i573=0; i573<10; i573++) { for (int i574=0; i574<10; i574++) { Console.WriteLine(i574); } } … … } }
для 500 вложенных циклов программа все еще может быть скомпилирована. С 575 петлями компилятор выручает:
предупреждение анализатор AD0001 – Microsoft.CodeAnalysis.CSharp.Диагностика.SimplifyTypeNames.CSharpSimplifyTypeNamesDiagnosticanalyzer ‘бросил исключение типа’ системы.InsufficientExecutionStackException ‘with message’ недостаточно стека для безопасного продолжения выполнения программы. Это может произойти из-за слишком большого количества функций в стеке вызовов или функции в стеке, использующей слишком много пространства стека.’.
с базовым сообщением компилятора
ошибка CS8078: выражение слишком долго или сложно компилировать
конечно, это чисто гипотетически результат. Если самый внутренний цикл делает больше, чем Console.WriteLine, то меньше вложенных циклов может быть возможно до превышения размера стека. Кроме того, это не может быть строго техническим ограничением, в том смысле, что могут быть скрытые настройки для увеличения максимального размера стека для “анализатора”, который упоминается в сообщении об ошибке, или (при необходимости) для результирующего исполняемого файла. Этот часть ответа, однако, оставлена людям, которые знают C# в глубине.
в ответ на вопрос в комментариях:
мне было бы интересно увидеть этот ответ расширен, чтобы “доказать” экспериментально, можно ли поместить 575 локальных переменных в стек, если они не используется в for-loops, и/или можно ли поставить 575 невложенной for-петли в одном функция
в обоих случаях, ответ: Да, это возможно. При заполнении метода 575 автоматически генерируемых операторов
int i0=0; Console.WriteLine(i0); int i1=0; Console.WriteLine(i1); … int i574=0; Console.WriteLine(i574);
он все еще может быть скомпилирован. Все остальное удивило бы меня. Размер стека, который требуется для int переменные – это всего 2,3 КБ. Но мне было любопытно, и чтобы проверить дальнейшие пределы, я увеличил это число. В конце концов, это произошло не компиляции вызывает ошибку
ошибка CS0204: разрешено только 65534 локальных объекта, включая созданные компилятором
это интересный момент, но уже было замечено в другом месте: максимальное количество переменных в методе
точно так же, 575 невложеннойfor-петли, как в
for (int i0=0; i0<10; i0++) { Console.WriteLine(i0); } for (int i1=0; i1<10; i1++) { Console.WriteLine(i1); } … for (int i574=0; i574<10; i574++) { Console.WriteLine(i574); }
также может быть скомпилирован. Здесь я также попытался найти предел и создал больше этих циклов. В частности, я не был уверен будут ли переменные цикла в этом случае также считать als “локальными”, потому что они находятся в своих собственных { block }. Но все же, более 65534 не представляется возможным. Наконец, я добавил тест, состоящий из 40000 петель узора
for (int i39999 = 0; i39999 < 10; i39999++) { int j = 0; Console.WriteLine(j + i39999); }
который содержал дополнительную переменную на цикл, но они, похоже, также считаются “локальными”, и это не удалось скомпилировать.
Итак, чтобы подвести итог: предел ~550 действительно вызван глубина вложенности из петель. Это также было указано в сообщении об ошибке
ошибка CS8078: выражение слишком длинное или сложное для компиляции
The документация ошибки CS1647 к сожалению (но понятно) не указывает на “измерение” сложности, а только дает прагматический совет
в компиляторе, обрабатывающем ваш код, произошло переполнение стека. Чтобы решить эту проблему ошибка, упростите свой код.
чтобы подчеркнуть это еще раз: для частного случая глубоко вложенных for-петли, все это довольно академично и гипотетических. Но webing для сообщения об ошибке CS1647 показывает несколько случаев, когда эта ошибка появилась для кода, который, скорее всего, не был преднамеренно сложным, но создавался в реалистичных сценариях.
Источник