Поднятие неатомных нагрузок путем приобретения атомных нагрузок

У меня создалось впечатление, что загруженность памяти не может быть поднята выше загружаемой нагрузки в модели памяти С++ 11. Однако, глядя на код, который производит gcc 4.8, кажется, что это правда только для других атомных нагрузок, а не для всей памяти. Если эти истинные и приобретающие нагрузки не синхронизируют всю память (просто std::atomics), то я не уверен, как можно было бы реализовать мьютексы общего назначения в терминах std:: atomic.

Следующий код:

extern std::atomic<unsigned> seq;
extern std::atomic<int> data;

int reader() {
    int data_copy;
    unsigned seq0;
    unsigned seq1;
    do {
        seq0 = seq.load(std::memory_order_acquire);
        data_copy = data.load(std::memory_order_relaxed);
        std::atomic_thread_fence(std::memory_order_acquire);
        seq1 = seq.load(std::memory_order_relaxed);
    } while (seq0 != seq1);
    return data_copy;
}

Выдает:

_Z6readerv:
.L3:
    mov ecx, DWORD PTR seq[rip]
    mov eax, DWORD PTR data[rip]
    mov edx, DWORD PTR seq[rip]
    cmp ecx, edx
    jne .L3
    rep ret

Что выглядит правильно для меня.

Однако изменение данных должно быть int, а не std::atomic:

extern std::atomic<unsigned> seq;
extern int data;

int reader() {
    int data_copy;
    unsigned seq0;
    unsigned seq1;
    do {
        seq0 = seq.load(std::memory_order_acquire);
        data_copy = data;
        std::atomic_thread_fence(std::memory_order_acquire);
        seq1 = seq.load(std::memory_order_relaxed);
    } while (seq0 != seq1);
    return data_copy;
}

Производит следующее:

_Z6readerv:
    mov eax, DWORD PTR data[rip]
.L3:
    mov ecx, DWORD PTR seq[rip]
    mov edx, DWORD PTR seq[rip]
    cmp ecx, edx
    jne .L3
    rep ret

Итак, что происходит?

Ответ 1

Почему загрузка была поднята над приобретением

Я разместил это на gcc bugzilla, и они подтвердили его как ошибку.

  

Предполагается, что предел псевдонима MEM -1 (ALIAS_SET_MEMORY_BARRIER) предотвратит это,     но PRE не знает об этом специальном свойстве (он должен "убивать" все ссылки     пересекая его).

  

Похоже, что gcc wiki имеет приятную страницу об этом.

  

Как правило, релиз является препятствием для тонущего кода, приобретение которого является препятствием для кода подъема.

  

Почему этот код все еще сломан.

По этот документ мой код по-прежнему неверен, потому что он вводит гонку данных. Несмотря на то, что исправленный gcc генерирует правильный код, он все равно не подходит для доступа к data без его упаковки в std::atomic. Причина в том, что расы данных - это поведение undefined, даже если вычисленные из них вычисления отбрасываются.

Пример любезности AdamH.Peterson:

int foo(unsigned x) {
    if (x < 10) {
        /* some calculations that spill all the 
           registers so x has to be reloaded below */
        switch (x) {
        case 0:
            return 5;
        case 1:
            return 10;
        // ...
        case 9:
            return 43;
        }
    }
    return 0;
}

Здесь компилятор может оптимизировать переход в таблицу переходов, и благодаря вышеприведенному оператору if можно будет избежать проверки диапазона. Однако, если скачки данных не были undefined, тогда потребуется вторая проверка диапазона.

Ответ 2

Я не думаю, что ваш atom_thread_fence правильный. Единственная модель памяти С++ 11, которая работает с вашим кодом, будет seq_cst one. Но это очень дорого (вы получите полный забор памяти) за то, что вам нужно.

Исходный код работает, и я думаю, что это лучший компромисс производительности.

ИЗМЕНИТЬ на основе ваших обновлений:

Если вы ищете формальную причину, почему код с обычным int не работает так, как вам хотелось бы, я считаю, что вы сами цитировали (http://www.hpl.hp.com/techreports/2012/HPL-2012-68.pdf) дает ответ. Посмотрите на конец раздела 2. У вашего кода есть те же проблемы, что и код на рисунке 1. У него есть расы данных. Несколько потоков могут выполнять операции с одной и той же памятью в обычном int одновременно. Это запрещено моделью памяти С++ 11, этот код формально недействителен С++-кодом.

gcc ожидает, что код не будет иметь никакой расы данных, т.е. действительный код на С++. Поскольку нет гонки, и код загружает int безоговорочно, нагрузка может быть выбрана в любом месте тела. Таким образом, gcc является умным, и он просто испускает его один раз, поскольку он не является изменчивым. Условное утверждение, которое обычно идет рука об руку с барьером приобретения, играет важную роль в том, что сделает компилятор.

В формальном сленге стандарта атомарные нагрузки и регулярные int-нагрузки не имеют никакого значения. Введение, например, условия создало бы точку последовательности и заставило компилятор оценивать регулярный int после точки последовательности (http://msdn.microsoft.com/en-us/library/d45c7a5d.aspx), Тогда модель памяти С++ сделает все остальное (то есть обеспечит видимость процессором, выполняющим инструкции)

Таким образом, ни одно из ваших утверждений не является истинным. Вы можете определенно создать блокировку с помощью С++ 11, а не с расами данных:-) Обычно блокировка предполагает ожидание перед чтением (что очевидно, что вы пытаетесь избежать здесь), поэтому у вас нет такого типа проблемы.

Обратите внимание, что ваш оригинальный seqlock неисправен, потому что вы не хотите просто проверять seq0!= seq1 (вы можете быть в середине обновления). У бумаги seqlock есть правильное условие.

Ответ 3

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

Программа считывает два следующих значения из атома, один до загрузки и один после загрузки, и повторно выдавая нагрузку, когда они не совпадают. В принципе, нет причин, по которым два атомных чтения когда-либо должны видеть разные значения друг от друга. Даже если атомная запись только что произошла, нет никакого способа, чтобы этот поток мог обнаружить, что он еще не прочитал старое значение. Затем поток возвращался в цикл и, в конце концов, считывал два согласованных значения из атома, а затем возвращал, но поскольку seq0 и seq1 затем отбрасываются, программа не может сказать, что значение в seq0 doesn ' t соответствует значению, считанному из data. Теперь, в принципе, это также говорит мне о том, что весь цикл можно было бы исключить, и на самом деле необходима правильная нагрузка от data, но неспособность выйти из цикла не обязательно является проблемой корректности.

Если reader() должен был вернуть pair<int,unsigned>, который включал seq0 (или seq1), и был создан тот же hoisted loop, я думаю, что это, вероятно, неправильный код (но опять же я новичок в этом не- последовательное согласование операций).