Почему запись в 24-битную структуру не является атомарной (при записи в 32-битную структуру)?

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

Я написал сообщение в блоге об этом, используя следующий тип в качестве иллюстрации:

struct SolidStruct
{
    public SolidStruct(int value)
    {
        X = Y = Z = value;
    }

    public readonly int X;
    public readonly int Y;
    public readonly int Z;
}

В то время как приведенное выше похоже на тип, для которого никогда не может быть правдой, что X != Y или Y != Z, на самом деле это может произойти, если значение является "промежуточным присвоением", в то же время оно копируется в другое место по отдельному потоку.

ОК, большое дело. Любопытство и немного больше. Но тогда у меня была такая догадка: мой 64-битный процессор должен действительно иметь возможность копировать 64 бита атомарно, не так ли? Так что, если бы я избавился от Z и просто застрял с X и Y? Это всего лишь 64 бита; их можно будет перезаписать за один шаг.

Конечно, это сработало. (я понимаю, что некоторые из вас, вероятно, сейчас бороздят ваши брови, думая: "Да, дух. Как это вообще интересно?" Юмор.) Конечно, у меня есть не знаю, гарантировано ли это или не дано моей системе. Я почти ничего не знаю о регистрах, пропущенных кешках и т.д. (Я буквально просто срываю термины, которые я слышал, не понимая их смысла); так что на данный момент это черный ящик.

Следующее, что я попробовал - снова, только на догадке, - это структура, состоящая из 32 бит с использованием полей 2 short. Казалось, что это тоже "атомарная уступчивость". Но потом я попробовал 24-битную структуру, используя поля 3 byte: не идти.

Внезапно структура оказалась восприимчивой к копиям "среднего назначения" еще раз.

До 16 бит с полями 2 byte: атомный снова!

Может кто-нибудь объяснить мне, почему это? Я слышал о "бит-упаковке", "линии кэширования", "выравнивании" и т.д., Но опять-таки, я действительно не знаю, что все это значит, и не имеет ли здесь значения. Но я чувствую, что вижу образец, не имея возможности сказать точно, что это такое; ясность была бы с благодарностью.

Ответ 1

Образец, который вы ищете, - это родной размер слова процессора.

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

По прошествии времени размер собственного элемента увеличился до 32 бит, а затем до 64 бит. В каждом случае была добавлена ​​инструкция для обработки этого определенного количества бит. Однако для обратной совместимости старые инструкции все еще поддерживались, поэтому ваш 64-разрядный процессор может работать со всеми предыдущими собственными размерами.

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

Однако размер собственного элемента не был 24 бита. Следовательно, нет единой инструкции для записи 24 бит, поэтому для этого требуется несколько инструкций, и вы теряете атомарность.

Ответ 2

Стандарт С# (ISO 23270: 2006, ECMA-334) это говорит об атомарности:

12.5 Атоматичность ссылок переменных Считывание и запись следующих типов данных должна быть атомарной: bool, char, byte, sbyte, short, ushort, uint, int, float и reference. Кроме того, считывает и записывает типы перечислений с базовым типом в предыдущем списке также должны быть атомарными. Чтение и запись других типов, включая длинные, улунговые, двойные, и десятичные, а также пользовательские типы, не обязательно должны быть атомарными. (выделение мое) Помимо функций библиотеки, разработанных для этой цели нет гарантии атомарного чтения-модификации-записи, например, в случае приращения или decment.
Ваш пример X = Y = Z = value - короткая рука для трех отдельных операций присваивания, каждая из которых определена как атомарная на 12.5. Последовательность из трех операций (назначьте value в Z, назначьте Z на Y , присвойте Y - X) не гарантированно быть атомарным.

Так как спецификация языка не требует атомарности, а X = Y = Z = value; может быть атомной операцией, независимо от того, является она или нет, зависит от целого ряда факторов:

  • капризы авторов компилятора
  • какие опции оптимизации генерации кода, если они есть, были выбраны во время сборки
  • детали компилятора JIT, ответственного за превращение сборки IL в машинный язык. Идентичный IL, выполняемый под Mono, скажем, может проявлять другое поведение, чем при работе под .Net 4.0 (и это может даже отличаться от более ранних версий .Net).
  • конкретный процессор, на котором выполняется сборка.

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

Далее, посетив стандарт CLI (ISO 23217: 2006), мы найдем раздел 12.6.6:

12.6.6 Атомные чтения и записиСоответствующий CLI должен гарантировать правильное чтение и запись выровненные ячейки памяти не больше, чем собственный размер слова (размер типа native int) является атомарным (см. п. 12.6.2), когда все обращения на запись к местоположению такого же размера. Атомные записи не должны изменять никаких битов, кроме написанных. Если не явное управление компоновкой (см. раздел II (контроль макета экземпляра)) используется для изменить поведение по умолчанию, элементы данных не превышают размер естественного слова ( размер a native int) должны быть правильно выровнены. Ссылки на объекты должны обрабатываться как будто они хранятся в размере родного слова.

[ Примечание: нет гарантии об атомном обновлении (чтение-изменение-запись) памяти, за исключением методов, предусмотренных для эта цель как часть библиотеки классов (см. раздел IV). (выделение мое) Атомная запись "маленького элемента данных" (элемент, размер которого не превышает размер родного слова) требуется выполнить атомарное чтение/изменение/запись на аппаратном обеспечении, которое не поддерживает прямой записывается в небольшие элементы данных. end note]

[Примечание. Гарантированный атомный доступ к 8-байтным данным отсутствует, если размер собственный int равен 32 бит, хотя некоторые реализации могут выполнять атомарную когда данные выравниваются по 8-байтовой границе. end note]

Ответ 3

Операции CPU

x86 выполняются в 8, 16, 32 или 64 битах; манипулирование другими размерами требует нескольких операций.

Ответ 4

Компилятор и процессор x86 будут осторожны, чтобы перемещать только столько же байтов, сколько определяет структура. Нет инструкций x86, которые могут перемещать 24 бита за одну операцию, но есть отдельные команды для 8, 16, 32 и 64-битных данных.

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

Некоторые компиляторы позволяют вам определять заполнение на структурах, чтобы заставить их вести себя как данные с регистровым регистром. Если вы поместите свою 24-битную структуру, компилятор добавит еще один байт, чтобы "округлить" размер до 32 бит, чтобы всю структуру можно было перемещать в одной атомной инструкции. Недостаток вашей структуры всегда будет занимать на 30% больше места в памяти.

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

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