Насколько важно выравнивание памяти? Это все еще имеет значение?

15

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

Но даже с этим у меня все еще есть некоторые вопросы об этом:

  1. Из встроенной системы у нас часто есть огромный кусок памяти на нашем компьютере, что делает управление памятью намного менее критичным, я полностью увлечен оптимизацией, но теперь, действительно ли это что-то, что может иметь значение, если мы сравниваем ту же программу с или без перестановки и выравнивания памяти?
  2. Есть ли у выравнивания памяти другие преимущества? Я где-то читал, что процессор работает лучше / быстрее с выровненной памятью, потому что для его обработки требуется меньше инструкций (если у кого-то есть ссылка на статью / тест по этому поводу?), В этом случае разница действительно значительна? Есть ли больше преимуществ, чем эти два?
  3. В ссылке на статью в главе 5 автор говорит:

    Осторожно: в C ++ классы, которые выглядят как структуры, могут нарушать это правило! (Зависит от того, как реализованы базовые классы и виртуальные функции-члены, и зависит от компилятора.)

  4. В статье в основном говорится о структурах, но влияет ли эта необходимость на объявление локальных переменных?

    Есть ли у вас какие-либо идеи о том, как выравнивание памяти работает точно в C ++, поскольку, похоже, есть некоторые различия?

Этот предыдущий вопрос содержит слово «выравнивание», но он не дает никаких ответов на поставленные выше вопросы.

Kane
источник
Компиляторы C ++ более склонны делать это (вставлять отступы там, где это необходимо или полезно) для вас. По ссылке, которую вы упомянули, посмотрите в разделе 12 «Инструменты» вещи, которые вы можете использовать.
Rwong

Ответы:

11

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

Возьмите этот цикл, две инструкции имеют значение, если вы запускаете достаточно циклов.

.globl ASMDELAY
ASMDELAY:
    subs r0,r0,#1
    bne ASMDELAY
    bx lr

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

min      max      difference
00016DDE 003E025D 003C947F

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

То же самое с доступом к данным. Некоторые архитектуры жалуются на невыровненный доступ (например, при выполнении 32-битного чтения по адресу 0x1001), сообщая о сбое данных. Некоторые из них вы можете отключить сбой и принять удар производительности. Другие, которые разрешают несогласованные доступы, просто получают удар по производительности.

Иногда это «инструкции», но в большинстве случаев это циклы часов / шин.

Посмотрите на реализации memcpy в gcc для различных целей. Скажем, вы копируете структуру размером 0x43 байта, вы можете найти реализацию, которая копирует один байт, оставляя 0x42, а затем копирует 0x40 байтов большими эффективными блоками, а затем последний 0x2, который он может сделать как два отдельных байта или как 16-битную передачу. Выравнивание и цель вступают в игру, если адреса источника и назначения находятся на одном и том же выравнивании, скажем, 0x1003 и 0x2003, тогда вы можете сделать один байт, затем 0x40 большими блоками, затем 0x2, но если один равен 0x1002, а другой 0x1003, то он получает очень уродливый и очень медленный

Большую часть времени это автобусные циклы. Или хуже количество переводов. Возьмите процессор с шиной данных 64-битной ширины, такой как ARM, и выполните передачу четырех слов (чтение или запись, LDM или STM) по адресу 0x1004, то есть адрес с выравниванием по словам, и вполне допустимый, но если шина равна 64 в битах шириной вероятно, что одна инструкция превратится в три передачи, в этом случае 32 бита в 0x1004, 64 бита в 0x1008 и 32 бита в 0x100A. Но если бы у вас была та же инструкция, но по адресу 0x1008, она могла бы выполнить однократную передачу четырех слов по адресу 0x1008. Каждой передаче соответствует время установки. Таким образом, разница адресов от 0x1004 до 0x1008 может быть в несколько раз быстрее, даже / esp при использовании кеша, и все это попадания в кеш.

Говоря о том, что даже если вы выполняете чтение двух слов по адресу 0x1000 против 0x0FFC, 0x0FFC с пропусками кэша вызовет две операции чтения строки кэша, где 0x1000 - одна строка кэша, у вас есть штраф за чтение строки кэша в любом случае для случайного доступ (чтение большего количества данных, чем использование), но тогда это удваивается. То, как ваши структуры выровнены или ваши данные в целом, а также ваша частота доступа к этим данным и т. Д., Может привести к перегрузке кэша.

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

Все, что мы добавили для повышения производительности, более широкие шины данных, конвейеры, кэши, прогнозирование ветвлений, множественные исполнительные блоки / пути и т. Д. Наиболее часто помогает, но у всех них есть слабые места, которые можно использовать намеренно или случайно. Компилятор или библиотеки мало что могут с этим поделать, если вам интересна производительность, которую вам нужно настроить, и одним из самых больших факторов настройки является выравнивание кода и данных, а не только выравнивание по 32, 64, 128, 256 битовые границы, но также там, где все относительно друг друга, вы хотите, чтобы интенсивно используемые циклы или повторно используемые данные не попадали в один и тот же способ кэширования, каждый из которых хочет иметь свой собственный. Компиляторы могут помочь, например, упорядочить инструкции для суперскалярной архитектуры, переупорядочив инструкции, которые не имеют значения,

Самым большим упущением является предположение, что процессор является узким местом. Уже десять или более лет это не так, проблема заключается в питании процессора, и именно здесь возникают такие проблемы, как снижение производительности выравнивания, перегрузка кэша и т. Д. Небольшая работа даже на уровне исходного кода, переупорядочение данных в структуре, упорядочение объявлений переменных / структур, упорядочение функций в исходном коде и немного дополнительного кода для выравнивания данных могут в несколько раз повысить производительность или Больше.

Старожил
источник
+1 если только для вашего последнего абзаца. Пропускная способность памяти является наиболее важной проблемой для всех, кто пытается сегодня написать быстрый код, а не счетчик команд. А это значит, что чрезвычайно важна оптимизация для уменьшения потерь в кеше, что может быть достигнуто путем изменения выравнивания во многих обстоятельствах.
Жюль
Если ваш код и данные становятся кэшированными, и вы выполняете достаточное количество циклов / циклов для этих данных, тогда количество команд и то, где инструкции лежат в строке выборки, где ветви попадают в канал относительно того, на что они полагаются, имеют значение. Но в системах, основанных на драмах и / или флэш-памяти, вам в первую очередь нужно беспокоиться о питании процессора.
old_timer
15

Да, выравнивание памяти все еще имеет значение.

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

Хорошей новостью является то, что на самом деле вам не нужно заботиться. Практически любой компилятор для почти любого языка будет производить машинный код, который соответствует требованиям выравнивания целевой системы. Вам нужно только начать думать об этом, если вы берете прямой контроль над представлением своих данных в памяти, что необязательно так часто, как это было раньше. Это интересная вещь, и абсолютно важно знать, хотите ли вы понять использование памяти из различных структур, которые вы создаете, и как, возможно, реорганизовать вещи, чтобы сделать их более эффективными (избегая заполнения). Но если вам не нужен такой контроль (а для большинства систем вы просто не нуждаетесь), вы можете счастливо пройти всю карьеру, не зная и не заботясь об этом.

Мэтью Уолтон
источник
1
В частности, ARM не поддерживает неприсоединенный доступ. И это процессор почти все, что мобильный использует.
Ян Худек
Также обратите внимание, что Linux эмулирует несогласованный доступ при некоторых затратах времени выполнения, но Windows (CE и Phone) этого не делают, и попытка несогласованного доступа просто приведет к сбою приложения.
Ян Худек
2
Хотя это в основном верно, обратите внимание, что некоторые платформы (включая x86) предъявляют различные требования к выравниванию в зависимости от того, какие инструкции будут использоваться , что не так-то просто для самого компилятора, так что вам иногда нужно набирать отступы, чтобы убедиться, что некоторые операции (например, инструкции SSE, многие из которых требуют выравнивания по 16 байтов) могут использоваться для некоторых операций. Кроме того, добавление дополнительного заполнения, чтобы два элемента, которые часто используются вместе, находились в одной и той же строке кэша (также 16 байтов), в некоторых случаях может оказать огромное влияние на производительность, а также не автоматизировано.
Жюль
3

Да, это все еще имеет значение, и в некоторых алгоритмах, критичных к производительности, вы не можете полагаться на компилятор.

Я собираюсь перечислить только несколько примеров:

  1. Из этого ответа :

Обычно микрокод извлекает правильное 4-байтовое количество из памяти, но если он не выровнен, ему придется извлечь два 4-байтовых местоположения из памяти и восстановить желаемое 4-байтовое количество из соответствующих байтов двух местоположений

  1. Набор инструкций SSE требует специального выравнивания. Если оно не выполнено, вы должны использовать специальные функции для загрузки и сохранения данных в невыровненной памяти. Это означает две дополнительные инструкции.

Если вы не работаете над алгоритмами, критичными к производительности, просто забудьте о выравнивании памяти. Это не очень нужно для нормального программирования.

BЈовић
источник
1

Мы склонны избегать ситуаций, когда это важно. Если это важно, это важно. Нераспределенные данные имели место, например, при обработке двоичных данных, чего, как кажется, в настоящее время избегают (люди часто используют XML или JSON).

Если вам каким-то образом удастся создать невыровненный массив целых чисел, то на типичном процессоре Intel ваш код обрабатывает этот массив немного медленнее, чем для выровненных данных. На процессоре ARM он работает немного медленнее, если вы сообщаете компилятору, что данные не выровнены. Он может либо работать ужасно, очень медленно, либо давать неправильные результаты, в зависимости от модели процессора и операционной системы, если вы используете невыровненные данные, не сообщая компилятору.

Объяснение ссылки на C ++: В C все поля в структуре должны храниться в порядке возрастания памяти. Таким образом, если у вас есть поля char / double / char и вы хотите, чтобы все было выровнено, у вас будет один байт, семь байт не используются, восемь байт-двойной, один байт-символ, семь байт не используются. В структурах C ++ то же самое для совместимости. Но для структур, компилятор может переупорядочивать поля, поэтому у вас может быть один байтовый символ, другой байтовый символ, шесть байтов неиспользованные, 8 байтов двойные. Использование 16 вместо 24 байтов. В структурах C разработчики обычно избегают такой ситуации и, в первую очередь, имеют поля в другом порядке.

gnasher729
источник
1
Нераспределенные данные происходят в памяти. Программы, которые не имеют должным образом упакованных структур данных, могут понести огромные потери производительности даже для, казалось бы, несущественного порядка значений. Например, в многопоточном коде два значения в одной строке кэша вызовут массовые задержки конвейера, когда два потока обращаются к ним одновременно (разумеется, игнорируя проблемы безопасности потока).
Greyfade
Компилятор C ++ может переупорядочивать поля только при определенных условиях, которые, вероятно, не будут выполнены, если вы не знаете этих правил. Кроме того, я не знаю ни одного компилятора C ++, который бы на самом деле использовал эту свободу.
Sjoerd
1
Я никогда не видел, чтобы C-компилятор переупорядочивал поля. Например, я видел много вставок и выравнивание между символами / целыми числами ..
PaulHK
1

Многие хорошие моменты уже упоминались в ответах выше. Просто для того, чтобы добавить даже во не встроенные системы, которые занимаются поиском / извлечением данных, производительность памяти имеет большое значение, и время доступа настолько важно, что кроме кода сборки выравнивания пишется для того же самого.

Я также рекомендую почитать: http://dewaele.org/~robbe/thesis/writing/references/what-every-programmer-should-know-about-memory.2007.pdf

Варун Мишра
источник
1

Насколько важно выравнивание памяти? Это все еще имеет значение?

Да. Нет, это зависит

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

Ваше приложение будет иметь меньший объем памяти и работать быстрее, если оно правильно выровнено. В типичном настольном приложении это не будет иметь значения, за исключением редких / нетипичных случаев (например, ваше приложение всегда заканчивается тем же узким местом производительности и требует оптимизации). То есть приложение будет меньше и быстрее при правильном выравнивании, но для большинства практических случаев оно не должно так или иначе влиять на пользователя.

Есть ли у выравнивания памяти другие преимущества? Я где-то читал, что процессор работает лучше / быстрее с выровненной памятью, потому что для его обработки требуется меньше инструкций (если у кого-то есть ссылка на статью / тест по этому поводу?), В этом случае разница действительно значительна? Есть ли больше преимуществ, чем эти два?

Может быть. Это что-то, что (возможно) нужно учитывать при написании кода, но в большинстве случаев это просто не должно иметь значения (то есть я все еще располагаю свои переменные-члены по размеру памяти и частоте доступа - что должно облегчить кэширование - но я делаю это для простота использования / чтения и рефакторинга кода, а не для целей кэширования).

Есть ли у вас какие-либо идеи о том, как выравнивание памяти работает точно в C ++, поскольку, похоже, есть некоторые различия?

Я читал об этом, когда вышел инструмент alignof (C ++ 11?), С тех пор я не беспокоился об этом (в настоящее время я занимаюсь в основном настольными приложениями и разработкой серверных приложений).

utnapistim
источник