Может ли современное оборудование x86 не хранить один байт в памяти?

Говоря о модели памяти C ++ для параллелизма, Stroustrup’s C ++ Programming Language, 4th ed., Sect. 41.2.1, говорит:

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

Тем не менее, мой процессор x86, несколько лет назад, может хранить и хранить объекты меньше, чем слово. Например:

#include  int main() { char a = 5; char b = 25; a = b; std::cout << int(a) << "\n"; return 0; } 

Без оптимизации GCC компилирует это как:

  [...] movb $5, -1(%rbp) # a = 5, one byte movb $25, -2(%rbp) # b = 25, one byte movzbl -2(%rbp), %eax # load b, one byte, not extending the sign movb %al, -1(%rbp) # a = b, one byte [...] 

Комментарии принадлежат мне, но assembly осуществляется GCC. Конечно, это нормально.

Очевидно, я не понимаю, о чем говорит Страуструп, когда он объясняет, что аппаратное обеспечение может загружать и хранить ничего меньше слова. Насколько я могу судить, моя программа ничего не делает, кроме загрузки и хранения объектов, меньших, чем слова.

Глубокий фокус C ++ на нулевой стоимости, аппаратно-абстракционирующие абстракции задает C ++ отдельно от других языков программирования, которые легче освоить. Поэтому, если Stroustrup имеет интересную ментальную модель сигналов на автобусе или что-то еще такого рода, то я хотел бы понять модель Страуструпа.

О чем говорит Страуструп, пожалуйста?

ДОЛГОЙ ЦИТАТЫ С КОНТЕКСТОМ

Вот цитата Stroustrup в более полном контексте:

Рассмотрим, что может произойти, если компоновщик выделил [переменные типа char типа] c и b в одном и том же слове в памяти и (как и большинство современных аппаратных средств), машина не могла загружать или хранить что-либо меньшее, чем слово …. Без колодца -определенная и разумная модель памяти, stream 1 может читать слово, содержащее b и c , изменять c и записывать слово обратно в память. В то же время stream 2 может сделать то же самое с b . Затем, какой бы нити не удалось прочитать слово первым, и какой бы нити не удалось записать свой результат обратно в память, последний определял бы результат ….

ДОПОЛНИТЕЛЬНЫЕ ЗАМЕЧАНИЯ

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

Я проверил спецификации аппаратного обеспечения моего процессора. Электрически мой процессор (мост Intel Ivy Bridge), похоже, обращается к памяти DDR3L с помощью своего рода 16-разрядной схемы мультиплексирования, поэтому я не знаю, что это значит. Мне непонятно, что это имеет много общего с точкой Страустрапа.

Строуструп – умный человек и выдающийся ученый, поэтому я не сомневаюсь, что он воспринимает что-то разумное. Я запутался.

См. Также этот вопрос. Мой вопрос напоминает связанный вопрос несколькими способами, и ответы на связанный вопрос также полезны здесь. Тем не менее, мой вопрос касается и аппаратной / шинной модели, которая мотивирует C ++ таким, каким она есть, и это заставляет Stroustrup писать то, что он пишет. Я не ищу ответа только относительно того, что формально гарантирует стандарт C ++, но также хочет понять, почему это гарантировал бы стандарт C ++. Какова основная мысль? Это тоже часть моего вопроса.

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

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

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


Я думаю, что Stroustrup говорит о том, как процессоры работают внутри, чтобы реализовать инструкции по хранению байтов. Он предполагает, что ЦП без четко определенной и разумной модели памяти может реализовать байтовый магазин с неатомным RMW содержащего слова в строке кэша или в памяти для ЦП без кеша.

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

Alpha AXP, высокопроизводительный дизайн RISC с 1992 года, лихо (и уникально среди современных несовместимых с ЦСП ISA) пропущенных байтов загрузки / хранения инструкций до Alpha 21164A (EV56) в 1996 году . По-видимому, они не считали word-RMW жизнеспособным вариантом для реализации байтовых магазинов, потому что один из приведенных преимуществ для реализации только 32-битных и 64-разрядных выровненных хранилищ был более эффективным ECC для кеша L1D. «Традиционный SECDED ECC потребует 7 дополнительных бит над 32-битными гранулами (22% накладных расходов) по сравнению с 4 дополнительными битами над 8-битными гранулами (50% накладных расходов)». (Ответ @Paul A. Clayton о адресной или байтовой адресации имеет некоторые другие интересные объекты компьютерной архитектуры.) Если бы байтовые магазины были реализованы со словом-RMW, вы все равно могли бы выполнять обнаружение / коррекцию ошибок с детализацией слов.

По этой причине текущие процессоры Intel используют только коэффициент четности (не ECC) в L1D. См. Этот вопрос и ответы об аппаратном обеспечении (не), исключающем «тихие хранилища»: проверка старого содержимого кеша перед записью, чтобы избежать маркировки линии, грязной, если она соответствует, для RMW требуется вместо RM, а это главное препятствие.

Я предполагаю, что другие (не x86) современные конструкции CPU не рассматривали RMW как вариант для хранения байтов в кэше L1D. Word-RMW не является полезной опцией для байт-хранилищ MMIO , поэтому, если у вас нет архитектуры, которая не нуждается в хранилищах под-слов для ввода-вывода, вам потребуется какая-то специальная обработка для ввода-вывода (например , O, где загрузка / хранилище слов была сопоставлена ​​байтовой загрузке / хранению, чтобы она могла использовать товарные PCI-карты вместо необходимости специального оборудования без байтовых регистров ввода-вывода).

Как отмечает @Margaret , controllerы памяти DDR3 могут создавать байтовые магазины, устанавливая управляющие сигналы, которые маскируют другие байты пакета. Те же самые механизмы, которые получают эту информацию в controllerе памяти (для нераскрытых хранилищ), также могут передавать эту информацию вместе с загрузкой или хранилищем в пространство MMIO. Таким образом, существуют аппаратные механизмы для реального создания байтового хранилища даже в пакетно-ориентированных системах памяти, и весьма вероятно, что современные процессоры будут использовать это вместо реализации RMW, потому что это, вероятно, проще и намного лучше для корректности MMIO.


Следующий параграф Страуструпа

«Модель памяти C ++ гарантирует, что два streamа выполнения могут обновлять и получать доступ к отдельным ячейкам памяти, не мешая друг другу . Это именно то, чего мы наивно ожидаем. Задача компилятора защитить нас от иногда очень странного и тонкого поведения современное оборудование. Как достигается компилятор и аппаратная комбинация, которая зависит от компилятора … »

Поэтому, по-видимому, он считает, что реальное современное оборудование не может обеспечить «безопасную» загрузку / хранение байтов. Люди, которые разрабатывают модели аппаратной памяти, соглашаются с людьми C / C ++ и понимают, что инструкции по хранению байтов не будут очень полезны для программистов / компиляторов, если они могут наступать на соседние байты.

Все современные (не DSP) архитектуры, кроме раннего Alpha AXP, имеют инструкции по хранению и загрузке байтов, и AFAIK все они определены по архитектуре, чтобы не влиять на соседние байты. Однако они достигают этого в аппаратном обеспечении, программное обеспечение не должно заботиться о правильности. Даже самая первая версия MIPS (в 1983 году) имела байтовые и полуслои нагрузки / магазины, и это очень ориентированная на слова ISA.

Тем не менее, он на самом деле не утверждает, что для большинства современных аппаратных средств требуется специальная поддержка компилятора для реализации этой части модели памяти C ++, только некоторые из них . Возможно, он действительно говорит только о адресных DSP-адресах в этом втором абзаце (где реализации C и C ++ часто используют 16 или 32-битный char как раз то, о чем говорилось в статье, о которой говорил Stroustrup.)


Большинство «современных» процессоров (включая все x86) имеют кэш L1D. Они будут извлекать целые строки кэша (как правило, 64 байта) и отслеживать грязные / нечистоты на основе каждого кэша. Таким образом, два смежных байта в точности совпадают с двумя смежными словами, если они оба находятся в одной строке кэша. Запись одного байта или слова приведет к извлечению всей строки и, в конечном итоге, к записи всей строки. Смотрите Ульрих Дреппер, что каждый программист должен знать о памяти . Вы правы, что MESI (или производная, такая как MESIF / MOESI) гарантирует, что это не проблема. (Но опять же, это связано с тем, что аппаратное обеспечение реализует разумную модель памяти.)

Хранилище может передавать только кеш L1D, пока строка находится в состоянии Modified (MESI). Таким образом, даже если внутренняя аппаратная реализация медленна для байтов и занимает дополнительное время, чтобы объединить байт в содержащее слово в строке кэша, это, фактически, запись с правами на чтение атома, если она не позволяет линии быть недействительной и повторно – запрашивается между чтением и записью. ( Хотя этот кеш имеет строку в Модифицированном состоянии, никакой другой кеш не может иметь действительную копию ). См . Комментарий @ old_timer, создающий ту же точку (но также и RMW в controllerе памяти).

Это проще, чем, например, атомный xchg или add из регистра, которому также необходим ALU и доступ к регистру, поскольку все задействованные HW находятся на одном и том же этапе конвейера, который может просто остановиться на дополнительный цикл или два. Это, очевидно, плохо для производительности и требует дополнительного оборудования, чтобы этот этап трубопровода сигнализировал о его остановке. Это не обязательно противоречит первому требованию Страуструпа, потому что он говорил о гипотетической ISA без модели памяти, но она все еще растянута.

На одноядерном микроcontrollerе внутреннее слово-RMW для кэшированных байтовых магазинов было бы более правдоподобным, так как не будет запросов Invalidate, поступающих с других ядер, что им придется откладывать ответы во время обновления кэш-слова Atom RMW , Но это не помогает для ввода / вывода в несказуемые регионы. Я говорю о микроcontrollerе, потому что другие одноядерные процессоры обычно поддерживают какой-то многопроцессорный SMP.


Многие RISC ISAs не поддерживают нагрузки / хранилища неравнозначных слов с одной инструкцией, но это отдельная проблема (сложность заключается в обработке случая, когда загрузка охватывает две строки кэша или даже страницы, что не может произойти с байтами или выровненными полуслова). Тем не менее, все больше и больше ISA добавляют гарантированную поддержку для нестандартной загрузки / хранения в последних версиях. (например, MIPS32 / 64 Release 6 в 2014 году, и я думаю, что AArch64 и недавняя 32-разрядная ARM).


Четвертое издание книги было опубликовано в 2013 году, когда Alpha была мертва годами. Первое издание было опубликовано в 1985 году , когда RISC стала новой большой идеей (например, Stanford MIPS в 1983 году, согласно временному графику Википедии для вычисления HW , но «современные» процессоры в то время были байт-адресуемыми с байтовыми магазинами. Cyber ​​CDC 6600 был с точки зрения адреса и, возможно, все еще вокруг, но не может быть назван современным.

Даже очень ориентированные на слова RISC-машины, такие как MIPS и SPARC, имеют байтовые и байтовые нагрузки (со знаком или с нулевым расширением). Они не поддерживают несвязанные нагрузки на слова, упрощают кеш (или доступ к памяти, если нет кеша) и загружают порты, но вы можете загрузить любой один байт с одной инструкцией и, что более важно, хранить байты без перезаписи окружающих байтов.

Я полагаю, что C ++ 11 (который вводит модель памяти с поддержкой streamов для языка) на Alpha должен будет использовать 32-битный char если таргетинг на версию Alpha ISA без байтовых магазинов. Или ему пришлось бы использовать программное обеспечение atom-RMW с LL / SC, если бы оно не могло доказать, что никакие другие streamи не могут иметь указатель, который позволял бы им записывать соседние байты.


IDK, как медленные байты загрузки / хранения инструкций находятся в любых процессорах, где они реализованы на аппаратных средствах, но не так дешево, как загрузка / хранение слов. Байтовые нагрузки дешевы на x86 до тех пор, пока вы используете movzx/movsx чтобы избежать ложных зависимостей частичного регистра или слияния ларьков. На AMD pre-Ryzen movsx нуждается в дополнительном ALU uop, но в противном случае нулевой / расширитель знака обрабатывается прямо в порту загрузки на процессорах Intel и AMD. ) Основной недостаток x86 состоит в том, что вам нужна отдельная инструкция загрузки вместо использования операнда памяти в качестве источника для команды ALU, что позволяет сохранить пропускную способность пропускной способности и размер кода на уровне интерфейса. RISC load-store ISAs всегда должны иметь разные инструкции по загрузке и хранению. Хранилища x86 байтов не стоят дороже, чем 32-разрядные магазины.

В качестве проблемы с производительностью хорошая реализация C ++ для аппаратного обеспечения с медленными байтовыми магазинами может поместить каждый char в свое собственное слово и использовать по возможности (например, для глобальных структур и для локальных пользователей в стеке). IDK, если какие-либо реальные реализации MIPS / ARM / независимо имеют медленную загрузку / сохранение байта, но если это возможно, gcc имеет -mtune= опции для управления им.

Это не помогает для char[] или разыменования char * когда вы не знаете, где он может указывать. (Это включает в себя volatile char* который вы использовали бы для MMIO.) Таким образом, если компилятор + компоновщик помещает переменные char в отдельные слова, это не полное решение, просто хак производительности, если истинные байтовые магазины медленны.


Подробнее о Alpha:

Из Linux Alpha HOWTO .

Когда была внедрена архитектура Alpha, она была уникальной среди архитектур RISC для предотвращения 8-битных и 16-разрядных нагрузок и хранилищ. Он поддерживал 32-битные и 64-разрядные нагрузки и хранилища (longword и quadword, в номенклатуре Digital). Со-архитекторы (Dick Sites, Rich Witek) оправдали это решение, сославшись на преимущества:

  1. Поддержка байтов в подсистеме кеша и памяти имеет тенденцию замедлять доступ к 32-битным и 64-разрядным количествам.
  2. Байт-поддержка затрудняет сбор высокоскоростных схем коррекции ошибок в подсистеме кеш-памяти.

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

Не только процессоры x86 способны читать и писать один байт, все современные процессоры общего назначения способны на это. Что еще более важно, большинство современных процессоров (включая x86, ARM, MIPS, PowerPC и SPARC) способны атомизировать чтение и запись одиночных байтов.

Я не знаю, о чем говорил Страуструп. Раньше существовало несколько машин с адресованными адресами, которые не были способны к 8-разрядной байтовой адресации, например Cray, и, как отметил Питер Кордес, ранние процессоры Alpha не поддерживали байтовые нагрузки и хранилища, но сегодня единственные процессоры, неспособные к байту нагрузки и магазины – это определенные DSP, используемые в нишевых приложениях. Даже если предположить, что он означает, что большинство современных процессоров не имеют атомной байтовой нагрузки, и это не относится к большинству процессоров.

Однако простые атомные нагрузки и хранилища не имеют большого значения при многопоточном программировании. Вам также, как правило, нужны заказывающие гарантии и способ сделать операции чтения-изменения-записи атомарными. Другое соображение состоит в том, что, хотя CPU a может иметь байтовую загрузку и сохранение инструкций, компилятор не должен их использовать. Например, компилятор может генерировать код, который описывает Stroustrup, загружая как b и c используя одну команду загрузки слов в качестве оптимизации.

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

Автор, похоже, обеспокоен тем, что stream 1 и stream 2 попадают в ситуацию, когда чтение-изменение-запись (не в программном обеспечении, программное обеспечение выполняет две отдельные инструкции размера байта, где-то по линии логика должна делать чтение- изменение-запись) вместо идеального чтения изменить запись чтения читать модифицировать запись, становится прочитанной прочитанной модификацией модифицировать запись записи или другое время, чтобы как прочитать предварительно модифицированную версию, так и последнюю, чтобы написать победы. read read изменить изменить запись записи или прочитать изменение читать изменить писать писать или читать изменить читать писать изменить писать.

Забота состоит в том, чтобы начать с 0x1122, и один stream хочет сделать это 0x33XX, другой хочет сделать это 0xXX44, но, например, прочитайте чтение изменить изменить запись записи, вы получите 0x1144 или 0x3322, но не 0x3344

Разумный (системный / логический) дизайн просто не имеет этой проблемы, конечно, не для процессора общего назначения, подобного этому, я работал над проектами с такими проблемами времени, как это, но это не то, о чем мы говорим здесь, совершенно разные системные проекты для разных целей. Чтение-изменение-запись не охватывает достаточно длинное расстояние в разумном дизайне, а x86 – разумные конструкции.

Чтение-изменение-запись будет происходить очень близко к первой вовлеченной SRAM (в идеале L1 при обычной работе x86 с операционной системой, способной выполнять скомпилированные многопоточные программы на C ++) и происходит в течение нескольких тактов, на скорости автобуса идеально. И, как отметил Питер, это считается всей линией кэша, которая испытывает это в кэше, а не чтение-изменение-запись между kernelм процессора и кешем.

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

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

Вы можете попробовать это сделать, сделать многопоточную программу, которую один пишет, чтобы сказать адрес 0xnnn00000, другой пишет на адрес 0xnnnn00001, каждый из них записывает, затем читает или лучше записывает одно и то же значение, чем прочитанное, байт, который они пишут, затем повторяется с другим значением. Пусть это работает некоторое время, часы / дни / недели / месяцы. Посмотрите, если вы отключите систему … используйте сборку для настоящих инструкций по записи, чтобы убедиться, что она делает то, что вы просили (а не C ++ или какой-либо компилятор, который делает или утверждает, что он не будет помещать эти элементы в одно и то же слово). Могут добавить задержки, чтобы разрешить больше выseleniumий кеша, но это уменьшает ваши шансы «в то же время» столкновения.

Ваш пример, пока вы застрахованы от того, что вы не сидите с двух сторон границы (кеш или другое), например 0xNNNNFFFFF и 0xNNNN00000, изолируйте записи двух байтов на адреса, такие как 0xNNNN00000 и 0xNNNN00001, имеют инструкции назад и назад, и посмотрите, получаете ли вы чтение читать изменить изменить запись записи. Оберните тест вокруг него, чтобы два значения были разными в каждом цикле, вы читаете слово в целом на любой задержке позже, когда хотите, и проверяете два значения. Повторите для дней / недель / месяцев / лет, чтобы узнать, не сработает ли он. Прочтите информацию о функциях ваших процессоров и функциях микрокода, чтобы узнать, что он делает с этой последовательностью команд, и при необходимости создайте другую последовательность команд, которая пытается получить транзакции, инициированные в течение нескольких или нескольких тактовых циклов на дальней стороне ядра процессора.

РЕДАКТИРОВАТЬ

проблема с кавычками заключается в том, что речь идет о языке и использовании. «как и большинство современных аппаратных средств» ставит всю тему / текст в сложную позицию, он слишком расплывчатый, одна сторона может утверждать, что все, что мне нужно сделать, – это найти один случай, который верен, чтобы сделать все остальное истинным, аналогично одной стороне может возразить, если я найду один случай, все остальное не соответствует действительности. Используя слово вроде рода беспорядков с этим, как можно выйти из тюрьмы бесплатно карты.

Реальность заключается в том, что значительная часть наших данных хранится в DRAM в 8-битных памяти, так как мы не обращаемся к ним как к 8-битной шире, обычно мы получаем 8 из них за раз, 64 бит в ширину. В течение нескольких недель / месяцев / лет / десятилетий это утверждение будет неверным.

Большая цитата говорит «в то же время», а затем говорит, что читать … сначала, напишите … последнее, хорошо первое и последнее и в то же время не имеющее смысла вместе, параллельное или серийное? Контекст в целом относится к приведенному выше чтению, прочитайте изменения, измените изменения записи записи, где у вас есть одна запись в последний раз и в зависимости от того, когда этот прочитанный определяет, произошли или нет две модификации. Не в то время, когда «как и большинство современных аппаратных средств» не имеет смысла вещи, которые начинают фактически параллельно в отдельных ядрах / модулях, в конечном итоге становятся сериализованными, если они нацелены на один и тот же флип-флоп / транзистор в памяти, должен ждать, пока другой пойдет первым. Основываясь на физике, я не вижу, чтобы это было неправильно в ближайшие недели / месяцы / годы.

Это верно. Процессор x86_64, как и исходный процессор x86, не способен читать или записывать что-либо меньшее, чем (в данном случае 64-битное) слово из rsp. к памяти. И он обычно не будет читать или писать меньше, чем целая строка кэша, хотя есть способы обойти кеш, особенно в письменной форме (см. Ниже).

В этом контексте , однако, Stroustrup ссылается на потенциальные расы данных (отсутствие атомарности на наблюдаемом уровне). Эта проблема корректности не имеет значения для x86_64 из-за протокола когерентности кэша, о котором вы упомянули. Другими словами, да, процессор ограничен целыми переносами слов, но это прозрачно обрабатывается, и вы, как программист, вообще не должны беспокоиться об этом. Фактически, язык C ++, начиная с C ++ 11, гарантирует, что параллельные операции в разных ячейках памяти имеют четко определенное поведение, то есть то, которое вы ожидаете. Даже если аппаратные средства не гарантируют этого, реализация должна будет найти способ, создавая, возможно, более сложный код.

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

  • Во-первых, и это относится только к людям, которые пишут драйверы устройств или устройства проектирования, операции ввода-вывода с памятью могут быть чувствительны к способу обращения к ним. В качестве примера рассмотрим устройство, которое предоставляет 64-битный регистр команд только для записи в физическом адресном пространстве. Тогда может потребоваться:
    • Отключить кеширование. Недопустимо считывать строку кэша, изменять одно слово и записывать строку кэша. Кроме того, даже если бы это было действительно, все равно был бы большой риск того, что команды могут быть потеряны, потому что кэш-память процессора не скоро будет записана. По крайней мере, страница должна быть сконфигурирована как «сквозная запись», что означает, что записи вступают в силу немедленно. Таким образом, запись таблицы страниц x86_64 содержит флаги, которые контролируют поведение кэширования процессора для этой страницы .
    • Убедитесь, что все слово всегда написано на уровне сборки. Например, рассмотрим случай, когда вы записываете значение 1 в регистр, а затем 2. Компилятор, особенно при оптимизации пространства, может решить переписать только младший байт, потому что остальные уже должны быть равны нулю (то есть, для обычной ОЗУ), или вместо этого можно удалить первую запись, потому что это значение, по-видимому, будет немедленно перезаписано. Однако ни один из них не должен происходить здесь. В C / C ++ ключевое слово volatile имеет жизненно важное значение для предотвращения таких неподходящих оптимизаций.
  • Во-вторых, и это относится практически ко всем разработчикам, которые пишут многопоточные программы, протокол когерентности кеша, в то же время предотвращая катастрофу, может иметь огромные эксплуатационные издержки, если их «злоупотребляют».

Вот несколько придуманный пример очень плохой структуры данных. Предположим, что у вас есть 16 streamов, анализирующих некоторый текст из файла. Каждый stream имеет id от 0 до 15.

 // shared state char c[16]; FILE *file[16]; void threadFunc(int id) { while ((c[id] = getc(file[id])) != EOF) { // ... } } 

Это безопасно, потому что каждый stream работает в другом месте памяти. Однако эти ячейки памяти, как правило, находятся в одной и той же строке кэша, или, самое большее, разбиваются на две строки кэша. Затем когерентный протокол кеширования используется для правильной синхронизации доступа к c[id] . И здесь проблема заключается в том, что это вынуждает каждый другой stream ждать, пока линия кэша станет доступной только до того, как сделать что-либо с помощью c[id] , если только она уже не работает на ядре, которое «владеет» линией кэша. Предполагая, что несколько, например 16 ядер, когерентность кеша, как правило, все время передают линию кэша от одного ядра к другому. По понятным причинам этот эффект известен как «пинг-понг кеш-линии». Это создает ужасное узкое место в производительности. Это результат очень плохого случая ложного обмена , то есть streamов, разделяющих физическую строку кэша, без фактического доступа к тем же местам логической памяти.

В отличие от этого, особенно если принять дополнительный шаг к обеспечению того, чтобы массив file находился в собственной строке кэша, его использование было бы совершенно безвредным (на x86_64) с точки зрения производительности, поскольку указатели считываются только с самого начала , В этом случае несколько ядер могут «делиться» с линией кэша только для чтения. Только когда какое-либо kernel ​​пытается записать в строку кэша, он должен сказать другим ядрам, что он собирается «захватить» строку кэша для эксклюзивного доступа.

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

Not sure what Stroustrup meant by “WORD”. Maybe it is the minimum size of memory storage of the machine?

Anyway not all machines were created with 8bit (BYTE) resolution. In fact I recommend this awesome article by Eric S. Raymond describing some of the history of computers: http://www.catb.org/esr/faqs/things-every-hacker-once-knew/

“… It used also to be generally known that 36-bit architectures explained some unfortunate features of the C language. The original Unix machine, the PDP-7, featured 18-bit words corresponding to half-words on larger 36-bit computers. These were more naturally represented as six octal (3-bit) digits.”

Stroustrup is not saying that no machine can perform loads and stores smaller than their native word size, he is saying that a machine couldn’t .

While this seems surprising at first, it’s nothing esoteric.
For starter, we will ignore the cache hierarchy, we will take that into account later.
Assume there are no caches between the CPU and the memory.

The big problem with memory is density , trying to put more bits possible into the smallest area.
In order to achieve that it is convenient, from an electrical design point of view, to expose a bus as wider as possible (this favours the reuse of some electrical signals, I haven’t looked at the specific details though).
So, in architecture where big memories are needed (like the x86) or a simple low-cost design is favourable (for example where RISC machines are involved), the memory bus is larger than the smallest addressable unit (typically the byte).

Depending on the budget and legacy of the project the memory can expose a wider bus alone or along with some sideband signals to select a particular unit into it.
What does this mean practically?
If you take a look at the datasheet of a DDR3 DIMM you’ll see that there are 64 DQ0–DQ63 pins to read/write the data.
This is the data bus, 64-bit wide, 8 bytes at a time.
This 8 bytes thing is very well founded in the x86 architecture to the point that Intel refers to it in the WC section of its optimisation manual where it says that data are transferred from the 64 bytes fill buffer (remember: we are ignoring the caches for now, but this is similar to how a cache line gets written back) in bursts of 8 bytes (hopefully, continuously).

Does this mean that the x86 can only write QWORDS (64-bit)?
No, the same datasheet shows that each DIMM has the DM0–DM7 ,DQ0–DQ7 and DQS0–DQS7 signals to mask, direct and strobe each of the 8 bytes in the 64-bit data bus.

So x86 can read and write bytes natively and atomically.
However, now it’s easy to see that this could not be the case for every architecture.
For instance, the VGA video memory was DWORD (32-bit) addressable and making it fit in the byte addressable world of the 8086 led to the messy bit-planes.

In general specific purpose architecture, like DSPs, could not have a byte addressable memory at the hardware level.

There is a twist: we have just talked about the memory data bus, this is the lowest layer possible.
Some CPUs can have instructions that build a byte addressable memory on top of a word addressable memory.
Что это значит?
It’s easy to load a smaller part of a word: just discard the rest of the bytes!
Unfortunately, I can’t recall the name of the architecture (if it even existed at all!) where the processor simulated a load of an unaligned byte by reading the aligned word containing it and rotating the result before saving it in a register.

With stores, the matter is more complex: if we can’t simply write the part of the word that we just updated we need to write the unchanged remaining part too.
The CPU, or the programmer, must read the old content, update it and write it back.
This is a Read-Modify-Write operation and it is a core concept when discussing atomicity.

Рассматривать:

 /* Assume unsigned char is 1 byte and a word is 4 bytes */ unsigned char foo[4] = {}; /* Thread 0 Thread 1 */ foo[0] = 1; foo[1] = 2; 

Is there a data race?
This is safe on x86 because they can write bytes, but what if the architecture cannot?
Both threads would have to read the whole foo array, modify it and write it back.
In pseudo-C this would be

 /* Assume unsigned char is 1 byte and a word is 4 bytes */ unsigned char foo[4] = {}; /* Thread 0 Thread 1 */ /* What a CPU would do (IS) What a CPU would do (IS) */ int tmp0 = *((int*)foo) int tmp1 = *((int*)foo) /* Assume little endian Assume little endian */ tmp0 = (tmp0 & ~0xff) | 1; tmp1 = (tmp1 & ~0xff00) | 0x200; /* Store it back Store it back */ *((int*)foo) = tmp0; *((int*)foo) = tmp1; 

We can now see what Stroustrup was talking about: the two stores *((int*)foo) = tmpX obstruct each other, to see this consider this possible execution sequence:

 int tmp0 = *((int*)foo) /* T0 */ tmp0 = (tmp0 & ~0xff) | 1; /* T1 */ int tmp1 = *((int*)foo) /* T1 */ tmp1 = (tmp1 & ~0xff00) | 0x200; /* T1 */ *((int*)foo) = tmp1; /* T0 */ *((int*)foo) = tmp0; /* T0, Whooopsy */ 

If the C++ didn’t have a memory model these kinds of nuisances would have been implementation specific details, leaving the C++ a useless programming language in a multithreading environment.

Considering how common is the situation depicted in the toy example, Stroustrup stressed out the importance of a well-defined memory model.
Formalizing a memory model is hard work, it’s an exhausting, error-prone and abstract process so I also see a bit of pride in the words of Stroustrup.

I have not brushed up on the C++ memory model but updating different array elements is fine .
That’s a very strong guarantee.

We have left out the caches but that doesn’t really change anything, at least for the x86 case.
The x86 writes to memory through the caches, the caches are evicted in lines of 64 bytes .
Internally each core can update a line at any position atomically unless a load/store crosses a line boundary (eg by writing near the end of it).
This can be avoided by naturally aligning data (can you prove that?).

In a multi-code/socket environment, the cache coherency protocol ensures that only a CPU at a time is allowed to freely write to a cached line of memory (the CPU that has it in the Exclusive or Modified state).
Basically, the MESI family of protocol use a concept similar to locking found the DBMSs.
This has the effect, for the writing purpose, of “assigning” different memory regions to different CPUs.
So it doesn’t really affect the discussion of above.

  • Несинхронизированные статические методы streamобезопасны, если они не изменяют переменные статического classа?
  • Как прервать BlockingQueue, который блокирует take ()?
  • Как эта `this` ссылка на внешний class исчезает из-за публикации экземпляра внутреннего classа?
  • Простые числа Eratoshenes быстрее последовательны, чем одновременно?
  • Гарантии гарантий безопасности
  • Можем ли мы иметь условия гонки в однопоточной программе?
  • JavaFX2: Могу ли я приостановить фоновое задание / службу?
  • Как вы запрашиваете pthread, чтобы узнать, все ли работает?
  • Interesting Posts

    Bootstrap 3 Слайд в меню / Navbar на мобильном телефоне

    Сценарий Bash – Как ссылаться на файл для переменных

    Bootmgr отсутствует после восстановления с помощью sysresccd

    Зачем использовать бинарный поиск, если есть тройной поиск?

    Почему объект Object.toString () по умолчанию возвращает шестнадцатеричное представление hash-кода?

    Clonezilla диск для клонирования диска на двойной загрузке ubuntu karmic & XP setup – невозможно открыть '/boot/grub/device.map'

    как я могу включить интегрированный движок в mysql после установки?

    Беспроводная связь очень медленная на одном ноутбуке в сети, все остальные машины нормальные?

    Есть ли что-то вроде общего списка в Cocoa / Objective-C?

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

    Запуск Cygwin Inferior Shell в Emacs

    Каков предпочтительный способ доступа к физическим дискам виртуальных машин VMware?

    GIMP: изменить один цвет на другой?

    C # – установить разрешения для всех пользователей Windows 7

    Что такое ярлык клавиатуры для перехода на последнее сообщение в Mac OS X Mail.app?

    Давайте будем гением компьютера.