Я получил эту идею из этого вопроса на stackoverflow.com
Следующий шаблон является распространенным:
final x = 10;//whatever constant value
for(int i = 0; i < Math.floor(Math.sqrt(x)) + 1; i++) {
//...do something
}
Суть, которую я пытаюсь сделать, заключается в том, что условное утверждение является чем-то сложным и не меняется.
Это лучше объявить в разделе инициализации цикла, как таковой?
final x = 10;//whatever constant value
for(int i = 0, j = Math.floor(Math.sqrt(x)) + 1; i < j; i++) {
//...do something
}
Это более понятно?
Что делать, если условное выражение простое, такое как
final x = 10;//whatever constant value
for(int i = 0, j = n*n; i > j; j++) {
//...do something
}
java
c++
coding-style
language-agnostic
Celeritas
источник
источник
x
велико по величине,Math.floor(Math.sqrt(x))+1
равноMath.floor(Math.sqrt(x))
. :-){ x=whatever; for (...) {...} }
или, еще лучше, подумайте, достаточно ли происходит, что это должна быть отдельная функция.Ответы:
Я бы сделал что-то вроде этого:
Честно говоря, единственная веская причина, чтобы втиснуть инициализацию
j
(сейчасlimit
) в заголовок цикла, состоит в том, чтобы держать его правильно. Все, что нужно для того, чтобы не создавать проблем, - это хороший ограниченный объем.Я могу ценить желание быть быстрым, но не жертвую удобочитаемостью без реальной уважительной причины.
Конечно, компилятор может оптимизировать, инициализация нескольких переменных может быть допустимой, но циклы достаточно сложны для отладки, как есть. Пожалуйста, будь добр к людям. Если это действительно замедляет нас, хорошо бы понять это достаточно, чтобы это исправить.
источник
for(int i = 0; i < n*n; i++){...}
вы бы не присвоилиn*n
переменную, не так ли?final
). Кого волнует, доступна ли константа с принудительным применением компилятора, предотвращающим ее изменение, позже в функции?Хороший компилятор будет генерировать один и тот же код в любом случае, поэтому, если вы стремитесь к производительности, вносите изменения только в том случае, если он находится в критическом цикле, и вы фактически его профилировали и обнаружили, что он имеет значение. Даже если компилятор не может оптимизировать его, как отмечали в комментариях о случае с вызовами функций, в подавляющем большинстве ситуаций разница в производительности будет слишком мала, чтобы ее стоило учитывать программисту.
Тем не мение...
Мы не должны забывать, что код - это прежде всего средство общения между людьми, и оба ваших варианта не очень хорошо общаются с другими людьми. Первое создает впечатление, что выражение необходимо вычислять при каждой итерации, а второе, находящееся в разделе инициализации, подразумевает, что оно будет обновляться где-то внутри цикла, где оно действительно постоянно.
Я бы на самом деле предпочел, чтобы его вытащили над петлей и сделали
final
чтобы это было сразу и совершенно понятно каждому, кто читает код. Это также не идеально, потому что это увеличивает область действия переменной, но в любом случае ваша включающая функция не должна содержать намного больше, чем этот цикл.источник
Как сказал @Karl Bielefeldt в своем ответе, обычно это не проблема.
Однако когда-то это было распространенной проблемой в C и C ++, и уловка была в том, чтобы обойти эту проблему без снижения читабельности кода - повторять в обратном направлении, вплоть до
0
.Теперь условием каждой итерации является то,
>= 0
что каждый компилятор будет компилироваться в 1 или 2 инструкции по сборке. Каждый процессор, созданный за последние несколько десятилетий, должен иметь такие базовые проверки; делая быструю проверку на моей машине x64, я вижу, что это предсказуемо превращается вcmpl $0x0, -0x14(%rbp)
(значение long-int-сравнения 0 против смещения регистра rbp -14) иjl 0x100000f59
(переход к инструкции, следующей за циклом, если предыдущее сравнение было верным для «2nd-arg» <1-й аргумент ”) .Обратите внимание, что я удалил
+ 1
изMath.floor(Math.sqrt(x)) + 1
; чтобы математика сработала, начальное значение должно бытьint i = «iterationCount» - 1
. Также стоит отметить, что ваш итератор должен быть подписан;unsigned int
не будет работать и, скорее всего, предупредит компилятор.После программирования на языках C в течение ~ 20 лет я теперь пишу только циклы с итерацией обратного индекса, если нет особой причины для итерации с обратным индексом. В дополнение к более простым проверкам в условных выражениях, обратная итерация часто также обходит стороной то, что иначе было бы проблематично с мутациями массива во время итерации.
источник
unsigned
счетчики будут работать здесь, если вы измените проверку (самый простой способ - добавить одинаковое значение в обе стороны); например, для любого декрементаDec
проверка(i + Dec) >= Dec
всегда должна иметь тот же результат, что иsigned
проверкаi >= 0
, с обоимиsigned
иunsigned
счетчиками, если в языке есть четко определенные правила обтеканияunsigned
переменных (в частности,-n + n == 0
это должно быть верно для обоихsigned
иunsigned
). Обратите внимание, однако, что это может быть менее эффективно, чем подписанная>=0
проверка, если у компилятора нет оптимизации для него.Dec
константы как к начальному, так и к конечному значению работает, но делает ее гораздо менее интуитивно понятной, и если вы используете ееi
в качестве индекса массива, вам также потребуется сделатьunsigned int arrayI = i - Dec;
в теле цикла. Я просто использую прямую итерацию, когда застрял с неподписанным итератором; часто сi <= count - 1
условием держать логику параллельной циклам обратной итерации.Dec
начальных и конечных значений, а сдвиг проверки состояния сDec
обеих сторон.for (unsigned i = N - 1; i + 1 >= 1; i--) /*...*/
Это позволяет вамi
нормально использовать в цикле, в то же время гарантируя, что самое низкое возможное значение в левой части условия0
(чтобы не допустить мешающего переноса). Это , безусловно , намного проще использовать прямую итерацию при работе с неподписанными счетчиками, хотя.Это становится интересным после замены Math.sqrt (x) на Mymodule.SomeNonPureMethodWithSideEffects (x).
По сути, мой образ действия: если ожидается, что что-то всегда будет давать одно и то же значение, оцените его только один раз. Например, List.Count, если список не должен изменяться во время работы цикла, то получить значение за пределами цикла в другую переменную.
Некоторые из этих «подсчетов» могут быть на удивление дорогими, особенно когда вы имеете дело с базами данных. Даже если вы работаете с набором данных, который не должен изменяться во время итерации списка.
источник
for( auto it = begin(dataset); !at_end(it); ++it )
На мой взгляд, это сильно зависит от языка. Например, если использовать C ++ 11, я бы заподозрил, что если проверка условия была
constexpr
функцией, компилятор с большой вероятностью оптимизировал бы несколько выполнений, поскольку он знал, что каждый раз будет давать одно и то же значение.Однако, если вызов функции является библиотечной функцией, которая не
constexpr
является компилятором, он почти наверняка выполнит ее на каждой итерации, поскольку не может вывести это (если только он не является встроенным и, следовательно, может быть выведен как чистый).Я меньше знаю о Java, но, учитывая, что JIT скомпилирован, я бы предположил, что компилятор имеет достаточно информации во время выполнения, чтобы, вероятно, встроить и оптимизировать условие. Но это будет зависеть от хорошего дизайна компилятора, и компилятор, решивший этот цикл, был приоритетом оптимизации, о котором мы можем только догадываться.
Лично я чувствую, что немного более элегантно поместить условие в цикл for, если вы можете, но если оно будет сложным, я запишу его в функцию
constexpr
илиinline
, или ваши языки будут равносильны намеку на то, что функция чистая и оптимизируемая. Это делает намерение очевидным и поддерживает идиоматический стиль цикла без создания огромной нечитаемой строки. Это также дает проверке состояния имя, если это его собственная функция, поэтому читатели могут сразу увидеть логически, для чего проверка, не считывая ее, если она сложная.источник