Могут ли изменчивые, но незащищенные чтения давать бесконечно устаревшие значения? (на реальном оборудовании)
Отвечаяэтот вопрос возник еще один вопрос о ситуации ОП, в котором я не был уверен: это в основном вопрос архитектуры процессоров, но также и вопрос о модели памяти C ++ 11.
По сути, код OP зацикливался на более высоких уровнях оптимизации из-за следующего кода (слегка измененного для простоты):
while (true) {
uint8_t ov = bits_; // bits_ is some "uint8_t" non-local variable
if (ov & MASK) {
continue;
}
if (ov == __sync_val_compare_and_swap(&bits_, ov, ov | MASK)) {
break;
}
}
где__sync_val_compare_and_swap()
встроенный в GCC атомарный CAS. GCC (разумно) оптимизировал это в бесконечный цикл в случае, еслиbits_ & mask
было обнаружено, чтоtrue
перед входом в цикл полностью пропустите операцию CAS, поэтому я предложил следующее изменение (которое работает):
while (true) {
uint8_t ov = bits_; // bits_ is some "uint8_t" non-local variable
if (ov & MASK) {
__sync_synchronize();
continue;
}
if (ov == __sync_val_compare_and_swap(&bits_, ov, ov | MASK)) {
break;
}
}
После того как я ответил, OP отметил, что изменениеbits_
вvolatile uint8_t
кажется, работает также. Я предложил не идти по этому пути, так какvolatile
обычно не следует использовать для синхронизации, и, похоже, в любом случае здесь не так уж много недостатков.
Тем не менее, я думал об этом больше, и в этом случае семантика такова, что это не имеет значения, еслиov & MASK
проверка основана на устаревшем значении, если оно не основано на неопределенно устаревшем значении (то есть, пока цикл в конечном итоге прерывается), так как фактическая попытка обновленияbits_
синхронизирован. Так этоvolatile
здесь достаточно, чтобы гарантировать, что этот цикл в конце концов завершится, еслиbits_
обновляется другим потоком, так чтоbits_ & MASK == false
для любого существующего процессора? Другими словами, при отсутствии явного ограничения памяти, возможно ли практически, чтобы операции чтения, не оптимизированные компилятором, эффективно оптимизировались процессором, на неопределенный срок? (РЕДАКТИРОВАТЬ: Чтобы быть ясным, я спрашиваю здесь о том, что на самом деле может делать современное оборудование, учитывая предположение, что чтения передаются в цикле компилятором, так что это технически не вопрос языка, хотя его выражение в терминах семантики C ++ удобно.)
Это аппаратный аспект, но чтобы немного его обновить и сделать также ответным вопросом о модели памяти C ++ 11, рассмотрим следующий вариант кода выше:
// bits_ is "std::atomic<unsigned char>"
unsigned char ov = bits_.load(std::memory_order_relaxed);
while (true) {
if (ov & MASK) {
ov = bits_.load(std::memory_order_relaxed);
continue;
}
// compare_exchange_weak also updates ov if the exchange fails
if (bits_.compare_exchange_weak(ov, ov | MASK, std::memory_order_acq_rel)) {
break;
}
}
cppreference заявляет, чтоstd::memory_order_relaxed
подразумевает «отсутствие ограничений на переупорядочение обращений к памяти вокруг атомарной переменной», поэтому независимо от того, что будет или не будет делать настоящее аппаратное обеспечение, подразумевается, чтоbits_.load(std::memory_order_relaxed)
может техническиникогда прочитать обновленное значение послеbits_
обновляется в другом потоке в соответствующей реализации?
РЕДАКТИРОВАТЬ: Я нашел это в стандарте (29,4 стр. 13):
Реализации должны сделать атомные хранилища видимыми для атомных нагрузок в течение разумного периода времени.
Таким образом, очевидно, что ожидание «бесконечно долгого» обновленного значения исключено (главным образом?), Но нет никакой твердой гарантии того, что какой-либо конкретный временной интервал свежести, кроме этого, должен быть «разумным»; Тем не менее, вопрос о реальном поведении оборудования стоит.