Подтвердить что ты не робот

Как эффективно использовать std:: atomic

std:: atomic - новая функция, введенная С++ 11, но я не могу найти много учебников о том, как правильно ее использовать. Итак, следующая практика распространена и эффективна?

Одна практика, которую я использовал, - это буфер, и я хочу использовать CAS на некоторых байтах, так что я сделал это:

uint8_t *buf = ....
auto ptr = reinterpret_cast<std::atomic<uint8_t>*>(&buf[index]);
uint8_t oldValue, newValue;
do {
  oldValue = ptr->load();
  // Do some computation and calculate the newValue;
  newValue = f(oldValue);
} while (!ptr->compare_exchange_strong(oldValue, newValue));

Итак, мои вопросы:

  • В приведенном выше коде используется уродливый reinterpret_cast, и это правильный способ получить атомный указатель, ссылающийся на местоположение & buf [index]?
  • Является ли CAS на один байт значительно медленнее CAS на машинное слово, поэтому я должен избегать его использования? Мой код будет выглядеть более сложным, если я изменю его, чтобы загрузить слово, извлечь байт, вычислить и установить байт в новом значении и сделать CAS. Это делает код более сложным, и мне также нужно иметь дело с выравниванием адресов.

EDIT: если эти вопросы зависят от процессора/архитектуры, то какой вывод для процессоров x86/x64?

4b9b3361

Ответ 1

  • reinterpret_cast приведет к поведению undefined. Ваша переменная является либо std::atomic<uint8_t>, либо простой uint8_t; вы не можете бросить между ними. Например, требования к размеру и выравниванию могут быть разными. например некоторые платформы предоставляют только атомарные операции над словами, поэтому std::atomic<uint8_t> будет использовать полное машинное слово, где plain uint8_t может просто использовать байт. Неатомные операции также могут быть оптимизированы различными способами, в том числе значительно переупорядочены с помощью окружающих операций и объединены с другими операциями в смежных местах памяти, где это может повысить производительность.

    Это означает, что если вам нужны атомные операции над некоторыми данными, вы должны заранее знать это и создавать подходящие объекты std::atomic<>, а не просто выделять общий буфер. Конечно, вы могли бы выделить буфер, а затем использовать place new для инициализации вашей атомной переменной в этом буфере, но вам нужно было бы убедиться, что размер и выравнивание верны, и вы не сможете использовать неатомные операции над этим объектом.

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

  • Если CAS медленнее для байта, чем слова, вы можете лучше использовать std::atomic<unsigned>, но это будет иметь штраф за пробел, и вы, конечно, не можете просто использовать std::atomic<unsigned> для доступа к последовательности необработанных байтов --- все операции над этими данными должны проходить через один и тот же объект std::atomic<unsigned>. Обычно вам лучше писать код, который делает то, что вам нужно, и дать компилятору понять, как это сделать.

Для x86/x64 с переменной std::atomic<unsigned> a, a.load(std::memory_order_acquire) и a.store(new_value,std::memory_order_release) не дороже, чем нагрузки и хранилища, к неатомным переменным до тех пор, пока не будут выполнены фактические инструкции, но они ограничивают оптимизация компилятора. Если вы используете по умолчанию std::memory_order_seq_cst, то одна или обе из этих операций будут нести стоимость синхронизации инструкции LOCK ed или забора (моя реализация ставит цену в магазине, но другие реализации могут выбирать по-другому). Тем не менее, операции memory_order_seq_cst легче рассуждать из-за ограничения "единого полного упорядочения", которое они налагают.

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

Ответ 2

Ваш код, безусловно, ошибочен и обязан делать что-то смешное. Если все пойдет очень плохо, это может сделать то, что вы считаете нужным сделать. Я не пойду так далеко, как понимаю, как правильно использовать, например. CAS, но вы бы использовали std::atomic<T> что-то вроде этого:

std::atomic<uint8_t> value(0); 
uint8_t oldvalue, newvalue;
do
{
    oldvalue = value.load();
    newvalue = f(oldvalue);
}
while (!value.compare_exchange_strong(oldvalue, newvalue));

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

Ответ 3

Ваш reinterpret_cast<std::atomic<uint8_t>*>(...) наиболее определенно не является правильным способом получения атома и даже не гарантированно работать. Это связано с тем, что std::atomic<T> не гарантированно имеет тот же размер, что и T.

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

Из того, что я вижу, не существует способа получить std::atomic для существующего значения, особенно потому, что они не гарантированно имеют одинаковый размер. Поэтому вам действительно нужно сделать buf a std::atomic<uint8_t>*. Кроме того, я уверен, что, даже если такой бросок будет работать, доступ через не атомику к тому же адресу не будет гарантированно работать так, как ожидалось (поскольку этот доступ не гарантированно является атомарным даже для байтов). Поэтому иметь неатомические средства доступа к ячейке памяти, в которой вы хотите выполнять атомарные операции, на самом деле не имеет смысла.

Обратите внимание, что для обычных архитектур хранилища и нагрузки байтов являются атомарными в любом случае, поэтому у вас мало накладных расходов на производительность при использовании атомарности там, пока вы используете упорядоченный порядок памяти для этих операций. Так что, если вам не очень-то нравится порядок выполнения в одну точку (например, поскольку программа не многопоточена) просто используйте a.store(0, std::memory_order_relaxed) вместо a.store(0).

Конечно, если вы говорите только о x86, ваш reinterpret_cast, скорее всего, будет работать, но ваш вопрос о производительности, вероятно, по-прежнему зависит от процессора (я думаю, я не искал фактические тайминги команд для cmpxchg).