Как сделать группу операторов atomi c без эффектов видимости памяти? - PullRequest
7 голосов
/ 07 мая 2020
Блоки

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

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

Как мне создать группу операторов atomi c, без создания отношения «происходит раньше», то есть без эффектов видимости памяти synchronized / Lock?

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


В этом примере будет достаточно мьютекса без эффектов видимости памяти.

(release/acquire) int x; // Variable with release/acquire semantics

// release fence
synchronized (this) {

    int y = x;
    // acquire fence

    // release fence
    x = 5;

}
// acquire fence

Тот же набор ограждений испускается дважды (мьютексом и x). Вызывает ли это ненужные накладные расходы?


Теоретически возможна ли блокировка без эффектов памяти?
Будет ли блокировка без эффектов памяти действительно более производительной?
Есть ли встроенный способ выполнения sh это в C ++ и / или Java?
Если нет, может ли это быть реализовано на C ++ и / или Java?

Ответы [ 3 ]

6 голосов
/ 10 мая 2020

Затраты на обеспечение видимости памяти в мьютексе незначительны, фактически на x86 это бесплатно.

Для получения мьютекса требуется операция atomi c чтения-изменения-записи с семантикой получения. Для освобождения мьютекса достаточно использовать простое хранилище с семантикой выпуска. Рассмотрим простую спин-блокировку - операция получения состоит из al oop, который неоднократно пытается установить флаг блокировки в 1, если он в настоящий момент равен 0. Чтобы снять блокировку, поток-владелец просто записывает 0 во флаг блокировки. Во многих отношениях такая простая спин-блокировка далека от оптимальной, и существует множество конструкций для блокировок, которые пытаются это улучшить (например, справедливость, вращение в строках локального кэша и т. Д. c.), Но во всех этих конструкциях освобождение блокировка, безусловно, дешевле, чем ее приобретение.

Модель памяти x86 довольно сильна: все операции чтения-изменения-записи atomi c последовательно согласованы, все операции хранилища эффективно освобождаются, а все операции загрузки усвоить семантику. Вот почему на x86 выпуск мьютекса может быть выполнен с помощью обычного хранилища, никаких дополнительных инструкций для обеспечения видимости эффектов памяти не требуется. На архитектурах с более слабыми моделями памяти, такими как ARM или Power, вам действительно нужны дополнительные инструкции, но их стоимость ничтожна по сравнению со стоимостью операции получения. x86 также имеет специальные барьерные инструкции, но они обычно актуальны только в определенных случаях при программировании без блокировок, и стоимость этих инструкций примерно такая же, как у некоторых atomi c чтение-изменение записи.

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

Я не уверен, что вы имеете в виду под «теоретически возможна блокировка без эффектов памяти?». Вся цель мьютекса - позволить выполнять некоторые операции, а также наблюдать за ними, как если бы они были atomi c. Это означает, что результат операции становится видимым для следующего владельца мьютекса. Именно это и гарантирует отношение "происходит до". Если поток A получает мьютекс, и эта операция получения происходит - после операции освобождения некоторым потоком B , то из-за транзитивности отношения «происходит до» операции выполняются by B при удерживании мьютекса должно было произойти до того, как операция A вот-вот будет выполняться - и это означает, что все эффекты памяти должны быть видны . Если это не гарантировано, то ваш мьютекс сломан, и у вас есть состояние гонки.

Что касается изменчивой переменной в вашем примере - модель памяти Java требует, чтобы все операции с совместно используемыми изменчивыми переменными были последовательно согласованы. Однако, если x доступен только внутри критического раздела (т. Е. Защищен каким-либо мьютексом), то он не обязательно должен быть изменчивым. Volatile требуется только в том случае, если некоторые потоки обращаются к переменной без каких-либо других механизмов синхронизации, таких как мьютекс.

Семантика освобождения / получения операций мьютекса необходима для упорядочивания операций внутри мьютекс. В C ++ можно реализовать мьютекс, используя расслабленные операции. Операции блокировки / разблокировки на самом мьютексе по-прежнему будут полностью упорядочены (из-за порядка модификации мьютекса), но мы потеряем отношение «происходит до», поэтому операции внутри мьютекса будут неупорядоченный . Хотя это было бы возможно в C ++, это было бы довольно абсурдно, потому что, как я пытался объяснить, сделать видимыми эффекты памяти очень дешево (на x86 это бесплатно ), но вы потеряете свойство, которое абсолютно важно практически во всех случаях. Примечание: операция сохранения для освобождения мьютекса на дешевле , чем сохранение в изменчивую переменную. Изменчивые переменные последовательно согласованы, но освобождение мьютекса может быть выполнено с помощью хранилища релизов. (Конечно, модель памяти Java не такая гибкая, как модель C ++, поэтому вы не можете реализовать ручную блокировку, используя более расслабленные операции получения / освобождения).

1 голос
/ 11 августа 2020

Блокировка с ожиданием занятости без связи между выходом и входом «происходит раньше» может быть реализована следующим образом в Java:

private static final VarHandle STATE = ...;

private boolean state;

void lock() {
    while ((boolean) STATE.getAndSetRelease(this, true)) {
        while (state) {
            Thread.onSpinWait();
        }
    }
}

void unlock() {
    STATE.setOpaque(this, false);
}

В приведенном выше коде Thread.onSpinWait() на x86 предотвращает state от кеширования на неопределенный срок. На архитектурах, где это не так, вместо этого можно использовать следующее:

while ((boolean) STATE.getOpaque(this)) {}
1 голос
/ 14 мая 2020

Некоторое время я задавал точно такой же вопрос go.

Я решил для своего конкретного случая использования с помощью простого фрагмента кода. Другие варианты использования будут иметь другие оптимальные решения.


Пример использования 1: Горячий l oop, который необходимо проверить atomi c

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

Решение удивительно простое: проверять только каждые 1024 итерации. Почему 1024? Это степень двойки, поэтому любые операторы MOD оптимизируются для быстрого побитового вычисления AND. Подойдет любая другая степень двойки. Настройтесь по своему усмотрению.

После этого накладные расходы atomi c становятся незначительными по сравнению с работой, которую выполняет l oop.

Можно было бы реализовать другие, более сложные решения. Но этих строк достаточно:

// Within a hot loop running on a single core ...
int32_t local = 0;
if (local % 1024 == 0) // Optimised to a bitwise AND (checks if lower N bits are 0).
{ 
  // Check atomics, which may force the processor to synchronize state with another core.
}

Примеры использования 2 .... N: Библия в реальном времени

Есть отличная беседа о различных уровнях блокировки для другого использования случаев см .: Реальное время 101 - Дэвид Роуленд и Фабиан Ренн Джайлз - Встреча с C ++ 2019 .


Q. Возможна ли теоретическая блокировка без эффектов памяти?

  • A. Да. Если два потока были привязаны к одному и тому же ядру, тогда оба потока могли бы совместно использовать состояние с помощью регистров. Если два потока работают на разных ядрах, они могут совместно использовать состояние с использованием кэшированной памяти L1, L2 или L3, если они находятся рядом друг с другом на d ie. Если два потока работают на разных ядрах, то обычно они передают общее состояние с помощью flu sh в RAM.

Q. Будет ли блокировка без эффектов памяти более производительной?

  • A. Да. Однако это действительно применимо только к встроенной операционной системе или драйверам устройств, см. Обновление ниже.

Q. Есть ли встроенный способ выполнить sh это в C ++ и / или Java?

  • A. Нет. Но приведенные выше ответы могут быть очень близки, часто вообще избегая блокировок большую часть времени. Лучше потратить время на освоение хорошего профайлера.

Q. Если нет, можно ли это реализовать на C ++ и / или Java?

  • Q. Нет. Не реализовано c на языке высокого уровня, см. Обновление ниже.

Аппаратные прерывания полностью отличаются от программных прерываний. Аппаратное прерывание вызывает переключение указателя вторжения (IP) процессора на выполнение другой процедуры обслуживания. Таким образом, блокировка без эффектов памяти теоретически возможна, если на одном ядре работает много «потоков», а «потоки» (т. Е. Подпрограммы обслуживания прерываний, запускаемые аппаратными прерываниями) взаимодействуют через регистры в ЦП или, по крайней мере, внутренним кешем (L1, L2, L3), так как это не приводит к попаданию в оперативную память.

На практическом уровне это, вероятно, не имеет отношения к каким-либо языкам высокого уровня, таким как C ++ или Java, и, вероятно, не имеет отношения к процессам пользовательского режима в операционных системах высокого уровня, таких как Linux или Windows. Вероятно, это возможно только при использовании встроенной ОС, такой как QMX, или, возможно, при написании драйверов устройств режима ядра для Windows или Linux.

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

...