Russian Qt Forum
Март 29, 2024, 08:18 *
Добро пожаловать, Гость. Пожалуйста, войдите или зарегистрируйтесь.
Вам не пришло письмо с кодом активации?

Войти
 
  Начало   Форум  WIKI (Вики)FAQ Помощь Поиск Войти Регистрация  

Страниц: 1 [2] 3   Вниз
  Печать  
Автор Тема: memory_order  (Прочитано 17822 раз)
Авварон
Джедай : наставник для всех
*******
Offline Offline

Сообщений: 3257


Просмотр профиля
« Ответ #15 : Май 24, 2019, 14:41 »

Компилятор разбирает исходник и в зависимости от типа барьера может генерировать разный код для процессора. Вы не согласны? Улыбающийся
Может. И делает. Вы же утверждаете, что реордер происходит только из-за того, что компилятор там что-то соптимизировал и переставил. Это неправда.
Еще раз:
Цитировать
Компилятору, точнее его оптимизатору, который и решает какие операции с какими можно переставить, а какие нельзя
Если бы была проблема в компиляторе, то проблемы бы не было - пишем везде volatile, компилируем без оптимизаций и вуаля. Ток чот это не помогает.
Записан
Old
Джедай : наставник для всех
*******
Offline Offline

Сообщений: 4349



Просмотр профиля
« Ответ #16 : Май 24, 2019, 14:53 »

Может. И делает. Вы же утверждаете, что реордер происходит только из-за того, что компилятор там что-то соптимизировал и переставил. Это неправда.
Да где же я утверждал что "ТОЛЬКО"? Улыбающийся
Перечитайте мое сообщение, на которое вы возразили и последнее на которое отвечаете сейчас. Я хотел сказать лишь это. Улыбающийся
Записан
Авварон
Джедай : наставник для всех
*******
Offline Offline

Сообщений: 3257


Просмотр профиля
« Ответ #17 : Май 24, 2019, 15:10 »

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

Слава богу, разобрались =)

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

Компилятор конечно может посмотреть на ваш атомик и вставить эту инструкцию сам, но скорее всего, там просто внутри ассемблерная вставка, типа такой. Потому что зачем хачить компилятор, если можно не хачить?
Возможно (!) есть какие-то верхнеуровневые оптимизации, которые смотрят на код как на черный ящик и стандарт запрещает их делать в случае атомиков, но это рассуждения из серии "срут ли единороги радугой" и только запутывают, см. выше почему. Ну и да, если бы такие оптимизации были, то QAtomic бы не работал Улыбающийся Ведь компилятор не в курсе, что QAtomic это атомик. Thread Sanitizer, кстати, имел такую проблему - не понимал QMutex и QAtomic как механизмы синхронизации.
Записан
Old
Джедай : наставник для всех
*******
Offline Offline

Сообщений: 4349



Просмотр профиля
« Ответ #18 : Май 24, 2019, 15:23 »

Ведь компилятор не в курсе, что QAtomic это атомик.
В курсе. Например для GCC под капотом QAtomic это https://gcc.gnu.org/onlinedocs/gcc-4.1.2/gcc/Atomic-Builtins.html
Записан
Авварон
Джедай : наставник для всех
*******
Offline Offline

Сообщений: 3257


Просмотр профиля
« Ответ #19 : Май 24, 2019, 15:26 »

В курсе. Например для GCC под капотом QAtomic это https://gcc.gnu.org/onlinedocs/gcc-4.1.2/gcc/Atomic-Builtins.html

Ну gcc в курсе, а не gcc не в курсе=) Если вы посмотрите, то моя ссылочка и atomic-gcc в разных файлах на минуточку Подмигивающий
Сейчас-то в Qt вообще все на с++11 атомиках уже.
Записан
Авварон
Джедай : наставник для всех
*******
Offline Offline

Сообщений: 3257


Просмотр профиля
« Ответ #20 : Май 24, 2019, 15:41 »

Т.к. никаких указаний/блокировок нет, то остается полагать что порядок выполнения AB / CD - любой. Но видимость в др. нитке срабатывает, то есть если Thread 1(B) прорвется первой и установит x - он будет видим в Thread 2. Оказывается если пишем relaxed - то записанное значение будет верно relaxed-прочитано в др нитке (конечно если оно не перекрыто опять). Однако про "всю память до того" ничего не говорится.

Да, всё верно.
Но атомики тут по большому счету ни при чем, есть понятие "когерентности кешей" - если вы что-то успешно записали в кэш по адресу 0xDEADBEEF, то другие ядра, кто будет читать этот адрес, увидят записанное значение. Если точнее, вся кэш-линия магическим образом синхронизируется.
Проблемы начинаются когда вы пишите одну переменную в одной кэш линии, а читаете из логически зависимой, но другой переменной в другой кэш-линии - они выпадают из синка т.к. процессор не в курсе, что они связаны.
Присыпьте это случайными перестановками инструкций и вы получите странные результаты.
« Последнее редактирование: Май 24, 2019, 15:44 от Авварон » Записан
Igors
Джедай : наставник для всех
*******
Offline Offline

Сообщений: 11445


Просмотр профиля
« Ответ #21 : Май 26, 2019, 04:28 »

Страсти поутихли, попробуем продолжить обсуждение. Немного в сторону, но все же - а как все эти дела соотносятся с обычными действиями без std::atomic? Напр
Код
C++ (Qt)
int a = 0;
...
a = 1;
Я понимаю так: присваивание a=1 неделимо и, вероятно, выльется в одну команду. Но никаких синхронизаций кешей не выполняется. Пусть больше никто не трогает "а". Тогда записавшая нитка прочтет свою 1 (свой кеш) а остальные - возможно и 0 (если сидят на др ядре), хотя запись уже свершилась. Рано или поздно ОС синхронизирует кеши (причем достаточно скоро), но шансы получить старое значение имеются. Которое, однако, не случайный "мусор" - в данном случае оно может быть только 0 или 1.

Однако все это - всего лишь мои интуиция/предположения Улыбающийся Никогда не видел "официального" объяснения (а хотелось бы). Ну здесь такое
Цитировать
Memory order

When a thread reads a value from a memory location, it may see the initial value, the value written in the same thread, or the value written in another thread. See std::memory_order for details on the order in which writes made from threads become visible to other threads.
Как-то "не густо" Улыбающийся

Еще о том же. Вот есть простецкий код
Код
C++ (Qt)
int abortFlag = 0;
 
// thread 1
 
while (!abortFlag) {
 ....
}
 
// thread 2 (main)
 
abortFlag = 1;
// thread1.joint();
Будет ли точнее (грамотнее и.т.п) юзать std::atomic<int> ? Если да то чем?

Возвращаясь к memory_order
Цитировать
Typical use for relaxed memory ordering is incrementing counters, such as the reference counters of std::shared_ptr, since this only requires atomicity, but not ordering or synchronization (note that decrementing the shared_ptr counters requires acquire-release synchronization with the destructor)
По-прежнему "не врубаюсь" в эту фразу. Атомарный инкремент должен выполняться через CAS - и никак иначе, причем тут relaxed order? Также в упор не вижу никакой половой разницы между инкрементом и декрементом. Ну может имеется ввиду что инкремент не возвращает (инкрементированное) значение, а декремент должен (но это опять догадки)
Записан
Авварон
Джедай : наставник для всех
*******
Offline Offline

Сообщений: 3257


Просмотр профиля
« Ответ #22 : Май 26, 2019, 15:08 »

Я понимаю так: присваивание a=1 неделимо и, вероятно, выльется в одну команду.
Неверно. На каком-нибудь 16битном процессоре это выльется в две команды. Другое дело, что я не знаю, есть ли живые архитектуры где запись Инта делится. Но я и под микроконтролеры не писал да и с АРМом очень мало общался. Стандарт считает, что может - значит может.
Если вы посмотрите на какой-нибудь код типа
Код:
int m_i = 0; // в классе
m_i++; // в функции
То это может вылиться аж в ТРИ инструкции - прочитать из памяти в регистр, инкрементнуть, записать взад. То есть то, что присвоение - одна инструкция - это скорее исключение из правила.

Но никаких синхронизаций кешей не выполняется. Пусть больше никто не трогает "а". Тогда записавшая нитка прочтет свою 1 (свой кеш) а остальные - возможно и 0 (если сидят на др ядре), хотя запись уже свершилась. Рано или поздно ОС синхронизирует кеши (причем достаточно скоро), но шансы получить старое значение имеются.
Кеши синхронизируются всегда. Объяснение многопоточных проблем что, мол, в Кешах значения разные - это распространенный миф; просто так проще объяснять. Однако, кроме кешей есть регистры (они не синхронизируются) и буфера записи в кеш (там чуть хитрее но тоже никто не гарантировал синк)

Которое, однако, не случайный "мусор" - в данном случае оно может быть только 0 или 1.
В предположении, что запись Инта неделима, что, опять же, лишь предположение об архитектуре на которой будет выполняться код. На x86 всё и без атомиков всё работает Подмигивающий
В целом да, на это можно полагаться (наверное), но непонятно, зачем. Что за желание "обмануть систему"? На чем вы экономите? На спичках? Ну так на десктопе 3 из 4х атомиков раскрываются в обычные операции, то есть ассемблерный код будет одинаковый что с атомиком, что без.
Но см ниже.

Еще о том же. Вот есть простецкий код
Код
C++ (Qt)
int abortFlag = 0;
 
// thread 1
 
while (!abortFlag) {
 ....
}
 
// thread 2 (main)
 
abortFlag = 1;
// thread1.joint();
Будет ли точнее (грамотнее и.т.п) юзать std::atomic<int> ? Если да то чем?

А кто сказал, что мы обязаны читать этот int в цикле из памяти/кеша? abortFlag в данном треде не меняется, давайте положим его в регистр, сделаем if перед циклом и уйдем в бесконечный луп, если flag == 0. Я почти уверен, что такая "оптимизация" возможна начиная с с++11. Другое дело, что она вряд ли включена, потому что очень много кода сломается. Но могут включить в дальнейшем, ваш код не валиден с тз стандарта.

Возвращаясь к memory_order
Цитировать
Typical use for relaxed memory ordering is incrementing counters, such as the reference counters of std::shared_ptr, since this only requires atomicity, but not ordering or synchronization (note that decrementing the shared_ptr counters requires acquire-release synchronization with the destructor)
По-прежнему "не врубаюсь" в эту фразу. Атомарный инкремент должен выполняться через CAS - и никак иначе, причем тут relaxed order? Также в упор не вижу никакой половой разницы между инкрементом и декрементом. Ну может имеется ввиду что инкремент не возвращает (инкрементированное) значение, а декремент должен (но это опять догадки)
Схрена ли inrement - это CAS? инкремент - это инкремент, CAS -  это CAS.
То, что все, что угодно можно реализовать через CAS не значит, что нужно. Если есть атомарная "операция" инкремента - будет использована она.
Relaxed нужен, если от значения атомика не зависит поток выполнения - например, мы тупо его печатаем в консоль. И не важно, что там - 0, 42 или 100500, на результат программы это не влияет. Без relaxed атомика у вас нет гарантии, что вы вообще когда-нибудь увидите что-то, кроме начального значения. Или конечного. Или промежуточного.
Ну например:
Код:
int counter = 0; // общий счетчик
for (int i = 0; i < 10000; ++i) {doWork(); ++counter;}
может вылиться в
Код:
int counter = 0; // общий счетчик
for (int i = 0; i < 10000; ++i) {doWork();}
counter = 9999;
С тз компилятора оба куска кода делают одно и то же.  Зачем "подергивать" медленную память, если можно записать один раз в конце цикла?
Помните другой миф про volatile и многопоточность? Вот volatile как раз такие приколы лечит.
« Последнее редактирование: Май 26, 2019, 23:27 от Авварон » Записан
Igors
Джедай : наставник для всех
*******
Offline Offline

Сообщений: 11445


Просмотр профиля
« Ответ #23 : Май 27, 2019, 08:36 »

Я понимаю так: присваивание a=1 неделимо и, вероятно, выльется в одну команду.
Неверно. На каком-нибудь ...
...
То это может вылиться аж в ТРИ инструкции ...
Если присваивание не "atomistic" (т.е. выполняется 2 и более командами) - то и разговаривать не о чем, никакой atomic не спасает от вклинивания между командами. Это просто не будет работать - и все.

Кеши синхронизируются всегда.
Очень в этом сомневаюсь. Тогда чем объяснить заметное падение скорости при замене обычных int'ов на atomic'и? Это мне хорошо известно по собственному опыту. Скорее всего это удовольствие достаточно дорогое.

А кто сказал, что мы обязаны читать этот int в цикле из памяти/кеша? abortFlag в данном треде не меняется, давайте положим его в регистр, сделаем if перед циклом и уйдем в бесконечный луп, если flag == 0. Я почти уверен, что такая "оптимизация" возможна начиная с с++11. Другое дело, что она вряд ли включена, потому что очень много кода сломается. Но могут включить в дальнейшем, ваш код не валиден с тз стандарта.
Здесь Вы явно перегибаете палку. В данном случае компилятору ничего не известно о неизменности abortFlag (хотя бы потому что это глобальная переменная и может быть изменена в др единице трансляции), и код будет его честно читать без всяких atomic'ов и volatile. То же касается др примера что Вы давеча приводили
Код:
data = std::make_shared<xxx> (...);
ready = true;
Нет никакой гарантии что предшествующий call никоим образом не завязан на readу, поэтому никаких перестановок не случится. Др дело так
Код:
int ready;  // локальная
...
data = std::make_shared<xxx> (...);
ready = true;
А так уже запросто переставит (особенно icc любит так делать), т.к. "гарантия неиспользования" ready есть
 
Схрена ли inrement - это CAS? инкремент - это инкремент, CAS -  это CAS.
То, что все, что угодно можно реализовать через CAS не значит, что нужно. Если есть атомарная "операция" инкремента - будет использована она.
Relaxed нужен, если от значения атомика не зависит поток выполнения - например, мы тупо его печатаем в консоль. И не важно, что там - 0, 42 или 100500, на результат программы это не влияет. Без relaxed атомика у вас нет гарантии, что вы вообще когда-нибудь увидите что-то, кроме начального значения. Или конечного. Или промежуточного.
Последняя фраза как-то не очень согласуется с предыдущим заявлением что, мол, кеши синхронизируются всегда Улыбающийся Если бы так, то все нитки видели бы одно и то же, а это явно не так.
Ну хорошо, вот есть "инкремент одной командой"
Код:
inc dword_ptr[some_offset]
Да, мы никак не сможем вернуть корректное инкрементированное значение (команда-то уже кончилась). Ну ладно, допустим "оно нас не интересует". Но разве это обеспечит что именно последнее (с учетом изменений сделанных всеми нитками) значение будет инкрементировано? По-моему нет
Записан
Авварон
Джедай : наставник для всех
*******
Offline Offline

Сообщений: 3257


Просмотр профиля
« Ответ #24 : Май 27, 2019, 10:10 »

Если присваивание не "atomistic" (т.е. выполняется 2 и более командами) - то и разговаривать не о чем, никакой atomic не спасает от вклинивания между командами. Это просто не будет работать - и все.
Если что, любой POD можно объявить как atomic. Другое дело, что он будет релизован не эффективно, см. is_lock_free(). Другое дело, что на 16битной платформе int будет 16битным и таки будет писаться одной инструкцией=)

Очень в этом сомневаюсь. Тогда чем объяснить заметное падение скорости при замене обычных int'ов на atomic'и? Это мне хорошо известно по собственному опыту. Скорее всего это удовольствие достаточно дорогое.
Тем, что вы форсите процессор/компилятор всегда писать в "память" (кэш), не используя регистры. Кроме того, помимо собственно синхронизации кешей, барьер памяти делает другие операции, а именно ждет, когда все чтения/записи станут видны, а не только самого атомика.

Здесь Вы явно перегибаете палку. В данном случае компилятору ничего не известно о неизменности abortFlag (хотя бы потому что это глобальная переменная и может быть изменена в др единице трансляции), и код будет его честно читать без всяких atomic'ов и volatile.
Компилятору известно, что в данном треде abortFlag не меняется, а также то, что он не помечен как атомик, а значит не может меняться из других тредов. Иначе он был бы помечен как атомик, верно?  Подмигивающий

То же касается др примера что Вы давеча приводили
Код:
data = std::make_shared<xxx> (...);
ready = true;
Нет никакой гарантии что предшествующий call никоим образом не завязан на readу, поэтому никаких перестановок не случится. Др дело так
Код:
int ready;  // локальная
...
data = std::make_shared<xxx> (...);
ready = true;
А так уже запросто переставит (особенно icc любит так делать), т.к. "гарантия неиспользования" ready есть
Мдааааа....
 
Последняя фраза как-то не очень согласуется с предыдущим заявлением что, мол, кеши синхронизируются всегда Улыбающийся Если бы так, то все нитки видели бы одно и то же, а это явно не так.
Вы успешно проигнорировали объяснение, что кроме кешей есть регистры и буфера записи, которые не видны другим ядрам.

Ладно, давайте по-другому. Сперва вы осилите букварь (первые 4 статьи), а потом уже поговорим.
« Последнее редактирование: Май 27, 2019, 11:33 от Авварон » Записан
Igors
Джедай : наставник для всех
*******
Offline Offline

Сообщений: 11445


Просмотр профиля
« Ответ #25 : Май 28, 2019, 14:15 »

Ладно, пожуем перестановки, если к ним такой уж интерес
Компилятору известно, что в данном треде abortFlag не меняется, а также то, что он не помечен как атомик, а значит не может меняться из других тредов. Иначе он был бы помечен как атомик, верно?  Подмигивающий
"Неверно" - не то слово, это просто "бред" (как говорил тут один гуру, кстати где он - что-то как ветром сдуло).
Код:
a = 1;
b = 2;
Да, такие рудиментарные независимые операции уже десятки лет переставляются как компилятором так и процессором. Но это вовсе не распространяется на все и вся. Пример с abortFlag может быть сколь угодно примитивным, убогим и.т.п. НО он не нарушает никаких правил языка/стандарта. Разве там сказано что "нельзя писать глобальную переменную из др нитки?". Что "изменяемые переменные обязательно должны быть atomic?". Нет. А значит любой компилятор обязан создать рабочий/совместимый код, невзирая ни на какие соображения оптимизации. Иначе судьба компилятора незавидна (как и создавшей его компании).

Нетрудно привести пример когда перестановка фатальна, ну хотя бы так
Код:
mutex.lock();
a = 1;
Следуя Вашей логике и здесь "может переставить" - вот мы уже и пришли к полному абсурду Улыбающийся Интересно, как программист мог бы защититься от такой перестановки и добиться лока "до"? Мне ничего не приходит в голову, кроме "никак", такая оптимизация - себе дороже. Компилятор видел передачу упр-я в "чужой" код, при этом нет гарантий что этот код не может изменить "a" или наоборот. Значит ничего переставлять он не должен. Процессор тоже не должен выполнять a=1 параллельно, увидев команду перехода. 

А в контексте atomic разговор о перестановках заходит только потому что у компилятора нет никаких оснований считать операции "зависимыми", вот и приходится создать/регламентировать явные правила для таких ситуевин.

Ладно, давайте по-другому. Сперва вы осилите букварь (первые 4 статьи), а потом уже поговорим.
Спасибо, но таких опусов я не читаю, и Вам не советую, вреда от них больше чем пользы.
Записан
Авварон
Джедай : наставник для всех
*******
Offline Offline

Сообщений: 3257


Просмотр профиля
« Ответ #26 : Май 28, 2019, 16:37 »

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

Код:
a = 1;
b = 2;
Да, такие рудиментарные независимые операции уже десятки лет переставляются как компилятором так и процессором.
Десятки лет стандарт ничего не говорил о том, что такое "thread" и что такое "race condition". Соответственно, разработчикам компиляторов приходилось "крутиться", предполагая худшее (то, что вы говорите что "нет гарантий что не пишется"). Есть хороший пример (из другой области) с флагом -fno-strict-aliasing, который собственно переключает компилятор между режимами "предполагать худшее" и "предполагать лучшее". Если чо, тупо включение этого флага может замедлять код на 30%.

Но это вовсе не распространяется на все и вся. Пример с abortFlag может быть сколь угодно примитивным, убогим и.т.п. НО он не нарушает никаких правил языка/стандарта.
Нарушает.
Разве там сказано что "нельзя писать глобальную переменную из др нитки?". Что "изменяемые переменные обязательно должны быть atomic?".
Сказано, еще раз:
Цитировать
When an evaluation of an expression writes to a memory location and another evaluation reads or modifies the same memory location, the expressions are said to conflict. A program that has two conflicting evaluations has a data race unless

both evaluations execute on the same thread or in the same signal handler, or
both conflicting evaluations are atomic operations (see std::atomic), or
one of the conflicting evaluations happens-before another (see std::memory_order)
If a data race occurs, the behavior of the program is undefined.


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

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

Нетрудно привести пример когда перестановка фатальна, ну хотя бы так
Код:
mutex.lock();
a = 1;
Следуя Вашей логике и здесь "может переставить" - вот мы уже и пришли к полному абсурду Улыбающийся

Не может, lock() включает в себя барьер памяти, на той страничке, что я уже устал цитировать, есть и такие строки:
Цитировать
in particular, release of a std::mutex is synchronized-with, and therefore, happens-before acquisition of the same mutex by another thread, which makes it possible to use mutex locks to guard against data races

Компилятор видел передачу упр-я в "чужой" код, при этом нет гарантий что этот код не может изменить "a" или наоборот. Значит ничего переставлять он не должен. Процессор тоже не должен выполнять a=1 параллельно, увидев команду перехода. 

Угу, только вызов make_shared это не "чужой" код и даже не вызов функции, там всё должно инлайнится, что сводит нас к тривиальному случаю a = 1, b = 2. Вы серьезно пишете свои программы в предположении что компилятор заинлайнит или не заинлайнит что-то?=)

А в контексте atomic разговор о перестановках заходит только потому что у компилятора нет никаких оснований считать операции "зависимыми", вот и приходится создать/регламентировать явные правила для таких ситуевин.

Да, и именно поэтому надо помечать явно атомиками то, что дергается из разных тредов.

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

Не читал, но осуждаю  Подмигивающий
Записан
Igors
Джедай : наставник для всех
*******
Offline Offline

Сообщений: 11445


Просмотр профиля
« Ответ #27 : Май 29, 2019, 11:41 »

Сказано, еще раз:
Цитировать
When an evaluation of an expression writes to a memory location and another evaluation reads or modifies the same memory location, the expressions are said to conflict. A program that has two conflicting evaluations has a data race unless

both evaluations execute on the same thread or in the same signal handler, or
both conflicting evaluations are atomic operations (see std::atomic), or
one of the conflicting evaluations happens-before another (see std::memory_order)
If a data race occurs, the behavior of the program is undefined.

Вы исходите из установки типа "UB = ппц, допускать его никак нельзя!" Это перегиб. Да, без атомиков мы можем увидеть abortFlag==1, но можем и abortFlag==0 (хотя 1 уже записана), в этом собсно и заключается "undefined". И что? Чем это нам грозит? Ну крутанется еще раз тело while, поймаем единичку на след итерации, это нормально, нитку мгновенно не остановить.

Не может, lock() включает в себя барьер памяти,
...
Угу, только вызов make_shared это не "чужой" код
Ну Вы же прекрасно понимаете что "частные случаи" ничего не доказывают. Тот же mutex может быть моим классом, а тот же make_shared - звать мой конструктор.

Да, и именно поэтому надо помечать явно атомиками то, что дергается из разных тредов.
Опять как-то у Вас все "очень просто", типа "натыкать атомиков побольше - и будет счастье" Улыбающийся Скорее эффект будет обратный - скорость подсядет на lock, код засорится, но без всяких достижений. Впрочем это обычный рез-т чрезмерно усердного чтения.

Не читал, но осуждаю  Подмигивающий
Да Улыбающийся Глянув первые пару фраз я быстро понял что речь пойдет обо всем-всем. Очень может быть что там есть вещи что я не постиг при написании своих lock-free великов, но их будет не так уж много. И как-то фильтровать/вылавливать эти "жемчужные зерна" - нет уж, увольте. А "беглый просмотр по диагонали" практически бесполезен.
Записан
Авварон
Джедай : наставник для всех
*******
Offline Offline

Сообщений: 3257


Просмотр профиля
« Ответ #28 : Май 29, 2019, 12:36 »

Вы исходите из установки типа "UB = ппц, допускать его никак нельзя!" Это перегиб. Да, без атомиков мы можем увидеть abortFlag==1, но можем и abortFlag==0 (хотя 1 уже записана), в этом собсно и заключается "undefined". И что? Чем это нам грозит? Ну крутанется еще раз тело while, поймаем единичку на след итерации, это нормально, нитку мгновенно не остановить.
UB допускать нельзя потому что это не "нолик" или "единичка", это ваш внезапно умерший кот или клоун, прыгающий у вас на носу. UB значит, что может случиться всё, что угодно. Я даже вам привел пример, какую оптимизацию может делать компилятор начиная с с++11 - а именно предполагать, что переменная НЕ шарится между тредами. Иначе можно быстро дорассуждаться до того, что компилить каждый инт как atomic.

Ну Вы же прекрасно понимаете что "частные случаи" ничего не доказывают. Тот же mutex может быть моим классом, а тот же make_shared - звать мой конструктор.
Давайте поговорим о конструкторе и "чужом" коде. Вспоминаем статью Александреску и Мейерса про сиглтоны.
В частности, такой код:
Код:
if (!m_instance) {
    std::unique_lock<std::mutex> l(m_mutex);
    if (!m_instance)
        m_instance = new Singleton();
}
return m_instance;
Код безопасный? Ну а чо, присваивание указателя "атомарно" же, как любят рассказывать "свидетели атомарного инта". Развернем немного что происходит под капотом:
Код:
if (!m_instance) {
    auto tmp = malloc(sizeof (Singleton));
    new (tmp) Singleton();
    m_instance = tmp;
}
А если компилятор не использует временную переменную (с чего бы, кстати?):
Код:
if (!m_instance) {
    m_instance = malloc(sizeof (Singleton));
    // <=== ой, тут пробой - указатель УЖЕ не ноль, а объекта ещё нет
    new (m_instance) Singleton();
}
А даже если использует:
Код:
if (!m_instance) {
    auto tmp = malloc(sizeof (Singleton));
    m_instance = tmp; // процессор сделал reorder
    // <=== ой, тут пробой
    new (tmp) Singleton(); // предположим что тут "вызов" заинлайнился и в конструкторе мы не юзаем m_instance, т.е. нет зависимости по данным
}
Помог мьютекс? Помогло "атомарное" присваивание указателя?
Если что, статья написана во времена до с++11 и все эти проблемы хорошо известны и решение то же, что и с с++11 - юзайте барьеры памяти:
Цитировать
The general solution is to use memory barriers (i.e., fences)


Опять как-то у Вас все "очень просто", типа "натыкать атомиков побольше - и будет счастье" Улыбающийся Скорее эффект будет обратный - скорость подсядет на lock, код засорится, но без всяких достижений. Впрочем это обычный рез-т чрезмерно усердного чтения.
У меня тут также говорят, а потом я две недели исправляю баг связанный с тем, что один объект дергается из двух тредов безо всяких синхронизаций. Ну а чо, мьютексы это ж медленно. Парсить гигабайтные файлы через QString::split быстро, а мьютексы медленно. Две недели это занимает потому, что попытка полечить одно ломает код в другом рандомном месте (например, внутри QLocale, ибо там тоже "забыли" что QLocale::system() может ВНЕЗАПНО дергаться из разных тредов), и нельзя просто взять и перенести код в "правильный" тред, тайминги поехали, хрупкое равновесие нарушилось и всё щячло попячься ололо пыщ-пыщ.

Глянув первые пару фраз я быстро понял что речь пойдет обо всем-всем. Очень может быть что там есть вещи что я не постиг при написании своих lock-free великов, но их будет не так уж много. И как-то фильтровать/вылавливать эти "жемчужные зерна" - нет уж, увольте. А "беглый просмотр по диагонали" практически бесполезен.
Речь идет обо всем-всем потому что необходимо понимание "всего-всего" чтобы не было мифов про всемогущий volatile, про "разъехавшиеся кеши" и про "атомарный int".
« Последнее редактирование: Май 29, 2019, 12:45 от Авварон » Записан
Igors
Джедай : наставник для всех
*******
Offline Offline

Сообщений: 11445


Просмотр профиля
« Ответ #29 : Май 29, 2019, 15:12 »

// <=== ой, тут пробой - указатель УЖЕ не ноль, а объекта ещё нет
Да, это подлянка известная. В одном из компиляторов что мне встречались даже была опция чтобы это пресекать

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

Если что, статья написана во времена до с++11 и все эти проблемы хорошо известны и решение то же, что и с с++11 - юзайте барьеры памяти:
Цитировать
The general solution is to use memory barriers (i.e., fences)
Вы прямо-таки навлекаете на себя (справедливый) гнев читателя букваря! Улыбающийся
Цитировать
Кароч, как правельно? Не сказал, вычитал в инете и измывается, гад
Ну ладно, мы не ищем легких путей. Откроем статью
Код
C++ (Qt)
Singleton* Singleton::instance ()
{
  Singleton* tmp = pInstance;
 
...// insert memory barrier
 
  if(tmp == 0) {
     Lock lock;
     tmp = pInstance;
     if (tmp == 0) {
        tmp = new Singleton;
 
...// insert memory barrier
 
        pInstance = tmp;
    }
  }
  return tmp;
}
Ну и в качестве барьера видимо надо заюзать
Код
C++ (Qt)
std::atomic_thread_fence(std::memory_order_acquire);
Согласен, хорошее, грамотное решение. Правда не вижу зачем первый-то барьер понадобился?
И все-таки рискну предложить свою версию (без барьеров)
Код
C++ (Qt)
if (!m_instance) {
   std::unique_lock<std::mutex> l(m_mutex);
   if (!m_instance) {
       auto temp = new Singleton();
       if (temp)
         m_instance = temp;
   }
}
return m_instance;
 
UB допускать нельзя потому что это не "нолик" или "единичка", это ваш внезапно умерший кот или клоун, прыгающий у вас на носу. UB значит, что может случиться всё, что угодно. Я даже вам привел пример..
Я тоже Вам привел цитату что при чтении памяти могут быть варианты, но отнюдь не "что угодно"

Речь идет обо всем-всем потому что необходимо понимание "всего-всего" чтобы не было мифов про всемогущий volatile, про "разъехавшиеся кеши" и про "атомарный int".
Ну каждый из этих мифов имеет весомые основания и не раз подтверждался на практике. Поэтому все они и подвержены (массовым) злоупотреблениям, т.е. тыкаются везде и без разбора. Замечу что Ваше использование атомиков тоже очень смахивает на очередной миф  Улыбающийся
Записан
Страниц: 1 [2] 3   Вверх
  Печать  
 
Перейти в:  


Страница сгенерирована за 0.27 секунд. Запросов: 22.