Так же, как C ++, аппаратный CAS (например, x86-64 или ARMv8.1) не поддерживает это в asm, вам придется свернуть свой собственный.
В C ++ это довольно просто: загрузить исходное значение и замените его часть. Это, конечно, может привести к ложному сбою, если другое ядро изменило другую часть, с которой вы не хотели сравнивать.
Если возможно, используйте unsigned m_index
вместо size_t
, чтобы вся структура могла умещаются в 8 байтах на типичных 64-битных машинах вместо 16. 16-байтовые атомики медленнее (особенно часть чистой загрузки) на x86-64 или даже не блокируются вообще в некоторых реализациях и / или некоторые ISA. См. Как я могу реализовать счетчик ABA с c ++ 11 CAS? re: x86-64 lock cmpgxchg16b
с текущим GCC / clang.
Если каждый доступ atomic<>
отдельно требует блокировки , было бы намного лучше просто взять мьютекс вокруг всего пользовательского сравнения и установки.
Я написал простую реализацию одной попытки CAS (например, cas_weak
) в качестве примера. Вы могли бы использовать его в специализации шаблона или производном классе std::atomic<Data>
, чтобы предоставить новую функцию-член для atomic<Data>
объектов.
#include <atomic>
struct Data {
// without alignment, clang's atomic<Data> doesn't inline load + CAS?!? even though return d.is_always_lock_free; is true
alignas(long long) char m_data;
unsigned m_index; // this last so compilers can replace it slightly more efficiently
};
inline bool partial_cas_weak(std::atomic<Data> &d, unsigned expected_idx, Data zz, std::memory_order order = std::memory_order_seq_cst)
{
Data expected = d.load(std::memory_order_relaxed);
expected.m_index = expected_idx; // new index, same everything else
return d.compare_exchange_weak(expected, zz, order);
// updated value of "expected" discarded on CAS failure
// If you make this a retry loop, use it instead of repeated d.load
}
На практике это хорошо компилируется с clang для x86-64 ( Godbolt ), встраивание в вызывающий объект, который передает константу времени компиляции order
(иначе clang идет неистовым ветвлением по этому order
arg для автономной не встроенной версии функции)
# clang10.0 -O3 for x86-64
test_pcw(std::atomic<Data>&, unsigned int, Data):
mov rax, qword ptr [rdi] # load the whole thing
shl rsi, 32
mov eax, eax # zero-extend the low 32 bits, clearing m_index
or rax, rsi # OR in a new high half = expected_idx
lock cmpxchg qword ptr [rdi], rdx # the actual 8-byte CAS
sete al # boolean FLAG result into register
ret
К сожалению, компиляторы слишком глупы, чтобы загружать только ту часть структуры atomi c, которая им действительно нужна, вместо этого загружая все это целиком, а затем обнуляя часть, которую они не хотели . (См. Как я могу реализовать счетчик ABA с c ++ 11 CAS? для хаков объединения, чтобы обойти это на некоторых компиляторах.)
К сожалению, G CC создает беспорядочный asm, который хранит / перезагружает временные файлы в стек, что приводит к остановке пересылки магазина. G CC также обнуляет заполнение после char m_data
(будь то первый или последний член), что может приводить к тому, что CAS всегда терпит неудачу, если у фактического объекта в памяти было ненулевое заполнение. Это может быть невозможно, если чистые хранилища и инициализация всегда сводятся к нулю.
LL / S C машина вроде ARM или PowerP C могла бы легко сделать это при сборке (сравнение / ветвление выполняется вручную, между привязкой к загрузке и условным хранилищем), но нет библиотек, которые переносят это. (Самое главное, потому что он не может компилироваться для таких машин, как x86, и потому, что то, что вы можете делать в транзакции LL / S C, сильно ограничено, и сброс / перезагрузка локальных переменных в режиме отладки может привести к коду, который всегда терпит неудачу. )