Перестановка условной оценки ускоряет цикл?

Немного странное: мне недавно рассказал друг, который переупорядочивает этот пример for loop из:

for(int i = 0; i < constant; ++i) {
    // code...
}

в

for(int i = 0; constant > i; ++i) {
    // code...
}

немного повысит производительность на С++. Я не вижу, как сравнение постоянного значения с переменной быстрее, чем наоборот, и некоторые рудиментарные тесты, которые я выполнял, не отображали никакой разницы в скорости между двумя реализациями. То же самое можно сказать и о тестировании этого цикла Python while:

while i < constant:
    # code...
    i += 1

против

while constant > i:
    # code...
    i += 1

Неужели я ошибаюсь? Являются ли мои простые тесты недостаточными для определения изменения скорости? Это касается других языков? Или это просто новая лучшая практика?

Ответ 1

Это больше в линейке фольклора С++, ручной микрооптимизации, которая работала некогда на конкретной версии конкретного компилятора и передавалась после того, как какое-то знание, отличающее владельца от общего стада. Это мусор. Профилирование - это правда.

Ответ 2

Вероятно, нет, но если это так, то компилятор, вероятно, автоматически сделает оптимизацию для вас. Так что сделайте все, чтобы ваш код был наиболее читаемым.

Ответ 3

Мое подозрение - ваш друг на 100% ошибочен. Но я больше не буду доверять своему мнению, чем буду доверять твоему другу. На самом деле, если есть проблема с производительностью, вам следует доверять только одному человеку.

Профайлер

Это только способ, которым вы когда-либо можете претендовать с любыми полномочиями, которые один или не быстрее другого.

Ответ 4

Приведенные вами примеры не должны иметь абсолютно никакой разницы в производительности на С++, и я сомневаюсь, что они будут отличаться и на Python.

Возможно, вы путаете его с другой оптимизацией:

for (int i = 0; i < variable; ++i)

// ...vs...

for (int i = variable; i ; --i)

Последнее происходит быстрее в некоторых архитектурах, потому что в результате действия декремента переменной устанавливается флаг нуля, который затем может быть проверен в команде "jump-if-no-zero", давая вам итерацию цикла и условное выражение за один раз, В предыдущем примере нужно выполнить явное сравнение или вычитание для установки флага, а затем перейти на него.

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

Ответ 5

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

while(bContinue && QueryStatusFromDatabase==1){
}  //while

Было бы намного быстрее, чем:

while(QueryStatusFromDatabase==1 && bContinue){
}  //while

Даже если они логически идентичны.

Это потому, что первый может остановиться, как только простое логическое значение FALSE - запрос должен выполняться только тогда, когда логическое значение имеет значение ИСТИНА, но второй всегда будет запускать запрос.

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

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

Ответ 6

В то время как профилирование является лучшим, это не способ только.

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

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

Ответ 7

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

Ответ 8

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

Ответ 9

Сравнение с 0 очень быстро, поэтому это будет немного быстрее:

for (int i = constant; i > 0; --i)
{ 
  //yo
}

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

Ответ 10

Сегодня, на хорошем компиляторе, совсем нет.

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

Мы не должны слепо отклонять работу. Реактивность все еще имеет значение, равно как и расчетное время. Особенно при написании кода библиотеки вы не знаете, когда вас назовут два миллиона раз подряд.

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

На настольных/серверных платформах вес сдвигается в сторону хорошо инкапсулированной сложности, которая реализует алгоритмы масштабирования.

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


Было время, когда конец цикла в нуле (например, путем подсчета) на x86 фактически мог дать заметные улучшения для жестких циклов, поскольку DEC CX/JCXNZ был быстрее (он все же потенциально мог быть, поскольку он мог бы сохранить регистр/доступ к памяти для сравнения, а оптимизация выполнения компилятора обычно находится за пределами этого времени). То, что слышал ваш друг, может быть измененной версией этого.

Ответ 11

Я смиренно предлагаю, чтобы на некоторых компиляторах на некоторых архитектурах следующее могло быть более эффективным, чем варианты:

i = constant - 1
while (--i) {
}

Получить постоянные итерации.

Как и многие из комментариев, компилятор будет хорошо работать над оптимизацией цикла для вас (люди, оптимизирующие компилятор, потратили много и много времени на размышления об этом). Разумный код, вероятно, более ценен, но YMMV!

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

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

Надеюсь, что это помогает и приветствует.

Ответ 12

Представленная оптимизация оптимизировала бы только для данного компилятора (возможно). Таким образом, он должен генерировать один и тот же код.

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

Например, я ++ может быть быстрее я + 1. Зависит. В наивных процессорах равенство 0 намного быстрее, чем меньше. Если ваш компилятор/процессор не поддерживает переупорядочение команд, вы можете обнаружить, что перемежающиеся назначения с вычислениями ускоряют ваш код вверх. (некоторые вычисления могут вызывать конвейерные стойки). Но что-то вам нужно будет конкретно определить для вашей комбинации компилятора/архитектуры.

Честно говоря, я бы не стал заниматься этим уровнем оптимизации, если я абсолютно не нуждался в каждом последнем цикле от моего процессора. Традиционно графика или научное вычисление - это то, где вам нужен этот материал [*].

* Я знаю программу, которая после нескольких месяцев оптимизации и на современных машинах все еще занимает много месяцев, чтобы обрабатывать данные. Сроки для одного набора данных находятся в диапазоне недель. Существует немало данных для использования....

Ответ 13

Это абсолютно случай микро-оптимизации и действительно не нужно делать.

Верно, что (особенно) в С++ существует небольшая разница в производительности между операцией post-increment и пред-инкрементной операцией, но эта разница в сегодняшних компиляторах вообще незначительна. Причина изменения порядка условного обусловлена ​​изменением от пост-до предварительного приращения.