1. C++ / Говнокод #26983

    0

    1. 01
    2. 02
    3. 03
    4. 04
    5. 05
    6. 06
    7. 07
    8. 08
    9. 09
    10. 10
    11. 11
    12. 12
    13. 13
    14. 14
    15. 15
    16. 16
    17. 17
    18. 18
    19. 19
    20. 20
    21. 21
    22. 22
    23. 23
    24. 24
    25. 25
    26. 26
    27. 27
    28. 28
    29. 29
    30. 30
    31. 31
    32. 32
    33. 33
    34. 34
    35. 35
    36. 36
    37. 37
    38. 38
    39. 39
    40. 40
    41. 41
    42. 42
    43. 43
    44. 44
    45. 45
    46. 46
    47. 47
    48. 48
    49. 49
    50. 50
    51. 51
    52. 52
    53. 53
    54. 54
    55. 55
    56. 56
    57. 57
    58. 58
    59. 59
    60. 60
    61. 61
    62. 62
    63. 63
    64. 64
    65. 65
    #include <cstdlib>
    #include <chrono>
    #include <iostream>
    #include <thread>
    
    int p = 0;
    int *q = nullptr;
    
    void g()
    {
        using namespace std::chrono_literals;
    
        std::cout << "g()" << std::endl;
    
        std::cout << "g(): p = 1" << std::endl;
        p = 1;
    
        std::this_thread::sleep_for(1s);
    
        
        if (q != nullptr) {
            std::cout << "g(): *q = 1" << std::endl;
            *q = 1;
        } else {
            std::cout << "g(): q == nullptr" << std::endl;
        }
    }
    
    void f()
    {
        using namespace std::chrono_literals;
    
        std::cout << "f()" << std::endl;
    
        if (p == 0) {
            std::cout << "f(): first loop start" << std::endl;
            while (p == 0) { }  // Потенциально конечный
            std::cout << "f(): first loop end" << std::endl;
        }
    
        int i = 0;
        q = &i;
        std::cout << "f(): second loop start" << std::endl;
        while (i == 0) { }  // Потенциально конечный, хотя в условии только автоматическая пельменная
        std::cout << "f(): second loop end" << std::endl;
    }
    
    int main()
    {
        using namespace std::chrono_literals;
    
        std::cout << "f() thread start" << std::endl;
        auto thr1 = std::thread(f);
        thr1.detach();
        std::this_thread::sleep_for(1s);
    
        std::cout << "g() thread start" << std::endl;
        auto thr2 = std::thread(g);
        thr2.detach();
        std::this_thread::sleep_for(2s);
    
        std::cout << "Done" << std::endl;
        
        std::_Exit(EXIT_SUCCESS);
    }

    Ожидание:

    f() thread start
    f()
    f(): first loop start
    g() thread start
    g()
    g(): p = 1
    f(): first loop end
    f(): second loop start
    g(): *q = 1
    f(): second loop end
    Done

    Запостил: gost, 28 Сентября 2020

    Комментарии (75) RSS

    • Реальность:
      f() thread start
      f()
      f(): first loop start
      g() thread start
      g()
      g(): p = 1
      g(): q == nullptr
      Done

      Реальный пример: https://ideone.com/OCJQg9 («Wandbox» лежит, зараза).
      А чтобы получить ожидаемый вывод — надо конпелировать с «-O0».

      Выхлоп компилятора прекрасен (чуть упрощённый кот): https://gcc.godbolt.org/z/Kx4xeE
      f():
              mov     eax, DWORD PTR p[rip]
              test    eax, eax
              je      .L5
              lea     rax, [rsp-4]
              mov     QWORD PTR q[rip], rax
      .L6:
              jmp     .L6
      .L5:
              jmp     .L5
      Ответить
    • Да, кстати, согласно Борманду этот код — UB (https://govnokod.ru/26981#comment579180).
      1.10 Multi-threaded executions and data races

      The implementation may assume that any thread will eventually do one of the following:
      — terminate,
      — make a call to a library I/O function,
      — access or modify a volatile object, or
      — perform a synchronization operation or an atomic operation.

      В f() нет ни I/O, ни доступа к волатильным объектам, ни атомарных операций.

      Какой карманный лев )))
      Ответить
      • Ну более того, на каких-то платформах это может повиснуть даже без оптимизации.

        Это на x86 мы привыкли к полной когерентности, а есть ведь платформы с более слабой моделью памяти, где проц тупо не будет перезагружать кешлайн, в котором лежит p или i. Ты ведь не дал ему такого повода.
        Ответить
        • >привыкли к полной когерентности,

          Реальна ли такая схема на платформе без когерентности:
          1. В кеше ядра1 переменная равна 1
          2. В кеше ядра2 переменная равна 2
          3. тред1 запускается то на первом ядре, то на втором, и с его точки зрения переменная равно то одному, то другому, пока барьер не поставят?
          Ответить
      • Странно что это вызывает удивление.
        Ответить
    • q и p нужно объявлять как volatile? Или i?
      Ответить
      • Нет. Как атомик. volatile к тредам не имеет вообще никакого отношения, он для прерываний и железа.
        Ответить
        • Как всё сложно...
          Ответить
          • Именно поэтому я просто юзаю мутексы и теку. В них все нужные барьеры и гарантии встроены.
            Ответить
        • volatile ничего же не гарантирует в любом случае?
          Ответить
          • volatile гарантирует, что конпелятор не будет выёбываться, переставляя и выбрасывая записи и чтения. В общем-то и всё.

            Это важно для обращения ко всяким железкам, когда порядок записей и даже чтений имеет значение.
            Ну и в обработчике прерываний, если у тебя с ним какие-то переменные расшарены.
            Ответить
            • иными словами, волатайл отключает софтварные оптимизации, но не хардварные (не ставит заборов и пр)?

              Какой хардкор) у нас в жабе всё проще
              Ответить
              • volatile предотвращает OoO (реордеринг) на одном ядре. И не даёт компилеру полностью выкидывать такие циклы.

                Every access (read or write operation, member function call, etc.) made through a glvalue expression of volatile-qualified type is treated as a visible side-effect for the purposes of optimization (that is, within a single thread of execution, volatile accesses cannot be optimized out or reordered with another visible side effect that is sequenced-before or sequenced-after the volatile access. This makes volatile objects suitable for communication with a signal handler, but not with another thread of execution

                Опять же, я считаю это не совсем правдой. volatile можно использовать для синхронизации если старая-добрая одноядерная машина.

                >у нас в жабе всё проще
                Я бы не сказал. Оно везде сложно.
                Они в 8ой вроде fence из крестов навезли.
                Ответить
                • ну я примерно это и сказал: волатильность заставляет компилятор распологать код последовательно и не оптимизировать. Но процессору ничего не мешает выполнять код как ему угодно, если ему срать на треды на других ядрах.
                  А фенсы заставляют его остановиться, и флашнтуь буфер в кеш, из которого уже по всеми этим MESI/MESA (вечно их путаю) он растечется по кешам остальных ядер.

                  В джаве ЕМНИП есть понятие "happens before", и оно определяет обе ситуации: и хардварную и софтварную грубо говоря.

                  Если я говорю, что Foo happened before Bar, то Bar видит ВСЁ.

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

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

                  >Они в 8ой вроде fence из крестов навезли.
                  Это какой-то ансфейс для царей же, не?
                  Ответить
                  • В крестах тоже есть happens before. Через него там вся семантика и описана.
                    Ответить
                    • >В крестах тоже есть happens before

                      Они из java memory model его и заимствовали :)

                      Но в лучших традициях С++, они поступили согласно принципу: давайте возьмём концепт и сделаем его в 10 раз сложнее.

                      Просто в Йажа volatile был full fence. Крестобляди рассудили что это черезчур медленно и черезчур просто и добавили 4 разных memory_order.
                      Ответить
                      • >Просто в Йажа volatile был full fence.
                        да, именно)

                        Потому в яже мне было срать на сорта заборов. Хепнс бефор куда более внятная и простая конструкция
                        Ответить
                        • >Хепнс бефор куда более внятная и простая конструкция

                          Но она хуёво ложится на разнообразие железа и низкоуровневых инструкций. (MFENCE, LFENCE, SFENCE)

                          И не всегда имеет оптимальный пирформанс, накладывая своими гарантиями лишние ограничения на компилятор.

                          Во многих случаях достаточно memory_order_relaxed.

                          Именно поэтому я за «C++».
                          Ответить
                          • >И не всегда имеет оптимальный пирформанс,
                            угу)

                            ну это же вечный трейдофф: "много думать" versus тормоза.
                            Вот в питоне люди юзают GIL, и им куда проще жить
                            Ответить
                    • ну волатайл-то появилась намного раньше;)

                      когда вообещ в кресты завезли мемори модел для тредов? C++11?
                      Ответить
                • > volatile можно использовать для синхронизации
                  > если старая добрая одноядерная машина

                  Нет. Разве что в примитивных случаях, где relaxed хватает. Насколько помню, volatile не является конпеляторным барьером и не запрещает переупорядочивать обычные записи вокруг себя. Только другие volatile и сайд-эффекты. Т.е. тот же мутекс из волатайла получится весьма хуёвый.
                  Ответить
                  • > Т.е. тот же мутекс из волатайла получится весьма хуёвый.
                    Да.

                    > Разве что в примитивных случаях, где relaxed хватает.
                    Да. Именно тот пример, который я привёл. Когда переменная меняется один раз за всё время работы программы.

                    Даже пытаться использовать его как атомик — путь в ад.
                    Ответить
                    • а вот атомарное чтение и запись в него вроде совсем не гарантируется.
                      Если я в 32х битном режиме пишу в 64х битную переменную, даже в волатильную, то ничего не помешает другому треду увидеть в ней мусор в середине исполнения, не?
                      Ответить
                      • Да. И compare exchange и fetch and add ты никогда из волатайлов не сложишь.
                        Ответить
                        • Именно потому я за "атомарные инструкции в процессоре")

                          Кстати, в джаве точно так же вроде: там запись например в long не гарантирует атомарности, даже если он волатильный, так что есть спец классы для этого (ну или надо юзать примитивы синхронизации_)
                          Ответить
                  • приведи пример, когда на одноядерной машине volatile нельзя юзать как мутекс

                    >не является конпеляторным барьером
                    запись в волатилку всегда имеет сайд эффект, а значит код ПОСЛЕ него должен быть реально исполнен после этой записи, не?
                    Ответить
                    • Нет. Только другие сайд-эффекты и volatile обращения. Остальной код конпелятор волен переставлять как ему заблагорассудится.
                      Ответить
                      • То есть если другой код не волатильный, то он не может увидеть случайно сайд эффект?
                        volatileFoo = 12;
                        doall(volatileBar); //этот код выполнится после
                        doall(volatileFoo); //И этот код выполнится после
                        doall(bar); //А этот может когда угодно

                        так?
                        Ответить
                        • int data;
                          volatile int mutex;
                          
                          // было
                          data = 42;
                          lock = 0; // release mutex
                          
                          // стало
                          lock = 0;
                          data = 42;
                          Второй тред увидит отпущенную лочку, пойдёт читать данные и наебнётся. Если же вместо volatile взять атомик с release семантикой, то такой хуйни не будет.
                          Ответить
                          • ну да, потому что data никак не зависит от лока, понятно, спасибо.

                            так что как мютекс его можно юзать только если ты будешь трогать другие волатильные переменные, потому что про них компилятор не знает, от чего они зависят
                            Ответить
                            • Я написал за volatile, как гипотетическую возможность, более дешевого способа чтения из пельменной в одной специфичной ситуации.

                              Любому вменяемому человеку понятно что использование volatile в качестве примитива синхронизации — полная хуйня.

                              Особенно в свете выхода С++11 и появления широкого набора кроссплатформенных альтернатив.
                              Ответить
                              • я люблю теоретизировать же) ну и за одно лучше вникать в тему

                                Например, у нас есть три булевые переменные, и тред1 делает
                                step1Completed = 1;
                                step2Completed = 1;
                                step3Completed = 1;


                                Если они волатильные, то я могу гарантировать, что другой тред увидит step3Completed ПОСЛЕ того, как он увидит step1Completed если машина одноядерная.

                                А если двухядерная, то ничего не мешает процессору сбросить в кеш из буфера step2Completed, а остальные не сбросить.

                                Потому что volatile не ставит fence.
                                И другой тред увидит их в любом порядке

                                верно?
                                Ответить
                                • Да. Вроде даже на арме можно на этот эффект посмотреть.

                                  Спасибо что напомнил, я когда-то хотел попробовать, но у меня не было подходящего железа.
                                  Ответить
                                  • а в x86 писание в память упорядоченное, так что он гарантирует тебе выпёздывание буфера в кеш в том порядке, в каком его заполнили? или почему обязательно нужен арм?
                                    Ответить
                                    • Да, в арме не было единого глобального порядка записей, который наблюдают все процы. Теперь походу есть. И для эксперимента подойдёт только какой-нибудь power pc.
                                      Ответить
                                      • А в каком поколении армов ситуация изменилась?
                                        Ответить
                                        • Стеоверфлоу пишет что гарантию добавили в ARMv8
                                          Ответить
                                          • Т. е. в ARMv7, которых пока ещё дофига в обращении, гарантии нет?
                                            Ответить
                                      • Самый мощный проц это Alpha, царство ему небесное
                                        Вика пишет, что Dependent loads can be reordered, хотя мне это не очень понятно. Видимо бывают сорта депенденсов.

                                        А что у ваших армов с кококококогерентностью кеша? попадание в кеш-то гарантировано бродкастится во все кеши, или тоже нужно явно?
                                        Ответить
                                • Зависит от языка на котором это написано, платформы и фазы Луны.
                                  Ответить
                                  • мы про сишечку

                                    В джаве вроде бы нет, потому volatile сделает fence.
                                    Ответить
                                • Ладно, надо идти работать, а не пиздеть про интересную хуйню.

                                  Жаль конечно, что нет такой профессии: "пиздун про интересную хуйню", я бы устроился
                                  Ответить
        • Подтверждаю. Если сделать вот так:
          #include <atomic>
          
          std::atomic<int> p = 0;
          
          void f() {
               if (p == 0) {
                  while (p == 0) { /* ... */ }  // Потенциально конечный
              }
          }

          То получится вот это:
          f():
          .L6:
                  mov     eax, DWORD PTR p[rip]
                  test    eax, eax
                  je      .L6
                  ret
          p:
                  .zero   4
          Ответить
          • Забавно, что с volatile для x86 ты получишь точно такой же код. А потом на какой-нибудь альфе он у тебя повиснет, в отличие от кода с std::atomic.
            Ответить
            • >>> А, ну да, это ж кресты, тут думать надо.[/color]
              Ответить
              • Над этим нужно думать не только в крестах. Разве что в крестах нужно думать в разы сильнее.

                Рекомендую почитать JCP, там все эти вопросы очень подробно разобраны.
                Ответить
            • хех, хотел спросить, есть ли в крестах compare and exchange

              оказалось, что это тот же неймспейс

              https://en.cppreference.com/w/cpp/atomic/atomic/compare_exchange
              Ответить
        • > volatile к тредам не имеет вообще никакого отношения
          Я уже вроде приводил контр-пример.

          volatile boolean done=0;
          ...
          while (!done)
          Ответить
          • И что этот контр-пример опровергает?

            То, что на интеле всё когерентно и в атомик риде нет барьеров и специальных инструкций, поэтому и обычное чтение сойдёт?
            Ответить
            • Что volatile можно использовать в многопоточных программах на флаге завершения.

              >То, что на интеле всё когерентно
              А на других платформах разве будут проблемы?
              Ответить
              • На x86 - вай нот. В общем случае - нет. Атомарное чтение может содержать какие-то специальные инструкции, которых нету в volatile чтении.
                Ответить
                • Так а какие спец. инструкции?

                  У нас есть признак шатдауна. Он может поменяться только одним способом (из 0 в 1).

                  Как только он стал ненулевым рано или поздно все это заметят и остановятся.
                  Ответить
                  • Ну смотри, даже на интеле у тебя запись может застрять во write-back кеше и годами висеть в нём, не попадая в оперативку. Но за счёт протокола когерентности ядро с циклом об этой записи узнает.

                    Если у тебя там какая-то хуйня с тыщей ядер, то им будет очень дорого следить за кешами друг-друга. В лучшем случае они будут мониторить только реальные записи в память (т.е. тебе понадобится write-through семантика на done = 1). В худшем случае они вообще ничего не будут мониторить (и тогда нужно чтение с инвалидацией кеша на !done). volatile ничего из этого не даёт.
                    Ответить
                    • Ну когда-то оно ведь остановится.

                      Я к тому что атомик-чтения и особенно мьютексы были бы слишком дорогими в этой ситуации с флагом.

                      volatile раньше был вполне адекватным методом.

                      Но в целом после завоза в кресты std::memory_order и happens-before семантики оно бесполезно.
                      Ответить
                      • > когда-то оно ведь остановится

                        Когда юзер психанёт и ткнёт в резет, ага. Представь, что это были ядерные треды, которые никто никогда не вытеснит.

                        > слишком дорогими

                        Да вот нихуя. Либо атомарное чтение бесплатное уже включено в цену (интел, арм) либо без него твой код тупо не работает (альфа?)
                        Ответить
                        • >что это были ядерные треды, которые никто никогда не вытеснит.

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

                          >атомарное чтение уже включено в цену (интел, арм)
                          Разве? Там же лишний LFENCE будет на каждом чтении, не?
                          Ответить
                          • > lfence

                            Зачем? Зачем? Посмотри на годболте во что атомарное чтение раскрывается.

                            На арме вроде тоже чтение бесплатно, а вот в записи барьер. Но я не изучал их модель.
                            Ответить
                            • > Посмотри на годболте во что атомарное чтение раскрывается.
                              Вон, я выше по ветке привёл реальный пример. В «mov eax, DWORD PTR p[rip]» раскрывается.
                              Ответить
                              • А в чём разница между .load() и перегруженным чтением?
                                Ответить
                                • Без параметров эквивалентны. Но в лоад можно более тонкую семантику указать. Да и лучше видно, что это именно атомарная хуйня.
                                  Ответить
                                  • > Да и лучше видно, что это именно атомарная хуйня.
                                    Подтверждаю.
                                    Так читаешь код, и даже не уверен в его атомарности.
                                    Ответить
                                • тем, что ему можно заказать memory_order?
                                  typedef enum memory_order {
                                      memory_order_relaxed,
                                      memory_order_consume,
                                      memory_order_acquire,
                                      memory_order_release,
                                      memory_order_acq_rel,
                                      memory_order_seq_cst
                                  } memory_order;


                                  какой хардкор;)
                                  Ответить
                                  • > какой хардкор;)
                                    В Йажа точно то же.

                                    https://openjdk.java.net/jeps/171
                                    Ответить
                                    • >Add three memory-ordering intrinsics to the sun.misc.Unsafe class же
                                      кажется, что в реальной жизни это редко когда нужно

                                      ну хотя в крестах же тоже вон борманд сказал, что можно мютексы юзать, и течь
                                      Ответить
                                      • Так они потом используются в кишках йажа.утил.конкарент и экспортятся юзеру в виде благородных высокоуровевых api.
                                        Ответить
                            • >while (p == 0)

                              В ваших крестах с блядскими перегрузочками нихера не понятно. Какие именно гарантии у этого чтения?
                              Ответить
                              • sequentially consistent судя по выхлопу для ARM.

                                Пиши явно: while (!p.load(std::memory_order_relaxed)), тогда бесплатно. Для флажка об остановке релакса должно хватить.
                                Ответить
                    • >Если у тебя там какая-то хуйня с тыщей ядер, то им будет очень дорого следить за кешами друг-друга.

                      Так а они ведь как-то видят изменения volatile-переменных после прерываний.
                      Ответить
                      • > после прерываний

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

                        А для MMIO с железками как правило write back отключен, поэтому проц всё честно пишет.
                        Ответить
    • Ничего не понимаю... И это программисты? Говно какое-то... Пидоры, блядь. Родина им дала практические задачи! Решай, решай задачи из предметной области — блядь, не хочу, хочу жрать говно! Что такое? Это программирование?! Это программирование?! Суки... Ядер понакупили, заборов и мьютексов понаставили! Говно жрут! Пидоры, блядь, ёбаные...
      Ответить
    • Прочитал "конченый"
      Ответить

    Добавить комментарий