Как именно выравнивание влияет на макет памяти и поведение размещения новых? - PullRequest
0 голосов
/ 24 января 2019

Мы много читали о выравнивании и его важности, например, для размещения new, но мне было интересно - как оно точно меняет расположение памяти?

Очевидно, что если мы сделаем

char buffer[10];
std::cout << sizeof buffer;

и

alignas(int) char buffer[10];
std::cout << sizeof buffer;

мы получаем тот же результат, который равен 10.

Но поведение не может быть точно таким же, не так ли? Почему это различимо? Я попытался найти ответ и побежал к Годболту, проверяя следующий код:

#include <memory>

int main() {
    alignas(int) char buffer[10];
    new (buffer) int;
}

, что в соответствии с GCC 8.2 и без оптимизаций приводит к следующей сборке:

operator new(unsigned long, void*):
    push    rbp
    mov     rbp, rsp
    mov     QWORD PTR [rbp-8], rdi
    mov     QWORD PTR [rbp-16], rsi
    mov     rax, QWORD PTR [rbp-16]
    pop     rbp
    ret
main:
    push    rbp
    mov     rbp, rsp
    sub     rsp, 16
    lea     rax, [rbp-12]
    mov     rsi, rax
    mov     edi, 4
    call    operator new(unsigned long, void*)
    mov     eax, 0
    leave
    ret

Давайте немного изменим код, удалив часть alignas(int). Теперь сгенерированная сборка немного отличается:

operator new(unsigned long, void*):
    push    rbp
    mov     rbp, rsp
    mov     QWORD PTR [rbp-8], rdi
    mov     QWORD PTR [rbp-16], rsi
    mov     rax, QWORD PTR [rbp-16]
    pop     rbp
    ret
main:
    push    rbp
    mov     rbp, rsp
    sub     rsp, 16
    lea     rax, [rbp-10]
    mov     rsi, rax
    mov     edi, 4
    call    operator new(unsigned long, void*)
    mov     eax, 0
    leave
    ret

Примечательно, что он отличается только инструкцией lea, где вторым параметром является [rbp-10] вместо [rbp-12], как это было в версии alignas(int).

Обратите внимание, что я обычно не понимаю сборку. Я не могу написать ассемблер, но могу кое-что прочитать. Насколько я понимаю, разница просто изменяет смещение адресов памяти, которое будет занимать наше размещение - new ed int.

Но чего он добивается? Зачем нам это нужно? Предположим, у нас есть «общее» представление массива buffer следующим образом:

[ ] [ ] [ ] [ ] [ ] [ ] [ ] [ ] [ ] [ ]

Теперь, я бы предположил, что после размещения new с int ( с или без выравнивания) мы бы получили что-то вроде этого:

[x] [x] [x] [x] [ ] [ ] [ ] [ ] [ ] [ ]

где x представляет один байт int (мы предполагаем, что sizeof(int) == 4).

Но я должен что-то упустить. Это еще не все, и я не знаю, что. Чего именно мы достигаем, выровняв подходящее выравнивание от buffer до int? Что произойдет, если мы не настроим это так?

1 Ответ

0 голосов
/ 24 января 2019

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

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

Когда вы выравниваете свой буфер char по целочисленной границе, это не влияет на то, как работает новое размещение. Это просто гарантирует, что вы можете использовать размещение new, чтобы поместить int в начало вашего буфера, не нарушая каких-либо ограничений выравнивания. Это достигается за счет того, что адрес буфера кратен 4 байтам.

...