как процессор читает память? - PullRequest
2 голосов
/ 09 февраля 2020

Я пытаюсь заново реализовать Mallo c, и мне нужно понять цель выравнивания. Насколько я понимаю, если память выровнена, код будет выполняться быстрее, потому что процессору не придется делать дополнительный шаг для восстановления обрезанных битов памяти. Мне кажется, я понимаю, что 64-разрядный процессор читает 64-разрядную 64-разрядную память. Теперь давайте представим, что у меня есть структура с порядком (без отступов): char, short, char и int. Почему короткие будут смещены? У нас есть все данные в блоке! Почему он должен быть на адресе, кратном 2. Тот же вопрос для целых чисел и других типов?

У меня также есть второй вопрос: со структурой, которую я упомянул ранее, как процессор узнает, когда он читает свои 64 бита, что первые 8 битов соответствуют символу, а следующие 16 соответствуют короткому c ...

Ответы [ 3 ]

4 голосов
/ 09 февраля 2020

Эффекты могут даже включать в себя корректность, а не только производительность: C Неопределенное поведение (UB), приводящее к возможным ошибкам сегмента или другому неправильному поведению, если у вас есть short объект, который не удовлетворяет alignof(short). (Отказ ожидается на ISA, где инструкции загрузки / сохранения требуют выравнивания по умолчанию, например, SPAR C и MIPS перед MIPS64r6)

Или разрыв операций atomi c, если _Atomic int не имеет alignof(_Atomic int).

(Обычно alignof(T) = sizeof(T) до некоторого размера, часто регистрируют ширину или шире, в любом данном ABI).


malloc должен возвращать память с alignof(max_align_t), поскольку у вас нет никакой информации о типе того, как будет использоваться распределение.

Для распределений, меньших sizeof(max_align_t), вы можете возвращает память, которая выровнена просто естественным образом (например, 4-байтовое распределение выровнено на 4 байта), если вы хотите, потому что вы знаете, что память не может использоваться ни для чего с более высоким требованием выравнивания.

Over-over выровненный материал, такой как динамически размещенный эквивалент alignas (16) int32_t foo, должен использовать специальный распределитель, такой как C11 aligned_alloc. Если вы реализуете свою собственную библиотеку-распределитель, вы, вероятно, захотите поддерживать align_reallo c и align_callo c, заполняя те пробелы, которые ISO C оставляют без видимой причины.

И убедитесь, что вы не реализует требование braindead ISO C ++ 17 для сбоя aligned_alloc, если размер выделения не кратен выравниванию. Никто не хочет, чтобы распределитель отклонял распределение из 101 числа с плавающей запятой, начиная с 16-байтовой границы, или намного больше для прозрачных огромных страниц. align_allo c требования к функциям и Как решить проблему 32-байтового выравнивания для операций загрузки / сохранения AVX?


Я думаю Я понимаю, что 64-разрядный процессор читает 64-разрядную 64-разрядную память

Нет. Ширина шины данных и размер пакета, а также максимальная ширина модуля загрузки / хранения или фактически используемая ширина не обязательно должны быть такими же, как ширина целочисленных регистров, или, тем не менее, процессор определяет его битность. (А в современных высокопроизводительных процессорах, как правило, нет. Например, 32-битный P5 Pentium имел 64-битную шину; современный 32-битный ARM имеет инструкции по загрузке / сохранению в паре, которые выполняют атомный c 64-битный доступ.)

Процессоры считывают целые строки кэша из кэша DRAM / L3 / L2 в кэш L1d; 64 байта на современном x86; 32 байта в некоторых других системах.

И при чтении отдельных объектов или элементов массива они читают из кэша L1d с шириной элемента. например, массив uint16_t может получить выгоду только от выравнивания по 2-байтовой границе для 2-байтовых загрузок / хранилищ.

Или, если компилятор векторизует все oop с SIMD, массив uint16_t может быть читать 16 или 32 байт за один раз, то есть векторы SIMD из 8 или 16 элементов. (Или даже 64 с AVX512). Выравнивание массивов по ожидаемой ширине вектора может быть полезным; загрузка / хранение SIMD без выравнивания выполняется быстро на современном x86, когда они не пересекают границу строки кэша.


Расщепление строки кэша и особенно разбиение страницы - это то место, где современный x86 замедляется от смещения; Выровненный в пределах строки кэша обычно не потому, что они тратят транзисторы на быструю невыровненную загрузку / хранение. Некоторые другие ISA замедляются, а некоторые даже ошибаются при любом смещении, даже в пределах строки кэша. Решение такое же: дать типам естественное выравнивание: alignof (T) = sizeof (T).

В вашем примере структуры современные процессоры x86 не будут наказываться, даже если short выровнен неправильно. alignof(int) = 4 в любом нормальном ABI, поэтому вся структура имеет alignof(struct) = 4, поэтому блок char;short;char начинается с 4-байтовой границы. Таким образом, short содержится в одном 4-байтовом мече, не пересекающем более широкой границы. AMD и Intel справляются с этим с полной эффективностью. (И ISA x86 гарантирует, что доступ к нему осуществляется через атомы c, даже не кэшированные, на процессорах, совместимых с P5 Pentium или новее: Почему целочисленное присваивание для естественно выровненной переменной atomi c на x86? )

Некоторые ЦП, отличные от x86, будут иметь штрафы за неправильно выровненное короткое замыкание или должны будут использовать другие инструкции. (Поскольку вы знаете выравнивание относительно выровненного 32-разрядного блока, для нагрузок вы, вероятно, выполняете 32-разрядную загрузку и сдвиг.)

Так что да, нет проблем с доступом к одному слову, содержащему short, но проблема заключается в том, что аппаратное обеспечение порта загрузки извлекает и расширяет нулями (или расширяет знак), что short, в полный регистр. Здесь x86 тратит транзисторы, чтобы сделать это быстрым. (В ответе @ Eri c на предыдущую версию этого вопроса более подробно рассказывается о необходимом сдвиге.)

Передача невыровненного хранилища обратно в кеш также нетривиальна , Например, кэш-память L1d может иметь E CC (исправление ошибок по сравнению с битами) в 32-битных или 64-битных блоках (которые я назову «кеш-словами»). Таким образом, запись только части слова кеша является проблемой по этой причине, а также смещением его к произвольной границе байта внутри слова кеша, к которому вы хотите получить доступ. (Объединение смежных узких хранилищ в буфере хранилищ может привести к фиксации полной ширины, которая позволяет избежать цикла RMW для обновления части слова в кешах, которые обрабатывают узкие хранилища таким образом). Обратите внимание, что сейчас я говорю «слово», потому что я говорю об аппаратном обеспечении, которое более ориентировано на слова, а не спроектировано на основе выровненных загрузок / хранилищ, как современный x86. См. Существуют ли какие-либо современные процессоры, в которых хранилище кэшированных байтов на самом деле медленнее, чем хранилище слов? (сохранение одного байта лишь немного проще, чем у невыровненного short)

(Если short охватывает два слова кэша, то, конечно, потребуется разделить циклы RMW, по одному на каждый байт.)

И, конечно, short выровнен по той простой причине, что alignof(short) = 2 и это нарушает это правило ABI (при условии, что ABI имеет это). Поэтому, если вы передадите указатель на него какой-либо другой функции, вы можете столкнуться с проблемами. Особенно на процессорах, которые имеют ошибки при смещении нагрузки, вместо аппаратной обработки того случая, когда он оказывается смещенным во время выполнения. Тогда вы можете получить такие случаи, как Почему при выравнивании доступа к памяти mmap иногда происходит ошибка на AMD64? , где G CC, как ожидается, автоматическая векторизация G * 1143 достигнет 16-байтовой границы, делая несколько кратных 2-байтным элементы скалярные, поэтому нарушение ABI приводит к segfault на x86 (который обычно допускает смещение.)


Для получения полной информации о доступе к памяти, от задержки DRAM RAS / CAS до пропускной способности кэша и о выравнивании, см. Что должен знать каждый программист о памяти? Она все еще актуальна / применима

Также Цель выравнивания памяти имеет хороший ответ. Есть много других хороших ответов в теге SO.

Для более подробного ознакомления с (несколько) современными модулями загрузки / хранения Intel, см .: https://electronics.stackexchange.com/questions/329789/how-can-cache-be-that-fast/329955#329955


как процессор узнает, когда он читает свои 64 бита, что первые 8 бит соответствуют символу, а следующие 16 соответствуют короткому et c. ..?

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

В asm / machine -код, все только байты. Каждая инструкция указывает , что именно делать с какими данными. Компилятор (или человек-программист) должен реализовать переменные с типами и логи c программы C поверх необработанного массива байтов (основной памяти).

Что я это означает, что в asm вы можете запускать любые команды загрузки или сохранения, которые вам нужны, и вы должны использовать правильные инструкции по нужным адресам. Вы можете загрузить 4 байта, которые перекрывают две смежные переменные int, в регистр с плавающей запятой, а затем запустить на нем addss (FP-добавление с одинарной точностью), и процессор не будет жаловаться. Но вы, вероятно, не хотите этого делать, поскольку заставить процессор интерпретировать эти 4 байта как двоичное значение IEEE754 с двоичным значением 32 вряд ли имеет смысл.

1 голос
/ 09 февраля 2020

современные процессоры и память построены так, чтобы максимально оптимизировать доступ к памяти. Одним из современных способов доступа к памяти является обращение к ней не по байту, а по адресу большего блока, например, 8-байтовыми блоками. Таким образом, вам не нужно 3 младших бита адреса. Чтобы получить доступ к определенному байту в блоке, процессам необходимо получить блок по выровненному адресу, затем сдвинуть и замаскировать байт. Итак, это становится медленнее.

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

Но требования к присвоению основаны на базовой платформе. Для систем, которые поддерживают доступ к словам (32 бита), 4-байтовое выравнивание в порядке, в противном случае можно использовать 8-байтовый или какой-либо другой. Компилятор (и lib c) знают требования.

Итак, в вашем примере char, short, char, short будет начинаться с нечетной позиции байта, если не дополнено. Чтобы получить к нему доступ, система может прочитать 64-битное слово для структуры, затем сдвинуть ее на 1 байт вправо и затем замаскировать 2 байта, чтобы предоставить вам этот байт.

0 голосов
/ 09 февраля 2020

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

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

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

Это очень большая архитектура и спецификация конструкции шины c, и вы можете иметь архитектуры с разными шинами с течением времени или разными версиями, например, вы можете получить руку с шиной 64 или 32 бита. но допустим, что у нас неоптическая ситуация, когда шина имеет ширину 64 бита и все транзакции на этой шине выровнены на границе 64 бита.

Если бы я должен был выполнить 64-битную запись в 0x1000, которая была бы транзакцией с одной шиной, которая в наши дни является своего рода шиной адреса записи с некоторым идентификатором x и длиной 0 (n-1), то другая сторона подтверждает, что я вижу, что вы хотите сделать запись с идентификатором x, я готов принять ваши данные. Затем процессор использует шину данных с идентификатором x для отправки данных, один такт на 64 бита, это один 64-битный, поэтому один такт на этой шине. и, возможно, подтверждение возвращается, а может и нет.

Но если бы я захотел выполнить 64-битную запись в 0x1004, то получилось бы, что бы это превратилось в две транзакции - одну полную 64-битную транзакцию адрес / данные по адресу 0x1000 с только 4 байтовые дорожки позволили использовать дорожки 4-7 (представляющие байты по адресу 0x1004-0x1007). Затем завершена транзакция в 0x1008 с включенными 4-байтовыми дорожками, полосы 0-3. Таким образом, фактическое перемещение данных по шине происходит от одного часа до двух, но для достижения этих циклов данных также требуется в два раза больше рукопожатий. На этой шине очень заметно, какова общая конструкция системы, хотя вы можете чувствовать это или нет, или, возможно, придется сделать много из них, чтобы почувствовать это или нет. Но есть неэффективность, скрытая в шуме или нет.

Мне кажется, я понимаю, что 64-разрядный процессор читает 64-разрядную 64-разрядную память.

Не очень хорошее предположение на всех. В наши дни 32-битные ARM имеют 64-битные шины, например, ARMv6 и ARMv7 поставляются с ними или могут.

Теперь давайте представим, что у меня есть структура с порядком (без заполнения): char, короткий, символ и инт. Почему короткие будут смещены? У нас есть все данные в блоке! Почему он должен быть на адресе, кратном 2. Один и тот же вопрос для целых чисел и других типов?

unsigned char a   0x1000
unsigned short b  0x1001
unsigned char c   0x1003
unsigned int d    0x1004

Вы обычно используете элементы структуры в коде что-то. что-то .b что-то. c что-то.d. Когда вы получаете доступ к нечто .b, это 16-битная транзакция по шине. В 64-битной системе вы правы в том, что если выровнен, как я к нему обращался, то вся структура читается, когда вы делаете x = что-то .b, но процессор отбрасывает все, кроме байтовых дорожек 1 и 2 (отбрасывая 0 и 3-7), то при доступе к чему-либо. c он выполнит другую транзакцию шины в 0x1000 и отбросит все, кроме полосы 3. Когда вы делаете запись во что-то. включен. Теперь, когда возникает больше боли, если есть кеш, он, вероятно, также построен из 64-битного ОЗУ для сопряжения с этой шиной, не обязательно, но давайте предположим, что это так. Вы хотите записать через кеш что-то. b, транзакция записи в 0x1000 с байтовыми дорожками 1 и 2 включена 0, 3-7 отключена. Кэш, в конечном счете, получает эту транзакцию, ему необходимо выполнить запись с изменением чтения, поскольку она не является полной 64-битной транзакцией (все линии включены), поэтому вы получаете удар с этой записью с изменением чтения с точки зрения производительности. (то же самое было верно для 64-битной записи без выравнивания выше).

короткое положение не выровнено, потому что когда упакован его адрес lsbit, для выравнивания 16-битный элемент в 8-битном мире байтов должен быть ноль, для выравнивания 32-битного элемента младшие два бита его адреса равны нулю, 64-битному, трем нулям и т. д.

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

Ваши высокоэффективные процессоры, такие как MIPS и ARM, использовали подход выровненных команд и принудительно выровняли транзакции даже во что-то. В случае, если конкретно нет штрафа на 32 или 64-битной шине. Подход заключается в производительности по сравнению с потреблением памяти, поэтому инструкции в некоторой степени расточительны при их использовании, чтобы быть более эффективными при их извлечении и выполнении. Шина данных также намного проще. Когда создаются высокоуровневые концепции, такие как структура в C, происходит потеря памяти при заполнении каждого элемента в структуре для повышения производительности.

unsigned char a   0x1000
unsigned short b  0x1002
unsigned char c   0x1004
unsigned int d    0x1008

в качестве примера

У меня также есть второй вопрос: со структурой, о которой я упоминал ранее, как процессор узнает, когда он читает свои 64 бита, что первые 8 бит соответствуют символу, а следующие 16 соответствуют короткому et c .. .?

unsigned char c   0x1003

компилятор генерирует однобайтовое чтение по адресу 0x1003, это превращается в указанную инструкцию c с этим адресом, и процессор генерирует транзакцию шины для этого, Затем другая сторона процессорной шины выполняет свою работу и так далее.

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

возможно, что в зависимости от набора команд, prefetcher, cac и т. д. и т. д. вместо того, чтобы использовать структуру на высоком уровне, вы создаете одно 64-битное целое число и выполняете работу в коде, тогда вы можете повысить или не повысить производительность. Ожидается, что это не будет работать лучше на большинстве архитектур, работающих с кешами и тому подобным, но когда вы попадаете во встроенные системы, где у вас может быть некоторое количество состояний ожидания на оперативной памяти или некоторое количество состояний ожидания на флаге sh или любого другого кода В хранилище вы можете найти время, когда вместо меньшего количества инструкций и большего количества транзакций данных вам нужно больше инструкций и меньше транзакций данных. Код является линейным фрагментом кода, таким как чтение, маска и смещение, маска и смещение и т. д. хранилище команд может иметь пакетный режим для линейных транзакций, но транзакции данных занимают столько тактов, сколько они берут.

Промежуточная точка - просто сделать все 32-битной переменной или 64-битной, тогда все выровнено и относительно неплохо работает за счет увеличения используемой памяти.

Поскольку люди не понимают выравнивание, были испорчены программированием x86, решили использовать структуры в доменах компиляции (такая плохая идея), ARM и другие Допустив невыровненный доступ, вы можете ощутить снижение производительности на этих платформах, поскольку они настолько эффективны, если все выровнено, но когда вы делаете что-то не выровненное, оно просто генерирует больше шинных транзакций, делая все дольше. Таким образом, старые руки будут по умолчанию давать сбой, рука 7 может отключить ошибку, но будет вращать данные вокруг слова (хороший прием для замены 16-битных значений в слове), а не перетекать в следующее слово, более поздние архитектуры по умолчанию не ошибка на выровненных, или большинство людей устанавливает их не на ошибки на выровненных, и они читают / записывают невыровненные передачи, как можно было бы надеяться / ожидать.

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

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

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

...