C : оптимизированная атомизация для быстрой среды с одним производителем и несколькими потребителями

#c #multithreading #atomic

#c #многопоточность #атомарный

Вопрос:

В настоящее время я работаю над проектом, который имеет довольно жесткие циклы и требования к реальному времени. Преждевременная оптимизация здесь определенно не порок. Я хочу максимально использовать возможные relaxed варианты C atomics для достижения своих целей. Вот моя (небезопасная для потоков) настройка:

 // Shared data structure
uint8 data[100];
uint64 timeStamp = 0;

// Consumer threads 1...N run this
void loopConsume() {
    uint64 lastTimeStamp = 0;
    uint8 localData[100];
    for (;;) {
        while (timeStamp == lastTimeStamp);
        memcpy(localData, latestData.data, 100);
        lastTimeStamp = timeStamp;
        doSomething(localData);
    }
}

// Producer callback (singleton)
void produce(uint8 *newData) {
    memcpy(latestData.data, newData, 100);
    latestData.timeStamp  ;
}
  

Вот неуклюжая (но, по-видимому, правильная) попытка сделать ее потокобезопасной (и потокорректной) с использованием 2 атомарных последовательной согласованности по умолчанию:

 #include <atomic>

// Shared variables
uint8 data[100];
std::atomic<uint64> timeStamp = 0;
std::atomic<int32> numReaders = 0; // -1 means producer is writing

// Consumer threads 1...N run this
void loopConsume() {
    uint64 lastTimeStamp = 0;
    uint8 localData[100];
    for (;;) {
        uint64 t = timeStamp.load();
        while (t == lastTimeStamp) t = timeStamp.load();
        lastTimeStamp = t;
        for (;;) {
            int32 r = numReaders.load();
            if (r >= 0 amp;amp; numReaders.compare_exchange_weak(r, r   1))
                 break; 
        }
        memcpy(localData, data, 100);
        doSomething(localData);
        numReaders.fetch_sub(1);
    }
}

// Producer callback (singleton)
void produce(uint8 *newData) {
    while (!numReaders.compare_exchange_weak(0, -1));
    memcpy(data, newData, 100);
    timeStamp.fetch_add(1);
    numReaders.store(0);
}
  

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

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

Большое спасибо!

Комментарии:

1. Я думаю, вас может заинтересовать этот доклад с cppcon 2016: m.youtube.com/watch?v=IB57wIf9W1k — Дж.Ф. Бастьен: Ни один здравомыслящий компилятор не стал бы оптимизировать атомику.

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

3. Такой код будет ужасно работать на многих процессорах x86. Вы ошибаетесь в том, что преждевременная оптимизация здесь не является пороком, именно здесь вы можете нанести наибольший ущерб, оптимизируя, не понимая, что вы заставляете делать процессор. Вы серьезно пытаетесь использовать функцию сравнения и обмена для ОЖИДАНИЯ ? Вы с ума сошли? (Знаете ли вы, что они выполняют безусловную запись на большинстве процессоров x86? Вы знаете, что они делают с межядерными шинами? Знаете ли вы, как они взаимодействуют с hyper-threading? Если нет, ТО ПОЧЕМУ ВЫ ИХ ИСПОЛЬЗУЕТЕ? Если да, ТО ПОЧЕМУ ВЫ ИХ ИСПОЛЬЗУЕТЕ!)