Skip to the content.

Выравнивание данных


Память программы можно представить так:

[байт 0] [байт 1] [байт 2] [байт 3] [байт 4] [байт 5] [байт 6] [байт 7]...

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

[байт 0] [байт 1] [байт 2] [байт 3] [байт 4] [байт 5] [байт 6] [байт 7]...
[        машинное слово 0         ] [        машинное слово 1         ]...

Однако, байты можно комбинировать в машинные слова не единственным способом:

[байт 0] [байт 1] [байт 2] [байт 3] [байт 4] [байт 5] [байт 6] [байт 7] [байт 8]...
         [         машинное слово 0        ] [         машинное слово 1        ]...

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

Что бы прочесть второй способ прийдется читать два машинных слова (по адресу 0 и 4) и выбирать из них нужные байты:

[байт 0] [байт 1] [байт 2] [байт 3] [байт 4] [байт 5] [байт 6] [байт 7]...
         [    нужно это машинное слово     ]
[     но приходится читать это    ] [              и это              ]

Поэтому компиляторы яп С и других проводят оптимизацию: например, в структуру вставляются пустые байтики (на самом деле просто оставляются дырки), чтобы у полей было хорошее выравнивание:

struct
{
  char x; // 1 байт
  int y;  // 4 байта
  char z; // 1 байт
};
[    ] [    ] [    ] [    ] [    ] [    ] [    ] [    ] [    ] [    ] [    ] [    ]
[  x ] { потерянные байты } [             y           ] [  z ] { потерянные байты }

Стоит заметить, что если переставить поля структуры местами, то можно немного сэкономить (8 байт вместо 12):

[    ] [    ] [    ] [    ] [    ] [    ] [    ] [    ]
[  x ] [  z ] {  потеряно } [             y           ]

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

Интересная тема для обсуждения: почти все ОС на самом деле выделяют память кусками, размер которых кратен странице (типично 4К). Но несмотря на это менеджеры памяти обычно сразу делят этот кусок на “чанки” (по 8, 16, 32 байт) и код обычно получает уже их.

Так же есть инструмент, позволяющий проанализировать код на выравнивание - pahole.

Пример:

struct Apple
{
    char x;
    int y;
    char z;
};

int main()
{
    // Обязательно нужно что бы структура использовалась:
    Apple apple; 
    return 0;
}

Компилируем с отладочными символами: g++ -g

Запуск анализа: pahole -R a.out

Результат:

struct Apple
{
        char                       x;                    /*     0     1 */

        /* XXX 3 bytes hole, try to pack */

        int                        y;                    /*     4     4 */
        char                       z;                    /*     8     1 */

        /* size: 12, cachelines: 1, members: 3 */
        /* sum members: 6, holes: 1, sum holes: 3 */
        /* padding: 3 */
        /* last cacheline: 12 bytes */
};