Какой из этих фрагментов кода быстрее в Java?
a) for(int i = 100000; i > 0; i--) {}
b) for(int i = 1; i < 100001; i++) {}
ответ есть на этот сайт (Вопрос 3). Я просто не могу понять почему? на сайте:
3. а
16 ответов
когда вы спускаетесь до самого низкого уровня (машинный код, но я буду использовать сборку, так как он отображает один к одному в основном), разница между пустым циклом, уменьшающимся до 0 и одним, увеличивающимся до 50 (например), часто составляет:
ld a,50 ld a,0
loop: dec a loop: inc a
jnz loop cmp a,50
jnz loop
это потому, что нулевой флаг в большинстве здравомыслящих процессоров устанавливается инструкцией декремента, когда вы достигаете нуля. То же самое обычно нельзя сказать о инструкции increment, когда она достигает 50 (поскольку в этом нет ничего особенного значение, в отличие от нуля). Поэтому вам нужно сравнить регистр с 50, чтобы установить нулевой флаг.
однако, спрашивая, какой из двух контуров:
for(int i = 100000; i > 0; i--) {}
for(int i = 1; i < 100001; i++) {}
быстрее (в значительной степени любой environment, Java или иначе) бесполезен, так как ни один из них не делает ничего полезного. The быстрый версия обоих этих петель вообще не петля. Я призываю всех, чтобы придумать более быструю версию, чем это: -)
они только станут полезно, когда вы начинаете делать полезную работу внутри фигурных скобок и, в этот момент,работа будет диктовать, какой порядок вы должны использовать.
например, если вы нужно чтобы сосчитать от 1 до 100 000, вы должны использовать второй цикл. Это потому, что преимущество обратного отсчета (если есть), вероятно, будет завалено тем фактом, что вам нужно оценить 100000-i
внутри цикла каждый раз, когда вам нужно использовать его. С точки зрения сборки, это будет разница между:
ld b,100000 dsw a
sub b,a
dsw b
(dsw
- это, конечно, печально известный do something with
ассемблерная мнемоника).
так как вы будете принимать удар только для инкрементного цикла один раз за итерацию, и вы будете принимать удар для вычитания по крайней мере один раз за итерацию (если вы будете использовать i
, в противном случае нет необходимости в цикле вообще), вы должны просто пойти с более естественной версией.
Если вам нужно подсчитать, подсчитайте. Если вам нужно отсчитывать, отсчитывать.
на многих компиляторах машинные инструкции, испускаемые для цикла, идущего в обратном направлении, более эффективны, потому что тестирование на ноль (и, следовательно, нулевой регистр) быстрее, чем немедленная нагрузка постоянного значения.
с другой стороны, хороший оптимизирующий компилятор должен иметь возможность проверять внутренний цикл и определять, что обратное движение не вызовет никаких побочных эффектов...
кстати, это ужасный вопрос интервью, на мой взгляд. Если вы говорите цикл, который выполняется 10 миллионов раз, и вы установили, что небольшое усиление не перевешивается многими экземплярами воссоздания значения прямого цикла (n - i), любое увеличение производительности будет минимальным.
как всегда, не микро-оптимизируйте без бенчмаркинга производительности и за счет более трудного для понимания кода.
такие вопросы в значительной степени являются неуместным отвлечением, что некоторые люди становятся одержимыми этим. Назовите это культ микро-оптимизации или что вам нравится, но быстрее ли петля вверх или вниз? Серьезно? Вы используете то, что подходит для того, что вы делаете. Вы не пишете свой код, сохраняя два такта или что-то еще.
пусть компилятор делает то, для чего он предназначен, и сделает вас намерение очистить (как составитель и читатель.) Еще одна распространенная пессимизация Java:
public final static String BLAH = new StringBuilder().append("This is ").append(3).append(' text").toString();
потому что чрезмерная конкатенация приводит к фрагментации памяти, но для константы компилятор может (и будет) оптимизировать это:
public final static String BLAH = "This is a " + 3 + " test";
где он не будет оптимизировать первый, а второй легче читать.
а как насчет (a>b)?a:b
vs Math.max(a,b)
? Я знаю, что предпочел бы прочитать второй, поэтому мне все равно, что первый не несет накладных расходов на вызов функции.
есть пара полезных вещей в этом списке, как знать, что finally
блок не призывает System.exit()
is потенциально полезное. Знание того, что деление поплавка на 0.0 не вызывает исключения, полезно.
но не беспокойтесь о втором угадывании компилятора, если он действительно имеет значение (и я уверен, что 99,99% времени это не так).
лучший вопрос;
что легче понять/работать?
Это гораздо важнее, чем условная разница в производительности. Лично я бы отметил, что производительность не должна быть критерием для определения разницы здесь. Если бы им не нравилось, что я бросаю вызов их предположениям, я бы не была недовольна тем, что не получила работу. ;)
в современной реализации Java это не так. Суммируя цифры до одного миллиарда в качестве ориентира:
Java(TM) SE Runtime Environment 1.6.0_05-b13 Java HotSpot(TM) Server VM 10.0-b19 up 1000000000: 1817ms 1.817ns/iteration (sum 499999999500000000) up 1000000000: 1786ms 1.786ns/iteration (sum 499999999500000000) up 1000000000: 1778ms 1.778ns/iteration (sum 499999999500000000) up 1000000000: 1769ms 1.769ns/iteration (sum 499999999500000000) up 1000000000: 1769ms 1.769ns/iteration (sum 499999999500000000) up 1000000000: 1766ms 1.766ns/iteration (sum 499999999500000000) up 1000000000: 1776ms 1.776ns/iteration (sum 499999999500000000) up 1000000000: 1768ms 1.768ns/iteration (sum 499999999500000000) up 1000000000: 1771ms 1.771ns/iteration (sum 499999999500000000) up 1000000000: 1768ms 1.768ns/iteration (sum 499999999500000000) down 1000000000: 1847ms 1.847ns/iteration (sum 499999999500000000) down 1000000000: 1842ms 1.842ns/iteration (sum 499999999500000000) down 1000000000: 1838ms 1.838ns/iteration (sum 499999999500000000) down 1000000000: 1832ms 1.832ns/iteration (sum 499999999500000000) down 1000000000: 1842ms 1.842ns/iteration (sum 499999999500000000) down 1000000000: 1838ms 1.838ns/iteration (sum 499999999500000000) down 1000000000: 1838ms 1.838ns/iteration (sum 499999999500000000) down 1000000000: 1847ms 1.847ns/iteration (sum 499999999500000000) down 1000000000: 1839ms 1.839ns/iteration (sum 499999999500000000) down 1000000000: 1838ms 1.838ns/iteration (sum 499999999500000000)
обратите внимание, что различия во времени хрупкие, небольшие изменения где-то рядом с петлями могут их повернуть.
Edit: Эталоном петли
long sum = 0;
for (int i = 0; i < limit; i++)
{
sum += i;
}
и
long sum = 0;
for (int i = limit - 1; i >= 0; i--)
{
sum += i;
}
использование суммы типа int примерно в три раза быстрее, но затем сумма переполняется. С BigInteger это более 50 раз медленнее:
BigInteger up 1000000000: 105943ms 105.943ns/iteration (sum 499999999500000000)
Как правило, реальный код будет работать быстрее, считая вверх. Для этого есть несколько причин:
- процессоры оптимизированы для чтения вперед памяти.
- HotSpot (и, предположительно, другие байт-код - >собственные компиляторы) сильно оптимизируют прямые циклы, но не беспокоятся о обратных циклах, потому что они происходят так редко.
- вверх обычно более очевидно, и более чистый код часто быстрее.
так счастливо делать правильные вещи обычно будет быстрее. Ненужная микрооптимизация-это зло. Я целенаправленно не писал обратные циклы с момента программирования ассемблера 6502.
вы уверены, что интервьюер, который задает такой вопрос, ожидает прямого ответа ("номер один быстрее "или" номер два быстрее"), или если этот вопрос задан, чтобы спровоцировать дискуссию, как это происходит в ответах, которые люди дают здесь?
В общем, невозможно сказать, какой из них быстрее, потому что он сильно зависит от компилятора Java, JRE, CPU и других факторов. Использование одного или другого в вашей программе только потому, что вы думаете, что один из двух быстрее без понимания деталей до самого низкого уровня это суеверный программирования. И даже если одна версия быстрее другой в вашей конкретной среде, то разница, скорее всего, настолько мала, что это не имеет значения.
написать четкий код вместо того, чтобы пытаться быть умным.
такие вопросы имеют свою основу на старых рекомендациях лучшей практики. Это все о сравнении: по сравнению с 0, как известно, быстрее. Много лет назад это казалось очень важным. В настоящее время, особенно с Java, я бы предпочел, чтобы компилятор и виртуальная машина делали свою работу, и я бы сосредоточился на написании кода, который легко поддерживать и понимать.
Если нет причин делать это иначе. Помните, что Java-приложения не всегда работают в HotSpot и / или быстро аппаратура.
что касается тестирования для нуля в JVM: это, по-видимому, можно сделать с ifeq в то время как тестирование для чего-либо еще требует if_icmpeq что также включает в себя дополнительное значение в стеке.
тестирование > 0
, как в вопросе, может быть сделано с помощью ifgt, тогда как тестирование на < 100001
потребуется if_icmplt.
Это самый тупой вопрос, который я когда-либо видел. Тело цикла пустое. Если компилятор хорош, он просто не будет выдавать никакого кода вообще. Он ничего не делает, не может создать исключение и не изменяет ничего за пределами своей области.
предполагая, что ваш компилятор не так умен, или что у вас на самом деле не было пустого тела цикла: Аргумент "backwards loop counter" имеет смысл для некоторых языков сборки (это может иметь смысл и для байтового кода java, я не знайте это конкретно). Однако компилятор очень часто будет иметь возможность преобразовывать цикл для использования уменьшающих счетчиков. Если у вас нет тела цикла, в котором явно используется значение i, компилятор может выполнить это преобразование. И снова вы часто не видите разницы.
Я решил укусить и некро назад нить.
оба цикла игнорируются JVM как no-ops. таким образом, по существу, даже одна из петель была до 10, а другая до 10000000, не было бы никакой разницы.
цикл назад к нулю-это другое дело (для инструкции jne, но опять же, он не скомпилирован так), связанный сайт просто странный (и неправильный).
этот тип вопроса не подходит ни для JVM (ни для любого другого компилятора, который может оптимизировать.)
петли идентичны, за исключением одной критической части:
i > 0; и i
проверка больше нуля выполняется путем проверки nzp (обычно известного как код условия или отрицательный ноль или положительный БИТ) БИТ компьютера.
бит НЗП установлен когда деятельность как нагрузка, и, ект добавления. исполняются.
больше, чем check не может напрямую использовать этот бит (и поэтому занимает немного больше времени...) Общий решение состоит в том, чтобы сделать одно из значений отрицательным (сделав побитовое не, а затем добавив 1), а затем добавив его к сравниваемому значению. Если результат равен нулю, то они равны. Положительное, тогда второе значение (не отрицательное) больше. Отрицательно, тогда первое значение (neg) больше. Эта проверка занимает немного больше времени, чем непосредственная проверка nzp.
Я не на 100% уверен, что это причина этого, хотя, но это кажется возможной причиной...
ответ a (как вы, вероятно, узнали на веб-сайте)
Я думаю, причина в том, что i > 0
условие для прекращения цикла быстрее тестировать.
суть в том, что для любого критического приложения с низкой производительностью разница, вероятно, не имеет значения. Как указывали другие, бывают случаи, когда использование ++i вместо i++ может быть быстрее, однако, особенно в циклах for любой современный компилятор должен оптимизировать это различие.
тем не менее, разница, вероятно, связана с базовыми инструкциями, которые генерируются для сравнения. Тестирование, если значение равно 0, является просто NAND NOR gate. В то время как тестирование, если значение равно произвольной константе, требует загрузки этой константы в регистр, а затем сравнения двух регистров. (Это, вероятно, потребует дополнительной задержки ворот или двух.) Тем не менее, с pipelining и современными ALUs я был бы удивлен, если бы различие было значительным для начала.
Я делаю тесты около 15 минут, ничего не работает, кроме eclipse на всякий случай, и я видел реальную разницу, вы можете попробовать.
когда я попытался рассчитать, сколько времени java занимает, чтобы сделать "ничего", и потребовалось около 500 наносекунд, чтобы иметь представление.
затем я проверил, сколько времени требуется для запуска for
оператор, где он увеличивается:
for(i=0;i<100;i++){}
затем через пять минут я попробовал " назад" один:
for(i=100;i>0;i--)
и у меня есть огромная разница (в крошечном крошечном уровне) 16% между первым и вторым for
заявления, последнее на 16% быстрее.
среднее время для запуска "увеличения"for
оператор во время 2000 тестов:1838 Н/
среднее время для запуска "снижения" for
оператор во время 2000 тестов:1555 Н/
код, используемый для таких тесты:
public static void main(String[] args) {
long time = 0;
for(int j=0; j<100; j++){
long startTime = System.nanoTime();
int i;
/*for(i=0;i<100;i++){
}*/
for(i=100;i>0;i--){
}
long endTime = System.nanoTime();
time += ((endTime-startTime));
}
time = time/100;
System.out.print("Time: "+time);
}
вывод:
Разница в основном ничего, она уже занимает значительное количество "ничего", чтобы сделать" ничего " по отношению к for
тесты операторов, что делает разницу между ними незначительной, просто время, необходимое для импорта библиотеки, такой как java.утиль.Сканер занимает гораздо больше времени для загрузки, чем запуск for
заявление, это не улучшит производительность вашего приложения значительно, но это все еще действительно здорово знать.