Справочник функций

Ваш аккаунт

Войти через: 
Забыли пароль?
Регистрация
Информацию о новых материалах можно получать и без регистрации:

Почтовая рассылка

Подписчиков: -1
Последний выпуск: 19.06.2015

Оперативная память. Эпизод I. Физическое устройство

Автор: Шаймарданов Булат
8 августа 2006 года

Введение

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

Вначале было слово. И слово было два байта

Физически оперативная память представляет собой линейный носитель информации. Под линейностью подразумевается то, что байты памяти пронумерованы, начиная с нуля, и для доступа к каждому байту процессору необходимо указать его порядковый номер. Этот номер называется адресом. У машины имеется адресная шина и шина данных, и обмен информацией между процессором и памятью происходит следующим образом: на шине адреса процессор выставляет адрес, затем - если происходит операция чтения из памяти - на шине данных появляются данные, выставленные оперативной памятью, если же происходит запись - процессор сам выставляет на шину данных новое значение.

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

Во-первых, шина данных имеет 32 бита и передает 4 байта разом (байт содержит 8 бит, откуда легко посчитать, что в 32 битах - 4 байта). Нюанс состоит в том, что передаются ровно 4 байта в любом случае, даже если процессор запросил, скажем, один байт. Второй нюанс: шина передает не произвольные 4 байта, а идущую подряд последовательность из 4 байт, первый из которых должен иметь адрес, кратный 4. Чтобы это легче было понять, представьте себе, что память состоит на самом деле не из байт, а из неделимых 4-байтовых групп. Вот пример: мы запрашиваем байт номер 1002. Процессор находит ближайшее не большее число, кратное 4 (говорят - выравнивает адрес по границе двойного слова), получается 1000, и выставляет этот адрес на адресной шине. Контроллер памяти выдает на шине данных байты с тысячного по 1003-й. Процессор берет 1002-й, остальные игнорирует.

Во-вторых, адресная шина также 32-битная и, таким образом, имеется возможность адресовать байты от 0 до 2^32-1 - итого чуть более 4 млрд. байт. Тот факт, что по шине данных передаются "неделимые 4-байтовые группы", дает небольшую экономию адресной шине - ведь младшие 2 бита адреса будут всегда содержать 0 (т. к. адрес кратен четырем), и поэтому в них нет необходимости. Фактически адресная шина 30-битная.

Какие проблемы может породить такой принцип работы памяти? В качестве примера рассмотрим программу, в которой имеется переменная размером как раз 4 байта (говорят - "двойное слово"), и проанализируем два варианта:

  • Переменная выровнена по границе двойного слова (т. е. адрес первого байта кратен четырем). В таком случае процессор, выставив на адресной шине её адрес, в один приём читает значение этой переменной.
  • Переменная не выровнена по границе двойного слова. Предположим, ее адрес равен 1002. Тогда процессор считает ближайшее не большее кратное четырем, получается 1000, читает двойное слово по этому адресу - там находятся байты 1000, 1001, 1002, 1003. Ему нужны последние два, остальные он игнорирует. Затем, выставив на адресной шине адрес 1004, считывает с 1004 по 1007 байты, из которых берет 1004 и 1005-й. И, наконец, "сливает" вместе 1002 и 1003-й байты, полученные в результате первой операции, и 1004-1005, полученные в результате второй.

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

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

Соответственно, на системах с 16-битной шиной размер так называемого машинного слова составлял 2 байта, и данные нужно было выравнивать по 2-байтной границе. Адресная шина была также 16-битной (и адресовала 2^16 = 65536 = всего лишь 64 Кб!), но позже она расширилась до 20 бит и стала адресовать около 1 Мб (здесь возникают свои нюансы, но об этом - в следующей статье), далее ее расширили до 24 бит и она стала адресовать 16 Мб. Экономия на адресной шине была - один бит.

А вот появляющиеся в последнее время 64-битные шины имеют ширину как адресной, так и шины данных 64 бит (насколько известно автору).

Кэш

Алгоритмы кэширования используются в общем-то довольно часто и реализованы как аппаратно, так и программно. Кэширование означает ускорение доступа к данным за счет того, что часто используемые данные помещаются в более быстрый носитель. Как видим, здесь участвуют два носителя: массивный и недорогой (имеется в виду удельная стоимость на единицу информации) и быстродействующий и сравнительно дорогой и потому менее объемистый. При обращении к массивному носителю прочитанные из него данные помещаются в специально выделенную область быстродействующего носителя, называемую кэшем, чтобы в дальнейшем, когда программе вновь потребуются эти данные, она могла их прочитать из кэша за более короткое время. Что значит "часто используемые" - определяется конкретной реализацией кэша. Что делать, когда кэш переполняется, или некоторые элементы кэша перестают часто использоваться и часто используются другие данные - решается также программистом или производителем.

Применительно к кэшу оперативной памяти алгоритм таков. Под часто используемыми данными на самом деле подразумеваются данные, к которым обращение происходило недавно, поэтому требует хранения времени последнего обращения к каждому элементу кэша. Кэширование происходит при каждом чтении, т. е. при каждой операции чтения из памяти микросхемы кэша помещают эти данные в кэш. В момент переполнения кэша из него "выкидываются" данные, обращение к которым было давно. Поэтому алгоритм был назван LRU (least recently used). если процессор захочет записать данные в оперативную память, данные сначала модифицируются в кэше (если их там не было, они туда помещаются), а последующие действия зависят от стратегии записи. Кэш с прямой записью (write through) сразу записывает данные в оперативную память, кэш с отложенной (write back) записью может записать их позже.

Данные кэшируются целыми строками (размер строки может меняться для разных реализаций). Предположим, длина строки составляет 32 байта, это будет означать, что в кэш помещаются блоки идущих подряд 32 байт. Кроме того, происходит выравнивание на 32-байтную границу - первый байт блока будет иметь адрес, кратный 32. Как раз это и важно для программиста, так как здесь появляются возможности для оптимизации. Например: если у вас имеются две переменные, которые часто используются вместе, старайтесь хранить их как можно ближе друг к другу, так, чтобы они попали в одну строку кэша - тогда не будет тратиться время на чтение каждой из переменных из оперативной памяти, так как обе переменные будут прочитаны в одну строку кэша при первом же обращении к одной из них. И наоборот: данные, используемые раздельно, лучше хранить подальше, так, чтобы они попали в разные строки кэша, иначе операции перезаписи и перечитывания строки кэша будут происходить слишком часто, и эффективность кэширования снизится. Особенно это касается внутренних данных каждого потока, если ваше приложение многопотоковое.

Таковы основные аспекты работы кэша оперативной памяти, которые необходимо знать программисту. Остается добавить для любознательных некоторую информацию по многопроцессорным системам: у каждого процессора в таких системах свой кэш, но специальные механизмы поддерживают когерентность кэшей в том случае, если вдруг некоторая область памяти окажется кэшированной сразу несколькими процессорами. Этот механизм работает по протоколу MESI (modified - exclusive - shared - invalid) и предусматривает 4 соответствующих состояния для строки кэша. Однако углубляться в MESI мы не будем. В качестве примера кэш-оптимизации многопотоковых приложений можете посмотреть статью "Неблокирующие межпроцессные коммуникации".

Читайте в следующей статье: логическая организация памяти - сегменты и страницы. Преимущества страничной трансляции. Отображение файлов на память и общий доступ к данным через отображение файлов на память.

Оставить комментарий

Комментарий:
можно использовать BB-коды
Максимальная длина комментария - 4000 символов.
 

Комментарии

1.
350
01 июня 2006 года
cheburator
589 / / 01.06.2006
+2 / -0
Мне нравитсяМне не нравится
9 августа 2006, 13:15:35
Статья "Неблокирующие межпроцессные коммуникации" находится по адресу http://www.codenet.ru/progr/cpp/dotnet/Lock-Free-IPC.php
Реклама на сайте | Обмен ссылками | Ссылки | Экспорт (RSS) | Контакты
Добавить статью | Добавить исходник | Добавить хостинг-провайдера | Добавить сайт в каталог