Всем привет!
Для новичков интересная тема, вот многие знают что компилятор выравнивает содержимое структур обычно кратно 4 в x32 и кратно 8 в x64.
Да есть разные атрибуты компилятора, которые запрещают это делать.
Но в этой статье предлагаю разобраться, зачем это вообще делается, как можно оптимизировать потребление памяти без специальных атрибут компилятора.
Итак, статья больше для новичков, но думаю будет интересно.)
Группа проводов(линий), используемых для передачи одинаковых данных, называется шиной. Для передачи адреса используется адресная шина. А шина данных позволяет получать или записывать данные.
Основной характеристикой адресной шины является её ширина в битах, что, как правило, равно максимально допустимому числу разрядов адреса.
У 32-разрядной шины каждый байт имеет 32-битный адрес, что позволит использовать 4Гб адресное пространство(2³²). С 64-битным адресом можно использовать до 2⁶⁴ байт.
При 32-разрядной шине за раз можно получить до 4 байта данных, а 64-разрядная шина позволяет прочитать сразу 8 байт. Такие куски информации принято называть машинным словом.
Компьютеры наиболее эффективно загружают и сохраняют значения в памяти, когда эти значения выравнены(alignment).
Адрес 2-байтового типа(int16) должен быть кратен 2, адрес 4-байтового значения(int32) должен быть кратен 4, а 8-байтового соответственно кратен 8.
Пример. У нас 64-разрядная архитектура, что позволяет за один раз прочитать до 8 байт. Сохраняем одно 1-байтовое значение и три 2-байтовых значения:
Один байт храниться по адресу 0, а 2-байтовые — 2, 4, 6 (кратные 2). Благодаря выравниванию не будет ситуации, когда придется делать два цикла чтения для получения одного значения:
Гарантии выравнивания типов (type alignment guarantees) также называют гарантиями выравнивания адресов(value address alignment guarantees). Если гарантией выравнивания типа T является N, то адрес каждого значения типа T должен быть кратным N во время выполнения. Можно также сказать, что адреса адресуемых значений типа T гарантированно выровнены по N-байтам.
К счастью, выравнивания и многое другое, гарантирует компилятор, и нам не нужно об этом беспокоиться. Но полезно помнить, что выравнивания делает обращение к памяти более эффективное, но вот использование памяти может стать менее эффективным.
Вспомним первый пример. В 8 байтах хранится одно 1-байтовое значение и три 2-байтовых. Из-за выравнивания (у 2-байтового значение должен быть четный адрес) ячейка памяти по адресу 1 осталась свободной. Но если для переменных с простым типов компилятор использует оптимизации, то для структур выравнивание происходит по самому большому полю и может образоваться большое кол-во занятой, но не использованной памяти.
Рассмотрим структуру:
Размер значения этой структуры должен составлять суму ее полей. 1 + 8 + 1 =10 байт.
Но в реальности это не так.)
Результат совсем не тот, который ожидали. Структура занимает почти в 2.5 раза больше: 24 байт.)
Почему так ?
В общем случае экземпляр структуры будет выровнен по самому длинному элементу. Для компилятора это самый простой способ убедиться, что все поля структуры будут также выровнены для быстрого доступа.
Неиспользованная память заполняется(padding) нулями.
Вот что получается в итоге:
Если элементам структуры задать a=0x1, b=0x2, c=0x3, то в памяти это будет выглядеть так:
Вот как-же можно оптимизировать использование памяти, не прибегая к различным флагам и расширения компилятора ?
Смежные поля могут быть объедены, если их сумма не превышает выравнивания структуры.
Переместив поля, можно уменьшить размер структуры до 16 байт.
6 байт все еще не используются, но с этим уже ничего не поделаешь. Туда всегда можно добавить данных до 6 байт, не изменив размер структуры.
Почти всегда, гораздо важней читаемость кода, чем такие оптимизации. Важно понимать по какой причины у значения типа именно такой размер и что вообще происходит, а уже в случае необходимости заниматься оптимизацией.
Также если вы работаете с железками, часто вам нужно запрещать выравнивать структуры, например это нужно при передаче данных в железку, для этого можно использовать attribute packed.
По материалам:
Для новичков интересная тема, вот многие знают что компилятор выравнивает содержимое структур обычно кратно 4 в x32 и кратно 8 в x64.
Да есть разные атрибуты компилятора, которые запрещают это делать.
Но в этой статье предлагаю разобраться, зачем это вообще делается, как можно оптимизировать потребление памяти без специальных атрибут компилятора.
Итак, статья больше для новичков, но думаю будет интересно.)
Процессор и память
Упрощенное представления взаимодействия процессора и памяти. Память имеет адресную байтовую последовательность и расположена последовательно. Чтение или запись данных в памяти выполняется посредством операций, которые воздействуют на одну ячейку за раз. Чтобы прочитать ячейку памяти или произвести запись в нее, мы должны передать ее числовой адрес. Память способна выполнять с адресом ячейки две операции: получить хранящееся в ней данные или записать новые. Память имеет специальный входной контакт для установки ее рабочего режима.Группа проводов(линий), используемых для передачи одинаковых данных, называется шиной. Для передачи адреса используется адресная шина. А шина данных позволяет получать или записывать данные.
Основной характеристикой адресной шины является её ширина в битах, что, как правило, равно максимально допустимому числу разрядов адреса.
У 32-разрядной шины каждый байт имеет 32-битный адрес, что позволит использовать 4Гб адресное пространство(2³²). С 64-битным адресом можно использовать до 2⁶⁴ байт.
При 32-разрядной шине за раз можно получить до 4 байта данных, а 64-разрядная шина позволяет прочитать сразу 8 байт. Такие куски информации принято называть машинным словом.
Компьютеры наиболее эффективно загружают и сохраняют значения в памяти, когда эти значения выравнены(alignment).
Адрес 2-байтового типа(int16) должен быть кратен 2, адрес 4-байтового значения(int32) должен быть кратен 4, а 8-байтового соответственно кратен 8.
Пример. У нас 64-разрядная архитектура, что позволяет за один раз прочитать до 8 байт. Сохраняем одно 1-байтовое значение и три 2-байтовых значения:
Один байт храниться по адресу 0, а 2-байтовые — 2, 4, 6 (кратные 2). Благодаря выравниванию не будет ситуации, когда придется делать два цикла чтения для получения одного значения:
Гарантии выравнивания типов (type alignment guarantees) также называют гарантиями выравнивания адресов(value address alignment guarantees). Если гарантией выравнивания типа T является N, то адрес каждого значения типа T должен быть кратным N во время выполнения. Можно также сказать, что адреса адресуемых значений типа T гарантированно выровнены по N-байтам.
К счастью, выравнивания и многое другое, гарантирует компилятор, и нам не нужно об этом беспокоиться. Но полезно помнить, что выравнивания делает обращение к памяти более эффективное, но вот использование памяти может стать менее эффективным.
Вспомним первый пример. В 8 байтах хранится одно 1-байтовое значение и три 2-байтовых. Из-за выравнивания (у 2-байтового значение должен быть четный адрес) ячейка памяти по адресу 1 осталась свободной. Но если для переменных с простым типов компилятор использует оптимизации, то для структур выравнивание происходит по самому большому полю и может образоваться большое кол-во занятой, но не использованной памяти.
Рассмотрим структуру:
Код:
type someData struct{
a int8 // 1 byte
b int64 // 8 byte
c int8 // 1 byte
}
Размер значения этой структуры должен составлять суму ее полей. 1 + 8 + 1 =10 байт.
Но в реальности это не так.)
Результат совсем не тот, который ожидали. Структура занимает почти в 2.5 раза больше: 24 байт.)
Почему так ?
В общем случае экземпляр структуры будет выровнен по самому длинному элементу. Для компилятора это самый простой способ убедиться, что все поля структуры будут также выровнены для быстрого доступа.
Неиспользованная память заполняется(padding) нулями.
Вот что получается в итоге:
Если элементам структуры задать a=0x1, b=0x2, c=0x3, то в памяти это будет выглядеть так:
Код:
Bytes are &[24]uint8{
0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}
Вот как-же можно оптимизировать использование памяти, не прибегая к различным флагам и расширения компилятора ?
Компилятор не меняет порядок полей в структуре и не может оптимизировать такие случае. А мы можем.
Смежные поля могут быть объедены, если их сумма не превышает выравнивания структуры.
Код:
type someDataV2 struct{
a int8 // 1 byte
c int8 // 1 byte
b int64 // 8 byte
}
Переместив поля, можно уменьшить размер структуры до 16 байт.
6 байт все еще не используются, но с этим уже ничего не поделаешь. Туда всегда можно добавить данных до 6 байт, не изменив размер структуры.
Почти всегда, гораздо важней читаемость кода, чем такие оптимизации. Важно понимать по какой причины у значения типа именно такой размер и что вообще происходит, а уже в случае необходимости заниматься оптимизацией.
Также если вы работаете с железками, часто вам нужно запрещать выравнивать структуры, например это нужно при передаче данных в железку, для этого можно использовать attribute packed.
По материалам:
Вы должны зарегистрироваться, чтобы увидеть внешние ссылки