Почему компиляторы такие глупые?

Мне всегда интересно, почему компиляторы не могут понять простые вещи, которые очевидны для человеческого глаза. Они делают много простых оптимизаций, но никогда не являются чем-то даже немного сложным. Например, этот код занимает около 6 секунд на моем компьютере, чтобы напечатать нулевое значение (используя Java 1.6):

int x = 0;
for (int i = 0; i < 100 * 1000 * 1000 * 1000; ++i) {
    x += x + x + x + x + x;
}
System.out.println(x);

Совершенно очевидно, что x никогда не изменяется, поэтому независимо от того, как часто вы добавляете 0 к себе, оно остается равным нулю. Таким образом, теоретически компилятор может заменить это на System.out.println(0).

Или даже лучше, это занимает 23 секунды:

public int slow() {
    String s = "x";
    for (int i = 0; i < 100000; ++i) {
        s += "x";
    }
    return 10;
}

Сначала компилятор мог заметить, что я на самом деле создаю строку s размером 100000 "x", чтобы он мог вместо этого автоматически использовать s StringBuilder или, что еще лучше, напрямую заменить ее полученной строкой, поскольку она всегда одинакова. Во-вторых, он не распознает, что я вообще не использую строку, поэтому весь цикл может быть отброшен!

Почему, после того, как так много рабочей силы уходит на быстрые компиляторы, они все еще настолько глупы?

РЕДАКТИРОВАТЬ: Конечно, это глупые примеры, которые никогда не должны использоваться нигде. Но всякий раз, когда мне приходится переписывать красивый и очень читаемый код во что-то нечитаемое, чтобы компилятор был доволен и создавал быстрый код, я удивляюсь, почему компиляторы или какой-либо другой автоматизированный инструмент не могут выполнить эту работу за меня.

28 ответов

Решение

О, я не знаю Иногда компиляторы довольно умны. Рассмотрим следующую программу на C:

#include <stdio.h>  /* printf() */

int factorial(int n) {
   return n == 0 ? 1 : n * factorial(n - 1);
}

int main() {
   int n = 10;

   printf("factorial(%d) = %d\n", n, factorial(n));

   return 0;
}

В моей версии GCC (4.3.2 по тестированию Debian), когда она компилируется без оптимизации или -O1, она генерирует код для factorial(), как и следовало ожидать, используя рекурсивный вызов для вычисления значения. Но на -O2 он делает что-то интересное: он компилируется в плотный цикл:

    factorial:
   .LFB13:
           testl   %edi, %edi
           movl    $1, %eax
           je  .L3
           .p2align 4,,10
           .p2align 3
   .L4:
           imull   %edi, %eax
           subl    $1, %edi
           jne .L4
   .L3:
           rep
           ret

Довольно внушительный. Рекурсивный вызов (даже не хвостовой рекурсии) был полностью исключен, поэтому теперь факториал использует пространство стека O(1) вместо O(N). И хотя у меня есть только очень поверхностные знания о сборке x86 (на самом деле AMD64 в данном случае, но я не думаю, что какие-либо расширения AMD64 используются выше), я сомневаюсь, что вы могли бы написать лучшую версию вручную. Но что поразило меня, так это код, сгенерированный на -O3. Реализация факториала осталась прежней. Но main() изменилось:

    main:
   .LFB14:
           subq    $8, %rsp
   .LCFI0:
           movl    $3628800, %edx
           movl    $10, %esi
           movl    $.LC0, %edi
           xorl    %eax, %eax
           call    printf
           xorl    %eax, %eax
           addq    $8, %rsp
           ret

Увидеть movl $3628800, %edx линия? gcc предварительно вычисляет факториал (10) во время компиляции. Он даже не вызывает факториал (). Невероятный. Я снимаю шляпу перед командой разработчиков GCC.

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

(Адаптировано из публикации в моем блоге.)

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

x = 0
sleep 6 // Let's assume this is defined somewhere.
print x

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

Код и компилятор, который его обрабатывает, являются инструментами, и вы должны быть кузнецом, если хотите эффективно их использовать. Сколько 12"бензопил откажется попробовать срубить 30" дерево? Сколько упражнений автоматически переключится в режим удара, если обнаружит бетонную стену?

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

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

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

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

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

Говоря с точки зрения C/C++:

Ваш первый пример будет оптимизирован большинством компиляторов. Если java-компилятор от Sun действительно выполняет этот цикл, то это ошибка компилятора, но, поверьте мне, любой пост-компилятор C, C++ или Fortran, выпущенный в 1990 году, полностью исключает такой цикл.

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

В целом, я доволен оптимизацией, которую могут выполнять компиляторы в эти дни.

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

  • Вы должны иметь возможность смотреть на свой код и делать разумные прогнозы относительно его производительности.

  • Небольшие изменения в коде не должны приводить к существенным различиям в производительности.

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

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


В обоих ваших примерах переменная обновляется в цикле, но не используется где-либо еще. Этот случай на самом деле довольно сложен, если вы не используете какую-то платформу, которая может сочетать устранение мертвого кода с другими оптимизациями, такими как копирование или постоянное распространение. Для простого оптимизатора потока данных переменная не выглядит мертвой. Чтобы понять, почему эта проблема сложная, см. Статью Лернера, Гроува и Чамберса в POPL 2002, в которой используется именно этот пример и объясняется, почему она сложная.

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

Это описано в FAQ HotSpot, под вопросом "Я пишу простой цикл для определения времени простой операции, и она медленная. Что я делаю не так?".

Шутки в сторону? Зачем кому-то писать такой код в реальном мире? ИМХО, код, а не компилятор - это "глупая" сущность здесь. Я, например, совершенно счастлив, что авторы компиляторов не тратят впустую свое время, пытаясь оптимизировать что-то подобное.

Редактировать / Уточнение: я знаю, что код в вопросе приведен в качестве примера, но это только подтверждает мою точку зрения: либо вы должны пытаться, либо быть совершенно невежественным, чтобы написать столь неэффективный код, как этот. Работа компилятора не в том, чтобы держать нас за руку, поэтому мы не пишем ужасный код. Как люди, которые пишут код, мы обязаны знать достаточно о наших инструментах, чтобы писать эффективно и четко.

Ну, я могу говорить только о C++, потому что я новичок в Java. В C++ компиляторы могут свободно игнорировать любые языковые требования, установленные стандартом, при условии, что наблюдаемое поведение такое, как если бы компилятор фактически эмулировал все правила, установленные стандартом. Наблюдаемое поведение определяется как любое чтение и запись в изменчивые данные и вызовы библиотечных функций. Учти это:

extern int x; // defined elsewhere
for (int i = 0; i < 100 * 1000 * 1000 * 1000; ++i) {
    x += x + x + x + x + x;
}
return x;

Компилятору C++ разрешено оптимизировать этот фрагмент кода и просто добавить правильное значение x, которое будет получено из этого цикла один раз, потому что код ведет себя так, как если бы цикл никогда не выполнялся, и не участвуют ни изменчивые данные, ни библиотечные функции. это может вызвать побочные эффекты, необходимые. Теперь рассмотрим изменчивые переменные:

extern volatile int x; // defined elsewhere
for (int i = 0; i < 100 * 1000 * 1000 * 1000; ++i) {
    x += x + x + x + x + x;
}
return x;

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


Говоря о Java, я проверил ваш цикл, и случается, что компилятор Java GNU (gcj) занимает слишком много времени, чтобы закончить цикл (он просто не закончился, и я его убил). Я включил флаги оптимизации (-O2), и это случилось, это распечатано 0 немедленно:

[js@HOST2 java]$ gcj --main=Optimize -O2 Optimize.java
[js@HOST2 java]$ ./a.out
0
[js@HOST2 java]$

Может быть, это наблюдение может быть полезным в этой теме? Почему это так быстро для gcj? Ну, одна из причин, безусловно, заключается в том, что gcj компилируется в машинный код, и поэтому у него нет возможности оптимизировать этот код на основе поведения кода во время выполнения. Он собирает все свои сильные стороны вместе и пытается оптимизировать как можно больше во время компиляции. Однако виртуальная машина может скомпилировать код Just in Time, как показано в выходных данных Java для этого кода:

class Optimize {
    private static int doIt() {
        int x = 0;
        for (int i = 0; i < 100 * 1000 * 1000 * 1000; ++i) {
            x += x + x + x + x + x;
        }
        return x;
    }
    public static void main(String[] args) {
        for(int i=0;i<5;i++) {
            doIt();
        }
    }
}

Выход для java -XX:+PrintCompilation Optimize:

1       java.lang.String::hashCode (60 bytes)
1%      Optimize::doIt @ 4 (30 bytes)
2       Optimize::doIt (30 bytes)

Как видим, JIT компилирует функцию doIt 2 раза. Основываясь на наблюдении за первым выполнением, он компилирует его во второй раз. Но он имеет тот же размер, что и байт-код два раза, предполагая, что цикл все еще на месте.

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

В первом примере это оптимизация, которая работает, только если значение равно нулю. Экстра if Оператор в компиляторе, необходимый для поиска этого редко встречающегося предложения, может просто не стоить этого (поскольку он должен проверять это для каждой переменной). Кроме того, что по этому поводу:

int x = 1;
int y = 1;
int z = x - y;
for (int i = 0; i < 100 * 1000 * 1000 * 1000; ++i) {
    z += z + z + z + z + z;
}
System.out.println(z);

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

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

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

Просто проще потратить лишний момент и убедиться, что вы пишете именно то, что вы действительно хотите, чтобы компьютер делал.:)

Компиляторы в целом очень умные.

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

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

Даже если ваш пример прост, все же могут возникнуть сложные ситуации.

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

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

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

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

Кроме того, я думаю, что люди, которые программируют компиляторы, скорее потратят время на оптимизацию, которая не так проста для человека.

Потому что мы просто еще не там. Вы могли бы также легко спросить: "Почему мне все еще нужно писать программы... почему я не могу просто подать документ с требованиями и попросить компьютер написать приложение для меня?"

Авторы компиляторов тратят время на мелочи, потому что это те вещи, которые программисты приложений обычно упускают.

Кроме того, они не могут предполагать слишком много (возможно, ваш цикл был чем-то вроде временной задержки в гетто или что-то в этом роде)?

Поскольку другие ответили на первую часть вашего вопроса адекватно, я постараюсь заняться второй частью, то есть "вместо этого автоматически используется StringBuilder".

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

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

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

Это вечная гонка вооружений между авторами компиляторов и программистами.

Непридуманные примеры прекрасно работают - большинство компиляторов действительно оптимизируют заведомо бесполезный код.

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

В будущем вам понадобится больше надуманных примеров, чем вы разместили здесь.

Задача компилятора - оптимизировать то, как код что-то делает, а не то, что он делает.

Когда вы пишете программу, вы говорите компьютеру, что делать. Если компилятор изменил ваш код для выполнения чего-то другого, кроме того, что вы ему сказали, это был бы не очень хороший компилятор! Когда ты пишешьx += x + x + x + x + x, вы явно указываете компьютеру, что хотите, чтобы он xдо 6 раз себя. Компилятор вполне может оптимизировать, как он это делает (например, умножаяx на 6 вместо повторного сложения), но, несмотря на это, он все равно вычислит это значение каким-то образом.

Если вы не хотите, чтобы что-то делалось, не говорите это кому-либо

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

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

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

Компиляторы, которые могут выполнять строгую оптимизацию, будут оптимизировать первый пример. Смотрите здесь.

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

Я сам также предпочел бы иметь malloc(100000), чем тысячу malloc(100) при выполнении s += "s"; но сейчас эта вещь выходит за рамки компиляторов и должна быть оптимизирована людьми. Это то, что язык D пытается решить, вводя чистые функции.

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

Абсолютная оптимизация - неразрешимая проблема, то есть нет машины Тьюринга (и, следовательно, нет компьютерной программы), которая могла бы дать оптимальную версию ЛЮБОЙ данной программы.

Некоторые простые оптимизации могут быть (и фактически) выполнены, но в приведенных вами примерах...

  1. Чтобы обнаружить, что ваша первая программа всегда печатает ноль, компилятор должен обнаружить, что x остается постоянным, несмотря на все итерации цикла. Как вы можете объяснить (я знаю, это не лучшее слово, но я не могу придумать другое) это компилятору?

  2. Как компилятор может узнать, что StringBuilder является подходящим инструментом для работы без ЛЮБОЙ ссылки на него?

В реальных приложениях, если эффективность важна для части вашего приложения, она должна быть написана на языке низкого уровня, таком как C. (Ха-ха, серьезно, я написал это?)

Оптимизировать подобные вещи при компиляции в байт-код JVM практически считается плохой практикой. Солнца javac имеет некоторые базовые оптимизации, как и scalac, groovycи т. д. Короче говоря, все, что действительно зависит от языка, может быть оптимизировано в компиляторе. Однако такие вещи, которые, очевидно, настолько изобретательны, что не зависят от языка, просто выскользнут из политики.

Причина этого в том, что он позволяет HotSpot иметь гораздо более согласованное представление о байт-коде и его шаблонах. Если компиляторы начинают дурачиться с крайними случаями, это уменьшает способность виртуальной машины оптимизировать общий случай, который может быть неочевиден во время компиляции. Стив Йегги любит говорить об этом: оптимизация часто проще, когда она выполняется во время выполнения умной виртуальной машиной. Он даже заходит так далеко, что утверждает, что HotSpot лишает оптимизацию javac. Хотя я не знаю, правда ли это, меня это не удивит.

Подводя итог: компиляторы, нацеленные на виртуальные машины, имеют совершенно другой набор критериев, особенно в области оптимизации и, когда это уместно. Не обвиняйте авторов компиляторов в том, что они оставили работу более способной JVM. Как уже несколько раз говорилось в этой теме, современные компиляторы ориентированы на нативную архитектуру (например, gcc family) чрезвычайно умны, производя непристойно быстрый код с помощью некоторых очень умных оптимизаций.

На самом деле, Java должна использовать построитель строк во втором примере.

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

Конечно, вы можете придумать тривиальные примеры, но количество тривиальных примеров не ограничено. Вы всегда можете думать о чем-то другом, поэтому нет способа поймать их всех.

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

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

Возможно, вы захотите взглянуть на Хаскель, который, я думаю, имеет самые передовые теоретические доказательства, хотя я не уверен в этом. Я сам этого не знаю.

В режиме выпуска VS 2010 C++ для запуска не требуется никакого времени. Однако режим отладки это другая история.

#include <stdio.h>
int main()
{
    int x = 0;
    for (int i = 0; i < 100 * 1000 * 1000 * 1000; ++i) {
        x += x + x + x + x + x;
    }
    printf("%d", x);
}

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

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

Это пример процедурного кода против функционального кода.

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

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

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

Поскольку авторы компиляторов пытаются добавить оптимизацию для вещей, которые имеют значение (я надеюсь) и которые измеряются в тестах *Stone (я боюсь).

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

Что меня смущает, так это то, что даже сегодня большинство компиляторов генерируют код для проверки того, что значение switchValue больше 255 для плотного или почти полного переключения беззнакового символа. Это добавляет 2 инструкции к внутреннему циклу большинства интерпретаторов байт-кода.

Предпосылка: я изучал компиляторы в университете.

Компилятор javac чрезвычайно глуп и не выполняет абсолютно никакой оптимизации, потому что для их выполнения ему нужна среда выполнения java. Среда выполнения поймает эту вещь и оптимизирует ее, но она поймает ее только после того, как функция будет выполнена несколько тысяч раз.

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

Я ненавижу поднимать этот вопрос по такому старому вопросу (как я сюда попал, в любом случае?), Но я думаю, что часть этого может быть чем-то вроде воздержания со времен Commodore 64.

В начале 1980-х все работало на фиксированных часах. Не было никакого Turbo Boosting, и код всегда создавался для конкретной системы с конкретным процессором и определенной памятью и т. Д. В Commodore BASIC - стандартный метод реализации delayЭто очень похоже на:

10 FOR X = 1 TO 1000
20 NEXT : REM 1-SECOND DELAY

(На самом деле, на практике это больше напоминало 10FORX=1TO1000:NEXT, но вы знаете, что я имею в виду.)

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

По общему признанию, эти неоптимизации сегодня не нужны. Однако, вероятно, среди разработчиков компиляторов есть негласное правило не оптимизировать подобные вещи. Я бы не знал.

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

Просто мои мысли.

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

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

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

Крайняя другая ситуация. У нас есть умный компилятор, компилирующий Windows. Тонны кода для компиляции. Но если он умный, он сводится к 3 строкам кода...

"starting windows"
"enjoy freecell/solitaire"
"shutting down windows"

Остальная часть кода устарела, потому что он никогда не использовался, не затрагивался и не использовался. Мы действительно этого хотим?

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

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

Это заставляет вас (программиста) думать о том, что вы пишете. Принуждение компиляторов выполнять свою работу за вас никому не помогает: это делает компиляторы намного более сложными (и медленнее!), И делает вас глупее и менее внимательным к вашему коду.

Другие вопросы по тегам