Итак, я знаю, что многие языки в стиле C имеют декремент (--
) и приращение (++
) и позволяют мутации произойти до или после вычисления всего выражения.
Что происходит, когда после возврата происходит постинкремент? Я не спрашиваю с точки зрения поведения, а скорее реализации.
Учитывая виртуальную машину (например, JavaScript / JVM) или физическую машину (например, скомпилированный C ++), сгенерированные коды операций похожи на следующие? (Предполагая основанные на стеке аргументы / возвраты.)
int x = 4, y = 8;
return f(++a) + y++;
Превращается в это: (может быть?)
LOAD 4 A
LOAD 8 B
INC A
PUSH A
CALL F
POP INTO C
ADD C BY B
INC B
RET C
Если так, как такие операции в этих языках решают, куда встраивать приращение, когда выражение становится сложным, возможно, даже немного Lispish?
Как только код был оптимизирован (либо компилятором C ++, либо JIT), я ожидал что-то похожее на следующее:
Источник:
int x = 4, y = 8;
return f(++x) + y++;
Инструкции:
PUSH 5
CALL f
POP INTO A
ADD 8 to A
RET A
Инструкции, которые я перечислил, верны (если я не сделал какую-то глупую ошибку), и они требуют только тех преобразований кода, которые, как я знаю, могут быть выполнены оптимизаторами. В основном, неиспользованные результаты не рассчитываются, а операции с известными результатами вычисляются во время оптимизации.
Вполне возможно, что в конкретной архитектуре есть более эффективный способ сделать это, и в этом случае есть хороший шанс, что оптимизатор узнает об этом и будет его использовать. Оптимизаторы часто превосходят мои ожидания, так как я не опытный программист сборки. В этом случае весьма вероятно, что соглашение о вызовах диктует одно и то же местоположение для возвращаемого значения в случае функции f
и этот код. Так что никакой POP может не понадобиться — в регистр возврата / расположение стека можно просто добавить 8. Кроме того, если функция f
тогда оптимизация будет применена после встраивание.
Так, например, если f
возвращается input * 2
тогда вся функция может быть оптимизирована для:
RET 18
Среда выполнения Java далека от вашего исходного кода и даже от байт-кода. Как только метод JIT-скомпилирован, результирующий машинный код агрессивно оптимизируется. Но что еще более важно, детали этой оптимизации выходят далеко за рамки любой спецификации. Если вас это заинтересует, вы можете углубиться в реализацию, подобную HotSpot, но все, что вы узнаете из нее, будет зависеть от платформы, версии, номера сборки, аргументов запуска JVM и даже от отдельного запуска JVM.
Вы можете точно увидеть, что генерирует компилятор, используя javap
. Например:
int x = 4, y = 8;
return f(++x) + y++;
был скомпилирован в эту последовательность байт-кода:
0: iconst_4
1: istore_1
2: bipush 8
4: istore_2
5: aload_0
6: iinc 1, 1
9: iload_1
10: invokevirtual #2; //Method f:(I)I
13: iload_2
14: iinc 2, 1
17: iadd
18: ireturn
Конечно, это зависит от JVM, как превратить это в ассемблер — смотрите Разберите Java JIT скомпилированный собственный байт-код о том, как вы можете увидеть результаты в OpenJDK 7.