Почему запись в 24-битную структуру не является атомарной (когда запись в 32-битную структуру выглядит так)? - PullRequest
12 голосов
/ 09 февраля 2011

Я тинкер - в этом нет сомнений. По этой причине (и совсем немного дальше) я недавно провел небольшой эксперимент, чтобы подтвердить мое подозрение, что запись в 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, на самом деле это может произойти, если значение равно "mid «назначение» одновременно копируется в другое место отдельным потоком.

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

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

Следующее, что я попробовал - опять же, просто догадка - была структура, состоящая из 32 битов с использованием полей 2 short. Это, казалось, также демонстрировало «атомарную присваиваемость». Но затем Я попробовал 24-битную структуру, используя 3 byte поля: no go .

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

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

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

Ответы [ 4 ]

14 голосов
/ 09 февраля 2011

Шаблон, который вы ищете, является собственным размером слова CPU.

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

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

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

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

5 голосов
/ 09 февраля 2011

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

12.5 Атомность ссылок на переменные Чтение и запись следующих типов данных должны быть атомарными: bool, char, byte, sbyte, short, ushort, Типы uint, int, float и reference. Кроме того, читает и пишет перечислимые типы с базовым типом в предыдущем списке также должен быть атомарный. Чтение и запись других типов, включая long, ulong, double, и десятичные, а также определяемые пользователем типы не обязательно должны быть атомарными. ( выделение мое ) Помимо функций библиотеки, разработанных для этой цели нет гарантии атомарного чтения-изменения-записи, например, в случае увеличения или декремент.
Ваш пример X = Y = Z = value является сокращением для 3 отдельных операций присваивания, каждая из которых определена как атомарная по 12.5. Последовательность из 3 операций (присвойте 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 (Управление макетом экземпляра)) используется для изменить поведение по умолчанию, элементы данных не превышают естественный размер слова ( размер native int) должен быть правильно выровнен. Ссылки на объекты должны быть обработаны как будто они хранятся в собственном размере слова.

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

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

3 голосов
/ 09 февраля 2011

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

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

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

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

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

3 голосов
/ 09 февраля 2011

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

...