LINUX.ORG.RU

Атомики vs «обычные» переменные

 , ,


1

7

Привет, лор.

Недавно тут у нас состоялась небольшая дискуссия о том, как лучше организовывать доступ к данным из разных тредов Гонка данных - тонкие тонкости?

Дискуссия, в целом, в классическом лоровском стиле: с одной стороны хорошие, правильные программисты, поясняют за UB и атомики, с другой - поехавшие наркоманы, которые пытаются пользоваться особенностями CPU. Я был вынужден выступить в дискуссии (частично, только из соображений производительности) на стороне наркоманов. Просто потому что иногда нужно чтобы софт работал быстрее, даже несмотря на то что яйцами в пилораму.

От товарища @Marvel поступило такое возражение:

Так запись в atomic_uint32_t с relaxed ordering в релизе на Intel сгенерирует код, идентичный обычному присвоению uint32_t (проверено)

В ассемблерном выводе видно, что это немного не так. Но насколько действительно пострадает (если пострадает) производительность от использования atomic с relaxed ordering вместо обычных переменных?

Вот наколеночный тест https://gist.github.com/vmxdev/31c377f55d6717fce86c3b1160c89017

Устроен он так: работают три потока - продюсер на атомиках, продюсер на обычных переменных и потребитель.

Продюсеры просто в цикле увеличивают на единицу элементы массива, потребитель спит, потом просыпается, печатает что там насчитали производители, и опять засыпает. Две строки вывода это значения элементов в разных массивах. Первая строка - «обычные» переменные, вторая - на атомиках. Чем больше число, тем больше успел наувеличивать поток.

На моих железках этот тест дает (с довольно сильным разбросом) устойчиво более быстрый код с «обычными» переменными на x64 (до 2-х раз, в зависимости от количества элементов в массиве, с одним элементом барьер на ассемблере не нужен). На ARM (у меня только 32-битный) - практически та же картина, атомики даже еще немного медленнее (до 3-х раз).

Интересно, можно ли как-то разогнать код с атомиками? Если кто-то знает и может подсказать, было бы отлично.

Возможно, у меня старые компиляторы, и в новых атомики быстрее. Если кто-то даст результаты со свежими gcc и clang (тест лучше запускать несколько раз, хвоста достаточно) - было бы тоже неплохо.

Deleted

поехавшие наркоманы, которые пытаются пользоваться особенностями CPU

А ты код не на CPU собрался исполнять?

Интересно, можно ли как-то разогнать код с атомиками? Если кто-то знает и может подсказать, было бы отлично.

Можно заменить v.store(v.load() + 1) на v.fetch_add(1).

Кстати, компилятор может переупорядочить обращение к «обычным» переменным, а то и вовсе выкинуть. Если это нежелательно, то надо использовать volatile.

NeXTSTEP ★★
()
Последнее исправление: NeXTSTEP (всего исправлений: 1)
Ответ на: комментарий от peregrine

какая разница как быстро работает UB?

Если речь про какую-нибудь специальную числодробилку, то на UB глубоко начхать, в отличие от скорости. Именно потому что запускаться будет на конкретном железе.

no-such-file ★★★★★
()
Ответ на: комментарий от no-such-file

И у него есть какие-то гарантии по поводу конкретного UB? Или «тупой, значит скомпилит в то, что я думаю»?

shdown
()
Ответ на: комментарий от no-such-file

А потом железо поменяется и начнётся магия поиска всех UB в коде или странные результаты пойдут, а все им будут верить.

peregrine ★★★★★
()
Ответ на: комментарий от peregrine

странные результаты пойдут, а все им будут верить

Так странные, или верить? UB не является никакой проблемой для внутреннего использования софта, т.к. гарантии обеспечиваются более общими (и эффективными) организационными мерами. Например, выборочным контролем результатов по «хорошей» (но медленной) версии без UB.

потом железо поменяется

Или в компиляторе появится баг. При чём тут UB?

no-such-file ★★★★★
()
Ответ на: комментарий от no-such-file

Например, выборочным контролем результатов по «хорошей» (но медленной) версии без UB.

Которая может не помогать в случае вероятностных алгоритмов или тестирование будет ужасно долгим и иметь вероятностную природу. А ещё можно тип распределения попутать и банально в тестах не заметить потому как не все параметры тестируются.

Или в компиляторе появится баг. При чём тут UB?

Потому что баг, который пофиксят или он будет висеть в багтрекере, а UB, даже когда работает как надо, не баг, а фича. И нет гарантий что UB 100 раз отработает как надо, а на 101 раз выдаст тыкву.

peregrine ★★★★★
()
Ответ на: комментарий от no-such-file

то приходи когда всю память поменяешь на ECC

Уже 3 года как поменял

peregrine ★★★★★
()

если платформа в случае с обычными переменными гарантирует атомарность, а код с атомиками работает медленнее, то вероятно, что код с атомиками не оптимизирован конкретно под данную платформу, либо компилятор не поддерживает данную оптимизацию

SR_team ★★★★★
()
Ответ на: комментарий от peregrine

ужасно долгим и иметь вероятностную природу

Ну извини, другой вселенной у меня для тебя нет.

который пофиксят или он будет висеть в багтрекере

100500 раз всё может поменяться туда и обратно. Очень вероятно, что прямо сейчас в gcc, например, есть баги которые не висят в трекере и никто про них не знает. Вероятно даже, что кто-то в них уже наступил, но не понял.

Для гарантий есть гарантийный QA отдел. Вот пусть там репу и чешут, как валидировать результаты вне зависимости от влияния реликтового излучения и фаз Луны.

no-such-file ★★★★★
()
Ответ на: комментарий от ZERG

А как ты скорость замеряешь?

Количеством операций (инкрементов), которые сделает производитель, за время пока наблюдающий поток спит

Deleted
()
Ответ на: комментарий от no-such-file

QA отдел. Вот пусть там репу и чешут

QA отдел говорит уволить программистов, которые используют UB в своём коде.

peregrine ★★★★★
()
Ответ на: комментарий от peregrine

говорит уволить программистов, которые используют UB в своём коде

Других программистов у меня для тебя тоже нет, ЛОЛ.

А если серьёзно, то это то, о чём я и говорю — организационные меры. Решает не какой-то там стандарт, а конкретные потребности на месте.

no-such-file ★★★★★
()
Ответ на: комментарий от NeXTSTEP

Можно заменить v.store(v.load() + 1) на v.fetch_add(1).

Это генерирует инструкцию с lock-префиксом, они бывают очень небыстрые. Обсуждали в прошлом тредике. Вот на этом моем тесте и на моем железе атомики с fetch_add(1) на x64 получаются медленнее чем «обычные» переменные где-то в 18 раз

Deleted
()

Просто потому что иногда нужно чтобы софт работал быстрее, даже несмотря на то что яйцами в пилораму.

Если ваш софт тормозит, потому-что два треда постоянно меняют и читают одну и ту же переменную, то надо думать о нужности такой реализации, а не ее оптимизации.

anonymous
()
Ответ на: комментарий от peregrine

какая разница как быстро работает UB? UB это UB.

Сейчас, как ни странно прозвучит, есть пласт задач, для которых важна производительность обмена данными между процессорами. Например, обработка сетевого трафика.

10Gbe - это в пике 14.88 млн пакетов в секунду, для одного 3Ghz CPU получается около 200 тактов на пакет. Обработку, конечно, стараются размазать на максимальное количество процессоров, но растут и скорости (сейчас в ДЦ 100Gbe карты уже практически обыденность), и количество процессоров не бесконечно.

То есть это все не абстрактные тесты из любопытства, а для вполне конкретных приложений

Deleted
()
Ответ на: комментарий от anonymous

два треда постоянно меняют и читают одну и ту же переменную

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

Deleted
()
Ответ на: комментарий от Deleted

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

Значит для второго треда выигрыша не будет. Вопрос в первом. Что значит обновляет? Это вычисления, данные из сети, датчика?

anonymous
()

Вносите гражданина царя!

anonymous
()
- std::uint32_t *raw_arr;
+ volatile std::uint32_t *raw_arr;

так будет более похожее поведение

на gcc atomic всё равно медленнее, а clang генерирует практически идентичный код.

anonymous
()

Проблема в том что переменная stop - волатильная. Это заставляет гцц генерить код совместимый со всякими пережитками прошлых цивилизаций и ложить болт на производительность в принципе. Кроме того я бы перепроверил барьеры.

cvv ★★★★★
()
Ответ на: комментарий от peregrine

Серьёзные дяди заплатят за это.

anonymous
()

тупое говно тупого говна говно чмо чмо пыль запартная ил забортный птушник дошколёнок

anonymous
()
Ответ на: ты не прав от anonymous

ты о volatile? Нет смысла смотреть что-либо с volatile переменными. atomic начинается с полного избавления от volatile.

cvv ★★★★★
()

Зачем ты добавил тег «c», наркоман?

anonymous
()
Ответ на: комментарий от cvv

вот как выглядит тело цикла:

1. просто uint32_t

        addl    $1, (%rax)
2. volatile uint32_t
        movl    (%rax), %edx
        addq    $4, %rax
        addl    $1, %edx
        movl    %edx, -4(%rax)
3. atomic<uint32_t>
        movl    (%rcx), %edx
        addl    $1, %edx
        movl    %edx, (%rcx)
это gcc, clang ведёт себя умнее: просто uint32_t разворачивает в sse, для остальных тоже разворачивает цикл, но без sse.

а всё почему?

[atomic.order]

[Note: Atomic operations specifying memory_order_relaxed are relaxed with respect to memory ordering. Implementationsmuststillguaranteethatanygivenatomicaccesstoaparticularatomicobjectbeindivisible with respect to all other atomic accesses to that object. —end note]

atomic<T> с memory_order_relaxed аналогичен volatile. естественно, только в том случае когда операции над T можно проводить атомарно на данной платформе.

anonymous
()
Ответ на: комментарий от anonymous

цитата побилась

Note: Atomic operations specifying memory_order_relaxed are relaxed with respect to memory ordering. Implementations must still guarantee that any given atomic access to a particular atomic object be indivisible with respect to all other atomic accesses to that object. —end note

anonymous
()

Во-первых, если один тред пишет массив данных, а другой/другие читают, то тут нужны естественно не атомики, а read-write lock(с преференциями чтения или записи в зависимости от того что чаще происходит). Массив атомиков это бред полнейший.

Во-вторых, как уже сказали, нужно смотреть на конкретное CPU, на котором запускается программа. Если CPU тебе не известно - пиши сразу на Java, там memory model расписана в одной толстой книжке.

Если тебе все же необходим низкоуровневый язык под конкретное железо, и ты зачем-то решил написать свои примитивы синхронизации или свои lock-free коллекции(вопрос, правда - зачем? NIH?):

Атомики и memory ordering - разные вещи вообще от слова совсем. Они друг друга скорее дополняют.

Атомики про то чтобы в одну переменную из двух потоков писать. На x86(64) это инструкции с lock префиксом. Очень жирные и медленные т.к. лочат шину памяти и кеш соответственно. Но тут есть лайфхак - x86(64) гарантирует, что все чтения и записи выровненного машинного слова - атомарны. А компиляторы обычно выравнивают структуры данных. Соответственно кроме крайних случаев можно не париться.

Memory ordering это про то, чтобы как минимум две переменные синхронизировать из разных потоков. Чтобы порядок чтения и записи двух переменных в коде был такой же как в реальности, несмотря на всякие суперскалярные оптимизации. Но на том же x86(64) - довольно сильная(strong) memory model, там никакие fetch/write правила и прочая муть не имеют смысла, там единственная значимая инструкция в этом плане это mfence. Она тупо гарантирует чтобы операции чтения и записи памяти, и до, и после барьера, были прямо как в коде. И она тоже дичайше жирная.

lovesan ★★
()
Ответ на: комментарий от lovesan

Атомики и memory ordering - разные вещи

На x86(64) это инструкции с lock префиксом

проходите дальше

anonymous
()
Ответ на: комментарий от lovesan

Оракловая Java-машина (JRE) на уровне дизайна прибита гвоздями к Спаркам. Именно поэтому Спарки и Солярка так долго держались на фоне других классических архитектур.

cvv ★★★★★
()
Ответ на: комментарий от peregrine

какая разница как быстро работает UB? UB это UB.

На конкретном проце UB - это не UB, если конкретный проц себя так ведет по своей спецификации. В жестком реалтайме писать платформозависимый код - это норма.

Dudraug ★★★★★
()
Ответ на: комментарий от Dudraug

На конкретном проце UB - это не UB

UB, если не конкретный компилятор, который гарантированно не проводит оптимизаций кода с конкретным UB. Ассемблерная вставка не UB, внезапно на конкретном проце.

peregrine ★★★★★
()
Последнее исправление: peregrine (всего исправлений: 1)
Ответ на: комментарий от peregrine

какая разница что UB? Ведь работает.

anonymous
()
Ответ на: комментарий от SR_team

вот это, да. остальное - попытки стрелять себе в ногу. и любители стрелять в ногу не переводятся, как я погляжу.

Iron_Bug ★★★★★
()
Ответ на: комментарий от Iron_Bug

Ну это естественно. Просто без вставки даже с ifdef'ами нет гарантий.

peregrine ★★★★★
()

Мой ответ: в этом примере, скорее всего, никак, и вот почему:

  1. В первой функции компилятор видит числодробилку над массивом в цикле. Для этого есть SIMD.

  2. Во второй функции компилятор видит общение между потоками путем установки тех или иных состояний. Смысла в SIMD тут мало.

Другими словами, оптимизации компилятора контекстно-зависимые. Атомики нужны для обмена состояниями между потоками. Компилятор это знает и оптимизирует код исходя из этих соображений.

Хочется добавить насчет UB. Аксиома компилятора: в вашем коде нет UB. Это важно понимать, потому что компилятор может сделать такую оптимизацию, от которой вам будет плохо.

Третья важная вещь: код пишется и для других программистов в том числе. Если автора собьет автомобиль, то кому-то другому придется поддерживать его код. С авторскими «оптимизациями» все чаще всего будут только рады его преждевременной кончине. Лучше раньше, чем позже.

Последнее, что хочется добавить:

  • Преждевременная оптимизация - корень всех зол.
  • Не надо писать велосипеды. Да, хочется быть самым уникальным, умным и талантливым, но нет, не надо.
  • В сложном коде больше ошибок, включая ошибок безопасности.

Если уже прямо все, других вариантов нет, пишите с UB, ассемблером, как хотите.

Marvel
()

Проблема в том что producer_fffuuu в общем случае работает не корректно, например если эта функция будет работать одновременно в двух тредах то будут пропуски в инкрементах.

zaz ★★★★
()

ИМХО сама постановка задачи некорректная. Если несколько потоков дерутся за дни и те же горячие данные, то явно что то не так с архитектурой приложения. Потоки должны вычислением заниматься, а тратить заметную часть времени синхронизацию.

dvetutnev
()
Ответ на: комментарий от zaz

200%, но от той ф-и никто не ждет инкремента. ТС пытается понять другой аспект синхронизации.

cvv ★★★★★
()
Последнее исправление: cvv (всего исправлений: 1)
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.