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

Говоря о модели памяти С++ для concurrency, язык программирования Stroustrup С++, 4-е изд., раздел. 41.2.1, говорит:

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

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

#include <iostream>
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
        [...]

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

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

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

Что говорит Страуструп, пожалуйста?

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

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

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

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

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

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

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

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

Ответ 1

TL: DR: На каждом современном ISA, который имеет инструкции по хранению байтов (включая x86), они являются атомарными и не мешают окружающим байтам. (Я не знаю ни одного более старого ISA, в котором инструкции по хранению байтов могли "изобретать записи" в соседние байты).

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



О фразе Страуструпа

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

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

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


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

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

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


Однако в некоторых высокопроизводительных проектах для ISA не-x86 используется атомарный цикл RMW для внутренней фиксации хранилищ в кэше L1d. Существуют ли какие-либо современные процессоры, где кэшированное хранилище байтов на самом деле медленнее, чем хранилище слов? Строка кэша все время находится в состоянии MESI Exclusive/Modified, поэтому она не может вызвать проблем с корректностью, только незначительное снижение производительности. Это сильно отличается от того, что можно делать в магазинах других процессоров. (Приведенные ниже аргументы о том, что этого не происходит, все еще применимы, но мое обновление, возможно, пропустило некоторые вещи, которые все еще утверждают, что Atomic cache-RMW маловероятен.)

(На многих ISA, отличных от x86, невыровненные хранилища вообще не поддерживаются или используются реже, чем в программном обеспечении x86. А слабо упорядоченные ISA позволяют больше объединяться в буферах хранилища, поэтому не так много инструкций байтового хранилища фактически приводят к фиксация байта в L1d. Без этих побуждений для причудливого (энергозатратного) оборудования для доступа к кэшу слово RMW для разбросанных хранилищ байтов является приемлемым компромиссом в некоторых проектах.)


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

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

Оказывается, некоторые конвейерные конструкции с высокой производительностью действительно используют атомарное слово-RMW для фиксации в L1d, несмотря на то, что это останавливает конвейер памяти, но (как я утверждаю ниже) гораздо менее вероятно, что любой из них сделает внешне видимый RMW в RAM.

Word-RMW также не является полезной опцией для хранилищ байтов MMIO, поэтому, если у вас нет архитектуры, которая не требует хранилищ подслов для ввода-вывода, вам понадобится какая-то специальная обработка для ввода-вывода (например, Alpha разреженный I/Пространство, в котором загрузка слов/хранилища были сопоставлены с загрузкой байтов/хранилищами, чтобы можно было использовать обычные PCI-карты вместо необходимости в специальном оборудовании без регистров байтового ввода-вывода).

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

Сколько циклов и циклов какого размера потребуется для выполнения длинного слова, передаваемого в ЦП, показывает, как микроконтроллер ColdFire сигнализирует о размере передачи (строка в байтах/словах/длинном слове /16-байтовых) внешними сигнальными линиями, позволяя ему выполнять загрузку/сохранение байтов если 32-битная память была подключена к ее 32-битной шине данных. Нечто подобное, по-видимому, типично для большинства настроек шины памяти (но я не знаю). Пример ColdFire сложен тем, что его также можно настроить на использование 16 или 8-битной памяти, что требует дополнительных циклов для более широких передач. Но не важно, что важным моментом является то, что он имеет внешнюю сигнализацию для размера передачи, чтобы сообщить HW памяти, какой байт он фактически записывает.


Страуструп следующий абзац

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

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

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

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


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

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

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

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


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


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

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

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


ИДК, как медленные инструкции загрузки/сохранения байтов в любых процессорах, где они реализованы аппаратно, но не так дешево, как загрузки/хранения слов. movzx/movsx байтов на x86 movzx/movsx дешево, если вы используете movzx/movsx чтобы избежать частичной регистрации ложных зависимостей или слияния остановок. На предварительных movsx AMD для movsx/movzx требуется дополнительная movzx ALU, но в противном случае расширение нуля/знака обрабатывается прямо в порту загрузки на процессорах Intel и AMD. Основным недостатком x86 является то, что вам нужна отдельная инструкция загрузки вместо использования операнда памяти в качестве источника для инструкции ALU (если вы добавляете байт с нулевым расширением к 32-разрядному целому числу), сохраняя входной uop пропускная способность и размер кода. Или, если вы просто добавляете байт в регистр байтов, то у x86 нет недостатка. RISC-хранилище Для ISA всегда нужны отдельные инструкции по загрузке и хранению. Хранилища x86 байтов не дороже 32-битных.

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

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


PS: подробнее об альфе:

Альфа интересна по многим причинам: один из немногих 64-битных ISA с чистого листа, а не расширение существующего 32-битного ISA. И один из более поздних ISA с "чистым сланцем", Itanium - еще один, созданный несколькими годами позже, в котором были предприняты некоторые изящные идеи архитектуры CPU.

Из Linux Alpha HOWTO.

Когда была представлена архитектура Alpha, она была уникальной среди архитектур RISC для избежания 8-битной и 16-битной загрузки и хранения. Он поддерживал 32-битные и 64-битные загрузки и хранения (длинное слово и четырехзначное слово в цифровой номенклатуре). Соавторы (Дик Сайтс, Рич Витек) обосновали это решение, сославшись на преимущества:

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

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

Ответ 2

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

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

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

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

Ответ 3

Не уверен, что означает Страустпап под словом "СЛОВО". Может быть, это минимальный размер памяти устройства?

В любом случае не все машины были созданы с разрешением 8 бит (BYTE). На самом деле я рекомендую эту удивительную статью Эрика С. Раймонда, описывающую некоторые истории компьютеров: http://www.catb.org/esr/faqs/things-every-hacker-once-knew/

"... Он также широко известен, что 36-разрядные архитектуры объяснил некоторые неудачные особенности языка C. Оригинал Unix-машина, PDP-7, отличалась 18-битовыми словами, соответствующими полуслова на более крупных 36-разрядных компьютерах. Это было более естественно представлены как шесть восьмеричных (3-разрядных) цифр."

Ответ 4

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

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

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

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

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

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

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

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

ИЗМЕНИТЬ

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

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

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

Ответ 5

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

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

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

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

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

// shared state
char c[16];
FILE *file[16];

void threadFunc(int id)
{
    while ((c[id] = getc(file[id])) != EOF)
    {
        // ...
    }
}

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

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

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

Ответ 6

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

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

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

В зависимости от бюджета и наследия проекта память может выставлять более широкую шину в одиночку или вместе с некоторыми сигналами боковой полосы, чтобы выбрать в ней конкретный блок.
Что это значит практически? Если вы посмотрите на таблицу DDR3 DIMM, вы увидите, что есть 64 вывода DQ0-DQ63 для чтения/записи данных.
Это шина данных, 64-битная ширина, 8 байтов за раз.
Эта 8-байтная вещь очень хорошо обоснована в архитектуре x86 до такой степени, что Intel ссылается на нее в разделе WC руководства по оптимизации, где говорится, что данные передаются из буфера заполнения 64 байта (помните: мы игнорируем кэши для теперь, но это похоже на то, как возвращается строка кэша) в пакетах из 8 байтов (надеюсь, постоянно).

Означает ли это, что x86 может писать только QWORDS (64-разрядные)?
Нет, в той же таблице показано, что каждый модуль DIMM имеет сигналы DM0-DM7, DQ0-DQ7 и DQS0-DQS7 для маскировки, прямого и стробирования каждого из 8 байтов в 64-битной шине данных.

Итак, x86 может читать и писать байты изначально и атомарно.
Однако теперь легко понять, что это не может быть для каждой архитектуры.
Например, видеопамять VGA была DWORD (32-разрядной) адресуемой и позволяла ей вписываться в адресный мир байтов 8086, приводивший к беспорядочным битовым плоскостям.

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

Существует поворот: мы только что говорили о шине данных памяти, это самый низкий уровень. Некоторые процессоры могут иметь инструкции, которые создают адресную память байтов поверх адресной памяти. Что это значит? Легко загрузить меньшую часть слова: просто отбросьте остальные байты!
К сожалению, я не могу вспомнить название архитектуры (если она даже существовала вообще!), Где процессор моделировал нагрузку неуравновешенного байта, читая согласованное слово, содержащее его, и поворачивая результат перед сохранением его в регистре.

С магазинами дело сложнее: если мы не можем просто написать часть слова, которое мы только что обновили, нам также нужно написать неизменную оставшуюся часть.
Процессор или программист должны прочитать старый контент, обновить его и записать обратно.
Это операция Read-Modify-Write, и это ключевая концепция при обсуждении атомарности.

Рассмотрим:

/* 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;

Есть ли гонка данных?
Это безопасно для x86, потому что они могут писать байты, но что, если архитектура не может?
Оба потока должны были бы прочитать весь массив foo, изменить его и записать обратно.
В псевдо-C это будет

/* 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;

Теперь мы можем видеть, о чем говорил Страуструп: два магазина *((int*)foo) = tmpX препятствуют друг другу, чтобы увидеть это, рассмотрим эту возможную последовательность выполнения:

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  */

Если у С++ не было модели памяти, эти виды помех были бы конкретными деталями реализации, оставив С++ бесполезным языком программирования в многопоточной среде.

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

Я не починил модель памяти С++, но обновляю различные элементы массива в порядке.
Это очень сильная гарантия.

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

В среде с несколькими кодами/сокетами когерентный протокол кэширования гарантирует, что только один процессор может свободно записывать в кэшированную строку памяти (процессор, который имеет его в эксклюзивном или модифицированном состоянии). < ш > В принципе, семейство протоколов MESI использует концепцию, аналогичную блокировке, найденную в СУБД.
Это означает, что для целей написания "назначение" различных областей памяти для разных ЦП.
Таким образом, это не влияет на обсуждение выше.