Преинкремент vs постинкремент
Особо бесцельный пост, просто чтобы вы просто посмотрели на то, какой машинный код компилятор генерирует для одного и другого оператора для знакового числа, когда их различия влияют на поведение программы. Иногда полезно залезать в ассемблер, чтобы понимать, как вещи реально работают на практике. Потому что, как мы убедились, не всегда то, что декларируется, реально осуществляется на практике. А мы, как плюсовики, должны докапываться до истины и никому не верить на слово!
Возьмем опять простенькую программу:
int main()
{
int i = 0;
int a = ++i; //i++;
return 0;
}
В первом случае я буду использовать преинкремент, во втором - постинкремент. Компилировать буду с g++ на x86-64 и без оптимизаций, потому что для такого примера просто не сгенерируется никакого кода, так как ничего полезного мы не делаем. Ну а на чуть более сложных примерах, компилятор понимает, что мы хотим сделать и просто засунет уже готовые числа по нужным адресам. Хочу продемонстрировать такую базовую хрестоматийную версию, о которой пишут в книжках.
Ассемблер для первой версии:
movl $0, -8(%rbp)
addl $1, -8(%rbp)
movl -8(%rbp), %eax
movl %eax, -4(%rbp)
Я сознательно опускаю всю обвязку и тут только суть. Кладем нолик в память со сдвигом 8 от rbp(этой ячейке памяти соответствует переменная i), добавляем к нему единичку, и через eax кладем это значение во вторую локальную переменную a со сдвигом 4 от rbp. eax - регистр-аккумулятор, в нем сохраняются промежуточные результаты арифметических вычислений, поэтому операция инкремента проходит через него. Интересно, что переменная a находится ближе к base pointer'у, хотя она объявлена позже. Это просто следствие того, что компилятор сам выбирает, как ему удобнее расположить локальные переменные на стеке и на это никак нельзя повлиять.
Теперь посмотрим на код постинкремента:
movl $0, -8(%rbp)
movl -8(%rbp), %eax
leal 1(%rax), %edx
movl %edx, -8(%rbp)
movl %eax, -4(%rbp)
Поменялось немногое, но различия уже заметны. Инструкцией leal мы складываем единичку с младшими 32-м битами в регистре rax(кто знает, почему не eax, напишите в комментах) и кладем это значение в edx, не трогая eax. Получается, что в edx создержится инкремент, а в eax - старое значение. Ну и дальше раскидываем их по правильным адресам на стеке. Обратите внимание, что в этом случае мы используем дополнительную память, а именно регистр edx, который не фигурировал в прошлом примере. Именно про это все говорят, когда объясняют, что постинкремент использует дополнительное копирование. Вот вам наглядное представление, как это утверждение ложится на уровень машинного кода.
Ставьте лайк, если нравятся такие низкоуровневые штуки и их на канале будет больше)
Stay hardwared. Stay cool
#hardcore #cppcore
Особо бесцельный пост, просто чтобы вы просто посмотрели на то, какой машинный код компилятор генерирует для одного и другого оператора для знакового числа, когда их различия влияют на поведение программы. Иногда полезно залезать в ассемблер, чтобы понимать, как вещи реально работают на практике. Потому что, как мы убедились, не всегда то, что декларируется, реально осуществляется на практике. А мы, как плюсовики, должны докапываться до истины и никому не верить на слово!
Возьмем опять простенькую программу:
int main()
{
int i = 0;
int a = ++i; //i++;
return 0;
}
В первом случае я буду использовать преинкремент, во втором - постинкремент. Компилировать буду с g++ на x86-64 и без оптимизаций, потому что для такого примера просто не сгенерируется никакого кода, так как ничего полезного мы не делаем. Ну а на чуть более сложных примерах, компилятор понимает, что мы хотим сделать и просто засунет уже готовые числа по нужным адресам. Хочу продемонстрировать такую базовую хрестоматийную версию, о которой пишут в книжках.
Ассемблер для первой версии:
movl $0, -8(%rbp)
addl $1, -8(%rbp)
movl -8(%rbp), %eax
movl %eax, -4(%rbp)
Я сознательно опускаю всю обвязку и тут только суть. Кладем нолик в память со сдвигом 8 от rbp(этой ячейке памяти соответствует переменная i), добавляем к нему единичку, и через eax кладем это значение во вторую локальную переменную a со сдвигом 4 от rbp. eax - регистр-аккумулятор, в нем сохраняются промежуточные результаты арифметических вычислений, поэтому операция инкремента проходит через него. Интересно, что переменная a находится ближе к base pointer'у, хотя она объявлена позже. Это просто следствие того, что компилятор сам выбирает, как ему удобнее расположить локальные переменные на стеке и на это никак нельзя повлиять.
Теперь посмотрим на код постинкремента:
movl $0, -8(%rbp)
movl -8(%rbp), %eax
leal 1(%rax), %edx
movl %edx, -8(%rbp)
movl %eax, -4(%rbp)
Поменялось немногое, но различия уже заметны. Инструкцией leal мы складываем единичку с младшими 32-м битами в регистре rax(кто знает, почему не eax, напишите в комментах) и кладем это значение в edx, не трогая eax. Получается, что в edx создержится инкремент, а в eax - старое значение. Ну и дальше раскидываем их по правильным адресам на стеке. Обратите внимание, что в этом случае мы используем дополнительную память, а именно регистр edx, который не фигурировал в прошлом примере. Именно про это все говорят, когда объясняют, что постинкремент использует дополнительное копирование. Вот вам наглядное представление, как это утверждение ложится на уровень машинного кода.
Ставьте лайк, если нравятся такие низкоуровневые штуки и их на канале будет больше)
Stay hardwared. Stay cool
#hardcore #cppcore