Re[54]: Безопасность Rust
От: · Великобритания  
Дата: 07.06.19 17:49
Оценка: :)
Здравствуйте, vdimas, Вы писали:

V>>>·>Да. И ведёт к undefined behaviour.

V>>>Докажи.
V>·>

If a data race occurs, the behavior of the program is undefined.

V>Вот как здесь — общий случай, который не относится к частному.
V>Требовалось доказать, что для атомарных операций чтения/записи это тоже верно.
Атомарные операции определены только над атомарными типами. Список атомарных типов приводится явно. Атомарные типы гарантируют отсутствие data race. Это я уже тоже многократно цитировал.

V>·>(с) https://www.modernescpp.com/index.php/race-condition-versus-data-race

V>Учу читать по твоей ссылке, дорого:
V>

V>Race condition:
V>A race condition is a situation, in which the result of an operation depends on the interleaving of certain individual operations.
V>Data race:
V>A data race is a situation, in which at least two threads access a shared variable at the same time. At least on thread tries to modify the variable.

V>Вопросы?
Ты читай до конца и не цитируй фигурно. Ты заявил "data race может быть причиной race condition." с этим я и спорил. На самом деле ровно наоборот:

A race condition can be the reason for a data race


V>>>>>Но может быть и частью алгоритма.

V>>>·>Data race не может быть частью корректного алгоритма, т.к. является undefined behaiour и никакие характеристики алгоритма обеспечить не может.
V>>>Докажи.
V>·>

If a data race occurs, the behavior of the program is undefined.

V>·>(с) https://en.cppreference.com/w/cpp/language/memory_model
V>Докажи верность этого утверждения для атомарно изменяемых переменных.
Я не знаю что такое ты имеешь конкретно "атомарно изменяемых переменных". Есть типы, объекты у которых есть атомарные операции. Эти типы — std::atomic или _Atomic.
Стандарт говорит: A program that has two conflicting evaluations has a data race unless ... both conflicting evaluations are atomic operations (see std::atomic)"
Следовательно, std::atomic не создают data race. И следовательно их поведение не является undefined.

V>·>Доказывать твои фантазии? Упаси боже. Ты глупость сказал, ты и доказывай.

V>Глупости тут говоришь ты.
V>А моей целью было заставить тебя начать думать.
V>Начал бы ты думать и обнаружил бы, что для атомарных типов мьютекс в std::atomic не нужен не спроста.
Разберись что такое is_lock_free. Если есть вопросы, задавай, вместо того чтобы в очередной раз писать некорректное утверждение.

V>И при этом гарантируется отсутствие гонок.

Отсутствие гонок гарантируется для всех std::atomic.

Да, я ещё забыл о std::atomic_flag, в котором по дизайну нет мьютекса и который гарантирует отсутствие data race.

V>При том что достоверно известно, что одновременные чтения и запись в переменную идут.

V>Т.е. прямо согласно определению, это data race.
V>Или не совсем?
описание data race явно упоминает atomic-типы: "data race unless ... atomic".

V>>>Я хочу сказать, что такой код всегда корректный.

V>·>Это твои фантазии.
V>Доказать не в состоянии.
V>Слился.
Я знаю, что ты хочешь сказать. Ты много что болтаешь. Ты это должен доказывать свои высказывания или идти лесом. Доказывать твои фантазии я не собираюсь.

V>·>(с) https://en.cppreference.com/w/c/language/atomic


V>Сравни:

V>they may be modified by two threads concurrently or modified by one and read by another.
V>...
V>A data race is a situation, in which at least two threads access a shared variable at the same time. At least on thread tries to modify the variable.
А слова чуть раньше ты конечно обрезал: "Objects of atomic types are the only objects that are free from data races".

Если ты ещё раз будешь такие фокусы выкидывать, я просто прекращу общение. Это тупо неуважение к собеседнику.

V>Ты уже сравнил определения выше.

V>В чём отличия?
В том что ты внаглую режешь предложения на кусочки. Классическое "Было бы ошибкой думать. В.И.Ленин."

V>Да, и этого достаточно.

V>Но тред не просто так прерывает своё исполнение, а в очень важный для абстрактной машины состояния процессора момент.
V>В какой именно момент, ы?
Это дело операционки, а не яп. Мы обсуждаем яп.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[50]: Безопасность Rust
От: · Великобритания  
Дата: 07.06.19 17:50
Оценка:
Здравствуйте, vdimas, Вы писали:

V>·>Это не имеет никакого отношения к тезису "int умещается в один регистр".

V>Имеет. ))
V>Попробуешь еще раз?
Если не можешь тыкнуть пальцем в стандарт, иди лесом.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[51]: Безопасность Rust
От: vdimas Россия  
Дата: 08.06.19 05:58
Оценка:
Здравствуйте, ·, Вы писали:

V>>·>Это не имеет никакого отношения к тезису "int умещается в один регистр".

V>>Имеет.
V>>Попробуешь еще раз?
·>Если не можешь тыкнуть пальцем в стандарт, иди лесом.

Могу ткнуть — стандарт отдаёт этот момент на откуп реализации.
Почему?
Re[55]: Безопасность Rust
От: vdimas Россия  
Дата: 08.06.19 13:36
Оценка:
Здравствуйте, ·, Вы писали:

V>>·>

If a data race occurs, the behavior of the program is undefined.

V>>Вот как здесь — общий случай, который не относится к частному.
V>>Требовалось доказать, что для атомарных операций чтения/записи это тоже верно.
·>Атомарные операции определены только над атомарными типами. Список атомарных типов приводится явно.

Он приводится для железок в т.ч.


·>Атомарные типы гарантируют отсутствие data race.


Верно.


·>Это я уже тоже многократно цитировал.


Без ума креститься, как известно...



V>>Учу читать по твоей ссылке, дорого:

V>>Race condition:
V>>A race condition is a situation, in which the result of an operation depends on the interleaving of certain individual operations.
V>>Data race:
V>>A data race is a situation, in which at least two threads access a shared variable at the same time. At least on thread tries to modify the variable.
V>>Вопросы?
·>Ты читай до конца и не цитируй фигурно.

А ты не бегай.
Решил поспорить с терминологией — спорь.
Заодно удачки, кстате.


·>Ты заявил "data race может быть причиной race condition." с этим я и спорил. На самом деле ровно наоборот:

·>

A race condition can be the reason for a data race


Ес-но, если уже пошли-поехали по памяти, то возможно что угодно.
Но это уже вторичные свето-шумовые эффекты, неуправляемые. И не факт что будут именно они, хотя могут и быть.
Но мы уже неделю обсуждаем другой сценарий — когда data race провоцируется программистом сознательно.

Так шта, заказнчивай бегать.
Не понимал ты определений до этой подветки? — не понимал.
Вникать в твои виляния "да вы не так меня поняли" желания ноль.


V>>·>

If a data race occurs, the behavior of the program is undefined.

V>>Докажи верность этого утверждения для атомарно изменяемых переменных.
·>Я не знаю что такое ты имеешь конкретно "атомарно изменяемых переменных".

Замени на "атомарно изменяемых данных", пофик.


·>Есть типы, объекты у которых есть атомарные операции. Эти типы — std::atomic или _Atomic.


А у других типов нету, что ле?
А что мне мешает ручками нарисовать объект навроде джавовского Synchronized?
Что-то ты совсем решил моск отключить, смотрю...

Сосредоточья, плиз, — стандарт не утверждает, что в природе не существует других атомарных типов и операций над ними.
Он лишь гарантирует, что вот эти — точно атомарны.
Ну и всё, собсно.


·>Стандарт говорит: A program that has two conflicting evaluations has a data race unless ... both conflicting evaluations are atomic operations (see std::atomic)"

·>Следовательно, std::atomic не создают data race. И следовательно их поведение не является undefined.

Лучше бы ты о memory_order говорил, там хоть какой-то смысл был.
Потому что тебя можно тыкать аки неразумного котёнка в спеку любой существующей на свете железки, в которой тоже будет сказано, что обращение к машинному слово атомарно.
И что характерно, блин, я тебя в эту цитату из стандарта тоже тыкал.


V>>Начал бы ты думать и обнаружил бы, что для атомарных типов мьютекс в std::atomic не нужен не спроста.

·>Разберись что такое is_lock_free.

Да ты упорот... )


·>Если есть вопросы, задавай, вместо того чтобы в очередной раз писать некорректное утверждение.


Я-то задаю нужные, но ты скромно их игноришь:

Для атомарных типов и единственной операции?
Не будет, бо это невозможно выразить схемотехнически.
Собсно, потому что нельзя выразить логически, поэтому нельзя схемотехнически.
И ты логически выразить тоже не сможешь.
Мог бы — уже давно бы членораздельно разложил всё по полочкам, что и где не так, вместо кивания на "авторитеты" в лице стандарта, который ты и не читал целиком, а где читал — толком не разобрался всё-равно.


Вот тебе вопрос — вырази, плиз, каким либо образом (можно на пальцах, можно на каракулях) неатомарную операцию записи в память машинного слова.
Напомню, что "машинное слово" означает основную (аппаратную) разрядность регистра процессора, соответствующих внутренних шин данных и как минимум не меньших по ширине (можно больших, кратных) внешних шин данных.
Вперёд!
Отредактировано 08.06.2019 13:38 vdimas . Предыдущая версия . Еще …
Отредактировано 08.06.2019 13:38 vdimas . Предыдущая версия .
Re[45]: Безопасность Rust
От: alex_public  
Дата: 10.06.19 10:36
Оценка: +1
Здравствуйте, ·, Вы писали:

_>>Скорее закон целесообразности. )

·>Стронгли белив, ок.

Ну да, приблизительно как вера в то, что бег в лыжах по асфальту не станет доминирующим видом спорта. Т.е. как бы если очень хочется, то можно, но только зачем? )))

_>>На том основание, что int умещается в один регистр. )

·>И? А ссылка на спеку где?

Это не входит в стандарт, т.к. платформозависимо. И если ты помнишь, то ещё в самом начале нашей дискуссии я формулировал это как "int (подразумевая машинное слово)", специально на случай таких придирок. Если уж так хочется кроссплатформенности по стандарту, то есть sig_atomic_t. Но на практике он всегда определяется как typedef int.

_>>Не, как раз всё видно. Вот если бы там было например по две операции чтения/записи нашей переменной в каждом потоке, то уже можно было начинать беспокоиться... )

·>А ты видишь какие есть операции в process() и какие там окажутся через год активной разработки?

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

_>>Ну я вроде как рядом приводил вполне конкретные примеры, в которых это вполне актуально.

·>Актуально писать корректный код, а не бояться оптимизаций.

Так, ещё раз по пунктам:
1. конкретный обсуждаемый пример кода не боится никаких оптимизаций
2. в тех случаях, когда оптимизация может навредить, ставят ключевое слово volatile и снова ничего не боятся.

Ещё какие-то комментарии?

_>>Правильно. И как ты видишь перестановку обращений к одной области памяти, которая не поменяет поведение однопоточного кода? ) Ну и которая не сводится к объединению двух одинаковых команд в одну (что отключается через volatile).

·>А, понял что ты имеешь в виду. Да, согласен. Другой вопрос, насколько это реально иметь не более одного обращения к памяти в сколько-нибудь нетривиальной программе.

Эммм, ну я надеюсь ты конечно же в курсе про то, что кроме искусственных (вставляемых человеком) барьеров памяти, есть и так сказать природные? )))

_>>Ты всё время путаешь два типа оптимизаций. В одной возможны перестановки команд, а в другой возможны слияния или даже опускания команд. Первый тип реализуется и компилятор и процессором. И отключается через соответствующие fence вызовы. А второй типа реализуется только компилятором и отключается для переменных с модификатором volatile.

·>Допустим мы заставили компилятор нагенерить:
·>
·>mov [x], 1
·>// do stuff
·>mov [x], 2
·>

·>Проц имеет право слить эти операции в mov [x], 2. Ну, понятное дело, если проц знает, что данный [x] это mm i/o, то он сливать не будет. Если же адрес в памяти и нет fence — можно же и заоптимизировать — всё в соответствии со спекой. Точно так же проц имеет право переупорядочивать или пропускать чтения и записи.

Ух, какая феерия... ) Ну тогда раз "в соответствие со спекой", то тебе будет не сложно указать какой конкретно процессор выполняет такие оптимизации и за одно кинуть ссылку на эту самую его спеку... )))
Re[43]: Безопасность Rust
От: alex_public  
Дата: 10.06.19 11:05
Оценка:
Здравствуйте, ·, Вы писали:

_>>·>Т.е. пользу языка ты оцениваешь по тому, что в ~двух конкретных параграфах документации не описано то что ты хочешь. УмнО!

_>>Я этот язык внимательно рассматриваю с таких давних пор, в которые ты даже мельком не слышал это название. ))) Причём рассматриваю не просто из абстрактного интереса, а с точки зрения замены им моего основного инструмента! Так что пользу его я отлично знаю. Правда чем дальше идёт время от 2014-го года, тем меньше я вижу смысла в переходе (сравнивая направление и темпы развития Rust и C++), но это отдельный вопрос.
·>Так бы и сказал, что это твоё мнение, тебе язык не импонирует. И т.п.

Плохо читаешь что-то. Как может не импонировать язык, переход на который (причём не по вынужденным внешним бизнес причинам, а по личному выбору — у нас я определяю стек технологий) серьёзно обдумываешь? В любом случае для меня Rust и C++ однозначно занимают первые два места в списке системных языков. Только вот до 2014-го года первым казался Rust, сейчас они кажутся равными, а в 2020-ом (после выхода соответствующего стандарта) первым явно станет старичок C++.

·>Зачем было перевирать факты — мне неясно.


Эээ что? Какие факты и кто здесь перевирал?

_>>Да, и в твоей цитате было ясно написано, что для атомарных данных data race не будет. )

·>Верно. Там же явно указывалось, что считается атомарными данными в том месте — только std::atomic, а не "все типы у которых есть слово atomic в имени".

Т.е. если я напишу свой тип с атомарным поведением (а внутри там вообще мьютекс будет!), то его использование из нескольких потоков всё равно будет data race по твоей теории? )))

_>>·>Objects of atomic types are the only objects that are free from data races, that is, they may be modified by two threads concurrently or modified by one and read by another.

_>>Это их свойство, а не определение. )
·>Это определяющее свойство. Просто формулировка такая. Можно немного вывернуть, чтобы получилось по виду как определение:
·>Atomic type — is a type which objects are free from data races.
·>Притом такие типы там явно перечислены. И ключевое слово там "only". Т.е. все остальные типы — подвержены data race, а следовательно их надо защищать от конкурентных операций с помощью мьютексов и т.п.

Нет, у них это как раз свойство. А вот если взять это твоё переделанное определение, то под него легко подойдёт и sig_atomic_t. Конечно можно попытаться каким-то образом приделать сюда необходимость fence. Но тогда резко окажется, что и atomic<int> с опцией relaxed нельзя считать атомарным (а он же вроде считается, не так ли?). В общем с твоими тезисами у тебя никак не выходит стройной картины — одна муть какая-то.
Re[46]: Безопасность Rust
От: · Великобритания  
Дата: 10.06.19 14:06
Оценка:
Здравствуйте, alex_public, Вы писали:

_>>>На том основание, что int умещается в один регистр. )

_>·>И? А ссылка на спеку где?
_>Это не входит в стандарт, т.к. платформозависимо. И если ты помнишь, то ещё в самом начале нашей дискуссии я формулировал это как "int (подразумевая машинное слово)", специально на случай таких придирок. Если уж так хочется кроссплатформенности по стандарту, то есть sig_atomic_t. Но на практике он всегда определяется как typedef int.
Т.е. это undefined behaviour. Я тогда не понимаю о чём спор. Ты меня пытаешься убедить, что в некоторых случаях undefined behaviour может работать так как ожидается? Я с этим и не спорил. Я спорил лишь о том, что такой код писать не надо. Более того, не то что не надо, а в общем-то нет никакой необходимости. Соответствующий корректно написанный код будет работать всегда, и притом работать ровно так же, с точностью до ассемблерных инструкций, если его использовать в тех же условиях, в которых ты используешь тот некорректный код.

_>>>Не, как раз всё видно. Вот если бы там было например по две операции чтения/записи нашей переменной в каждом потоке, то уже можно было начинать беспокоиться... )

_>·>А ты видишь какие есть операции в process() и какие там окажутся через год активной разработки?
_>С этой переменной там в любом случае не должно быть никаких операций, т.к. она передаётся как параметр в функцию.
Хм. Это вообще-то глобальная переменная, и ничто не мешает её использовать откуда угодно. Более того, там могут использоваться какие-то другие переменные и внезапно проблема реордеринга возникнет в полный рост.

_>>>Ну я вроде как рядом приводил вполне конкретные примеры, в которых это вполне актуально.

_>·>Актуально писать корректный код, а не бояться оптимизаций.
_>Так, ещё раз по пунктам:
_>1. конкретный обсуждаемый пример кода не боится никаких оптимизаций
Это нельзя утверждать без точного знания что происходит в окружающем коде.

_>2. в тех случаях, когда оптимизация может навредить, ставят ключевое слово volatile и снова ничего не боятся.

_>Ещё какие-то комментарии?
В корректно написанном коде оптимизация не может навредить в принципе (если исключить баги компилятора). В худшем случае вред от оптимизации может выражаться лишь в ухудшении перформанса, вместо улучшения.

_>·>А, понял что ты имеешь в виду. Да, согласен. Другой вопрос, насколько это реально иметь не более одного обращения к памяти в сколько-нибудь нетривиальной программе.

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

_>·>Проц имеет право слить эти операции в mov [x], 2. Ну, понятное дело, если проц знает, что данный [x] это mm i/o, то он сливать не будет. Если же адрес в памяти и нет fence — можно же и заоптимизировать — всё в соответствии со спекой. Точно так же проц имеет право переупорядочивать или пропускать чтения и записи.

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

A multi-processor system consists of four processors — P1, P2, P3 and P4, all containing cached copies of a shared variable S whose initial value is 0. Processor P1 changes the value of S (in its cached copy) to 10 following which processor P2 changes the value of S in its own cached copy to 20. If we ensure only write propagation, then P3 and P4 will certainly see the changes made to S by P1 and P2. However, P3 may see the change made by P1 after seeing the change made by P2 and hence return 10 on a read to S. P4 on the other hand may see changes made by P1 and P2 in the order in which they are made and hence return 20 on a read to S. The processors P3 and P4 now have an incoherent view of the memory.

но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[47]: Безопасность Rust
От: vdimas Россия  
Дата: 10.06.19 14:30
Оценка:
Здравствуйте, ·, Вы писали:

·>Это вроде эффект наличия кешей. Другое ядро увидит значение только после того как данное ядро выгрузит свой кеш в оперативку.


До выгрузки в оперативку когерентный механизм сначала разулит у себя унутре.
Там в любом случае писать в переменную сможет лишь одно ядро в один момент времени.

Многопоточные ядра Intel (2 потока) и Sun (4 потока) как раз возникли из-за простоя ядер в результате коллизий кеша — можно отдавать простаивающие вычислительные блоки ядра другим аппаратным логическим потокам (но физическим с т.з. операционки).
Re[44]: Безопасность Rust
От: · Великобритания  
Дата: 10.06.19 14:35
Оценка:
Здравствуйте, alex_public, Вы писали:

_>·>Так бы и сказал, что это твоё мнение, тебе язык не импонирует. И т.п.

_>Плохо читаешь что-то. Как может не импонировать язык, переход на который (причём не по вынужденным внешним бизнес причинам, а по личному выбору — у нас я определяю стек технологий) серьёзно обдумываешь? В любом случае для меня Rust и C++ однозначно занимают первые два места в списке системных языков. Только вот до 2014-го года первым казался Rust, сейчас они кажутся равными, а в 2020-ом (после выхода соответствующего стандарта) первым явно станет старичок C++.
Ок, я не правильно выразился. Просто "для ваших проектов не имеет значительных преимуществ".

_>·>Зачем было перевирать факты — мне неясно.

_>Эээ что? Какие факты и кто здесь перевирал?
Ну что rust не даёт гарантий больше, чем плюсы.

_>>>Да, и в твоей цитате было ясно написано, что для атомарных данных data race не будет. )

_>·>Верно. Там же явно указывалось, что считается атомарными данными в том месте — только std::atomic, а не "все типы у которых есть слово atomic в имени".
_>Т.е. если я напишу свой тип с атомарным поведением (а внутри там вообще мьютекс будет!), то его использование из нескольких потоков всё равно будет data race по твоей теории? )))
Начнём с того, что такой тип ты не сможешь написать _корректно_ без использования стандартизованных подходов в языке. В крайнем случае, это будет платформозависимый код, ffi по сути, т.е. к языку не имеет отношения. Иначе говоря, тебе придётся обеспечивать атомарность внешними по отношению к языку путями (aka unsafe в rust).
Во-вторых, вроде бы очевидно, что стандарт описывает типы в стандарте. Так что тип твой может и будет atomic, но это не стандартный тип. sig_atomic, volatile, int — и т.п. это мы всё осбуждали стандартные типы.
В-третьих, твой "внутри там мьютекс" это и есть стандартный способ избежать data race, т.к. обращение к памяти будет эксклюзивным, а не конкурентным.

_>·>Это определяющее свойство. Просто формулировка такая. Можно немного вывернуть, чтобы получилось по виду как определение:

_>·>Atomic type — is a type which objects are free from data races.
_>·>Притом такие типы там явно перечислены. И ключевое слово там "only". Т.е. все остальные типы — подвержены data race, а следовательно их надо защищать от конкурентных операций с помощью мьютексов и т.п.
_>Нет, у них это как раз свойство. А вот если взять это твоё переделанное определение, то под него легко подойдёт и sig_atomic_t. Конечно можно попытаться каким-то образом приделать сюда необходимость fence. Но тогда резко окажется, что и atomic<int> с опцией relaxed нельзя считать атомарным (а он же вроде считается, не так ли?). В общем с твоими тезисами у тебя никак не выходит стройной картины — одна муть какая-то.
Нет. Принципиальная разница между sig_atomic и atomic+relaxed в том, что поведение первого типа стандарт не определяет, а второго — определяет.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[47]: Безопасность Rust
От: alex_public  
Дата: 10.06.19 23:26
Оценка:
Здравствуйте, ·, Вы писали:

_>>Это не входит в стандарт, т.к. платформозависимо. И если ты помнишь, то ещё в самом начале нашей дискуссии я формулировал это как "int (подразумевая машинное слово)", специально на случай таких придирок. Если уж так хочется кроссплатформенности по стандарту, то есть sig_atomic_t. Но на практике он всегда определяется как typedef int.

·>Т.е. это undefined behaviour. Я тогда не понимаю о чём спор. Ты меня пытаешься убедить, что в некоторых случаях undefined behaviour может работать так как ожидается? Я с этим и не спорил. Я спорил лишь о том, что такой код писать не надо. Более того, не то что не надо, а в общем-то нет никакой необходимости. Соответствующий корректно написанный код будет работать всегда, и притом работать ровно так же, с точностью до ассемблерных инструкций, если его использовать в тех же условиях, в которых ты используешь тот некорректный код.

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

_>>С этой переменной там в любом случае не должно быть никаких операций, т.к. она передаётся как параметр в функцию.

·>Хм. Это вообще-то глобальная переменная, и ничто не мешает её использовать откуда угодно. Более того, там могут использоваться какие-то другие переменные и внезапно проблема реордеринга возникнет в полный рост.

Ну так глобальная она только ради простоты форумного примера. Понятно что в серьёзных проектах прямо такой код никогда не пишется (собственно там вообще глобальные переменные — это зло).

_>>Так, ещё раз по пунктам:

_>>1. конкретный обсуждаемый пример кода не боится никаких оптимизаций
·>Это нельзя утверждать без точного знания что происходит в окружающем коде.

)))

_>>2. в тех случаях, когда оптимизация может навредить, ставят ключевое слово volatile и снова ничего не боятся.

_>>Ещё какие-то комментарии?
·>В корректно написанном коде оптимизация не может навредить в принципе (если исключить баги компилятора). В худшем случае вред от оптимизации может выражаться лишь в ухудшении перформанса, вместо улучшения.

И к чему ты написал эти очевидные истины? )))

_>>·>А, понял что ты имеешь в виду. Да, согласен. Другой вопрос, насколько это реально иметь не более одного обращения к памяти в сколько-нибудь нетривиальной программе.

_>>Эммм, ну я надеюсь ты конечно же в курсе про то, что кроме искусственных (вставляемых человеком) барьеров памяти, есть и так сказать природные? )))
·>"природные"? Вставляемые богом что-ли? Нет уж, есть лишь определённые в стандарте, в соответствующих конструкциях. Например, создание треда ставит барьер.

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

_>>·>Проц имеет право слить эти операции в mov [x], 2. Ну, понятное дело, если проц знает, что данный [x] это mm i/o, то он сливать не будет. Если же адрес в памяти и нет fence — можно же и заоптимизировать — всё в соответствии со спекой. Точно так же проц имеет право переупорядочивать или пропускать чтения и записи.

_>>Ух, какая феерия... ) Ну тогда раз "в соответствие со спекой", то тебе будет не сложно указать какой конкретно процессор выполняет такие оптимизации и за одно кинуть ссылку на эту самую его спеку... )))
·>Это вроде эффект наличия кешей. Другое ядро увидит значение только после того как данное ядро выгрузит свой кеш в оперативку. Это происходит не при каждой модификации адреса и ещё веселее когда есть несколько пишущих ядер.
·>

A multi-processor system consists of four processors — P1, P2, P3 and P4, all containing cached copies of a shared variable S whose initial value is 0. Processor P1 changes the value of S (in its cached copy) to 10 following which processor P2 changes the value of S in its own cached copy to 20. If we ensure only write propagation, then P3 and P4 will certainly see the changes made to S by P1 and P2. However, P3 may see the change made by P1 after seeing the change made by P2 and hence return 10 on a read to S. P4 on the other hand may see changes made by P1 and P2 in the order in which they are made and hence return 20 on a read to S. The processors P3 and P4 now have an incoherent view of the memory.


Причём тут вообще кэши. У тебя было однозначное заявление, что некие процессоры в целях оптимизации не только меняют порядок выполнения инструкций, но и сливают/выкидывают некоторые из них. Так вот хотелось услышать что это за такие процессоры, поимённо.
Re[63]: Безопасность Rust
От: Ikemefula Беларусь http://blogs.rsdn.org/ikemefula
Дата: 11.06.19 14:06
Оценка:
Здравствуйте, vdimas, Вы писали:

V>>>>>Так в языке или через внешние ср-ва? ))

I>>>>Это у тебя надо спросить
V>>>Ты на вопрос ответь.
I>>Я процитировал твоё собтсвенное противоречие.

V>Ты процитировал не меня.

V>Процитируй мои точные фразы.

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

Сначала ты утверждаешь, что volatile это для многопоточности. Но внезапно выясняется, что до недавних пор, C11, в Си на этот счет ничего не было, то есть, решалось индивидуально, компилятор + конкретное железо.
Далее, ты сам же утверждаешь, что для взрослых кейсов нужен fence, т.к. volatile не хватит.
В целом, получается так volatile может ничего и не дать для меж-поточных флагов
Странная фича — вроде бы про многопоточность, а никаких гарантий не даёт.
Соответственно, все выглядит так, как будто ты или придаешь какой то иной смысл volatile и многозадачности, или хочешь просто выиграть форумный спор. Второе как то более вероятно.
Re[45]: Безопасность Rust
От: alex_public  
Дата: 11.06.19 16:05
Оценка:
Здравствуйте, ·, Вы писали:

_>>Эээ что? Какие факты и кто здесь перевирал?

·>Ну что rust не даёт гарантий больше, чем плюсы.

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

1. Управление памятью. Здесь и C++ и Rust полностью решают проблему со всеми гарантиями, причём практически одинаковыми способами. Но есть небольшие отличия в нюансах. Самая идея решения заключается в том, что мы локализуем (это возможно только если в языке реализованы механизмы RAII и передачи владения) весь "опасный" код непосредственно управления памятью в нескольких специальных типах-контейнерах. Соответственно если в оставшейся части программы не будет использоваться "опасный код" напрямую, а только через эти контейнеры, то мы получим все нужные гарантии. А нюанс заключается в том, что в Rust этот самый опасный код описывается достаточно просто "код с блоками unsafe", в то время как в C++ это формулируется несколько более длинно. Это как бы маленький плюс Rust'а — неявное принуждение компилятором к написанию безопасного кода за пределами специальных типов. Но тут же есть и минус: при такой модели существует целый класс абсолютно безопасных кусков кода, которые при этом компилятор Rust не пропустит без unsafe. И соответственно при требование минимизировать количество unsafe это будет приводит к переписыванию части кода существенно более сложным образом (на самом деле там всё равно добавляется unsafe, но спрятанный в библиотечном коде и используемый например как лишний контейнер). Казалось бы наблюдается равенство (мелкий плюс на мелкий минус), однако если вспомнить про то, что в Rust почему-то отсутствую гарантии (в том числе и в чистом safe коде) на работу RAII, то оказывается что C++ тут имеет даже больше гарантий.

2. Управление параллелизмом. В C++ в рамках стандарта языка имеется бедненький набор самых базовых примитивов и никаких разговоров о гарантиях. Плюс к этому имеется огромное числе сторонних библиотек, решающих все возможные варианты данной проблемы, в том числе и с широчайшими гарантиями. В Rust'е ситуация практически аналогичная, разве что библиотеки послабее в силу меньшего времени развития. Плюс к этому в Rust'е сделана попытка в рамках языка добавить гарантии решения одной мелкой проблемы параллелизма (причём только в определённых условиях) приблизительно по той же схеме, что и с памятью: объявляем все межпоточные (но и только!) взаимодействия как unsafe и соответственно заставляем локализовать их в специальных типах, через которых происходит всё взаимодействие. Разница в ситуации с памятью в том, что таким образом решается только маленькая часть проблем параллелизма. В то время как некоторые сторонние библиотеки (и в C++ и в Rust), основанные на других подходах к проблеме параллелизма, предлагают решения с гораздо большими гарантиями. Ну а в тех редких случаях, когда эти библиотеки не подходят, обычно подходит только "матёрый unsafe" (что в C++, что в Rust). В итоге пользы от такой попытки особой нет (проще сразу взять сильное решение с настоящими гарантиями), а вот вред (в точности такой же, как и в ситуации с памятью) никуда не девается.

_>>·>Верно. Там же явно указывалось, что считается атомарными данными в том месте — только std::atomic, а не "все типы у которых есть слово atomic в имени".

_>>Т.е. если я напишу свой тип с атомарным поведением (а внутри там вообще мьютекс будет!), то его использование из нескольких потоков всё равно будет data race по твоей теории? )))
·>Начнём с того, что такой тип ты не сможешь написать _корректно_ без использования стандартизованных подходов в языке. В крайнем случае, это будет платформозависимый код, ffi по сути, т.е. к языку не имеет отношения. Иначе говоря, тебе придётся обеспечивать атомарность внешними по отношению к языку путями (aka unsafe в rust).

Эээ что? А std::mutex чем не подходит? )))

·>Во-вторых, вроде бы очевидно, что стандарт описывает типы в стандарте. Так что тип твой может и будет atomic, но это не стандартный тип. sig_atomic, volatile, int — и т.п. это мы всё осбуждали стандартные типы.


Ну т.е. получается что возможны атомарные типы не из того списка, да? )))

·>В-третьих, твой "внутри там мьютекс" это и есть стандартный способ избежать data race, т.к. обращение к памяти будет эксклюзивным, а не конкурентным.


Так о том и речь. Хотя конечно же это будет не lock-free атомарный тип, но это не суть. Мы же ведь не путаем понятие атомарности со многими другими? )

_>>·>Это определяющее свойство. Просто формулировка такая. Можно немного вывернуть, чтобы получилось по виду как определение:

_>>·>Atomic type — is a type which objects are free from data races.
_>>·>Притом такие типы там явно перечислены. И ключевое слово там "only". Т.е. все остальные типы — подвержены data race, а следовательно их надо защищать от конкурентных операций с помощью мьютексов и т.п.
_>>Нет, у них это как раз свойство. А вот если взять это твоё переделанное определение, то под него легко подойдёт и sig_atomic_t. Конечно можно попытаться каким-то образом приделать сюда необходимость fence. Но тогда резко окажется, что и atomic<int> с опцией relaxed нельзя считать атомарным (а он же вроде считается, не так ли?). В общем с твоими тезисами у тебя никак не выходит стройной картины — одна муть какая-то.
·>Нет. Принципиальная разница между sig_atomic и atomic+relaxed в том, что поведение первого типа стандарт не определяет, а второго — определяет.

Вообще то поведение sig_atomic как раз определено в стандарте — это "целочисленый тип, обращения к которому атомарны". Возможно ты где-то там читал, что этого недостаточно (и мы даже знаем, чего не хватает) для спокойного использования в условиях многопоточности. Но это речь про произвольный окружающий код, а не про конкретный частный случай. Точно так же как и relaxed std::atomic<int> вообще говоря нельзя применять в случае произвольного многопоточного кода. Однако почему-то никто не пишет, что relaxed std::atomic<int> не подходит для многопоточности.
Re[46]: Безопасность Rust
От: Zhendos  
Дата: 11.06.19 17:31
Оценка:
Здравствуйте, alex_public, Вы писали:

_>Здравствуйте, ·, Вы писали:


_>>>Эээ что? Какие факты и кто здесь перевирал?

_>·>Ну что rust не даёт гарантий больше, чем плюсы.

_>Это слишком большой и сложный вопрос, чтобы формулировать его в таких детских терминах как больше или меньше гарантий. Именно поэтому тут было такое большое обсуждение. Так уж и быть, попробую в последний раз сформулировать это в одном месте. Для начала надо поделить эту тему на два отдельных вопроса:


_>1. Управление памятью. Здесь и C++ и Rust полностью решают проблему со всеми гарантиями, причём практически одинаковыми способами. Но есть небольшие отличия в нюансах. Самая идея решения заключается в том, что мы локализуем (это возможно только если в языке реализованы механизмы RAII и передачи владения) весь "опасный" код непосредственно управления памятью в нескольких специальных типах-контейнерах. Соответственно если в оставшейся части программы не будет использоваться "опасный код" напрямую, а только через эти контейнеры, то мы получим все нужные гарантии.


Э, в C++ никаких гарантий нет даже если работать только через контейнеры/итераторы/умные указатели.
Например можно получить итератор для std::vector, потом вызвать push_back, а потом продолжать
пользоваться итератором. В Rust это ошибка компиляции, а в С++ это проблема программиста.

_>2. Управление параллелизмом. В C++ в рамках стандарта языка имеется бедненький набор самых базовых примитивов и никаких разговоров о гарантиях. Плюс к этому имеется огромное числе сторонних библиотек, решающих все возможные варианты данной проблемы, в том числе и с широчайшими гарантиями. В Rust'е ситуация практически аналогичная, разве что библиотеки послабее в силу меньшего времени развития. Плюс к этому в Rust'е сделана попытка в рамках языка добавить гарантии решения одной мелкой проблемы параллелизма (причём только в определённых условиях) приблизительно по той же схеме, что и с памятью: объявляем все межпоточные (но и только!) взаимодействия как unsafe и соответственно заставляем локализовать их в специальных типах, через которых происходит всё взаимодействие. Разница в ситуации с памятью в том, что таким образом решается только маленькая часть проблем параллелизма. В то время как некоторые сторонние библиотеки (и в C++ и в Rust), основанные на других подходах к проблеме параллелизма, предлагают решения с гораздо большими гарантиями. Ну а в тех редких случаях, когда эти библиотеки не подходят, обычно подходит только "матёрый unsafe" (что в C++, что в Rust). В итоге пользы от такой попытки особой нет (проще сразу взять сильное решение с настоящими гарантиями), а вот вред (в точности такой же, как и в ситуации с памятью) никуда не девается.


А можно пояснить почему это только "маленькая" проблема.
Допустим мы берем сторонее решение для С++ и Rust.


В Rust сторонняя библиотека "проанатирована" с помощью trait Send/Sync,
поэтому неправильно ее использовать нельзя, остается только проблема с багами
в самой сторонней библиотеке. В С++ же сразу обе проблемы налицо и сама библиотека и ее использование.
Re[46]: Безопасность Rust
От: · Великобритания  
Дата: 11.06.19 17:32
Оценка:
Здравствуйте, alex_public, Вы писали:

_>>>Эээ что? Какие факты и кто здесь перевирал?

_>·>Ну что rust не даёт гарантий больше, чем плюсы.
_>Это слишком большой и сложный вопрос, чтобы формулировать его в таких детских терминах как больше или меньше гарантий. Именно поэтому тут было такое большое обсуждение. Так уж и быть, попробую в последний раз сформулировать это в одном месте. Для начала надо поделить эту тему на два отдельных вопроса:
_>1. Управление памятью. Здесь и C++ и Rust полностью решают проблему со всеми гарантиями, причём практически одинаковыми способами. Но есть небольшие отличия в нюансах. Самая идея решения заключается в том, что мы локализуем (это возможно только если в языке реализованы механизмы RAII и передачи владения) весь "опасный" код непосредственно управления памятью в нескольких специальных типах-контейнерах. Соответственно если в оставшейся части программы не будет использоваться "опасный код" напрямую, а только через эти контейнеры, то мы получим все нужные гарантии.
Это опять некое манипулирование фактами. Ты забыл важный аспект, borrow checker, который и обеспечивает довольно серьёзные гарантии. Rust предоставляет способ композиции безопасного кода в безопасный код. В C++ можно скомбинировать безопасные конструкции и получить хрен знает что и фиг поймёшь чего.

_>А нюанс заключается в том, что в Rust этот самый опасный код описывается достаточно просто "код с блоками unsafe", в то время как в C++ это формулируется несколько более длинно.

Опять привираешь. Никак оно не формулируется. Или формулировка слишком длинна, чтобы её можно было поместить на полях этого форума?

_>Это как бы маленький плюс Rust'а — неявное принуждение компилятором к написанию безопасного кода за пределами специальных типов. Но тут же есть и минус: при такой модели существует целый класс абсолютно безопасных кусков кода, которые при этом компилятор Rust не пропустит без unsafe. И соответственно при требование минимизировать количество unsafe это будет приводит к переписыванию части кода существенно более сложным образом (на самом деле там всё равно добавляется unsafe, но спрятанный в библиотечном коде и используемый например как лишний контейнер). Казалось бы наблюдается равенство (мелкий плюс на мелкий минус), однако если вспомнить про то, что

Это ты тоже немного загнался. Ты предложил переписать какой-то сишный код, в котором ты никак не сформулировал решаемую задачу. Притом очевидно, что этот код сам по себе не имеет смысла. Я тебе это продемонстрировал попросив переписать этот код на модель акторов.

_>в Rust почему-то отсутствую гарантии (в том числе и в чистом safe коде) на работу RAII, то оказывается что C++ тут имеет даже больше гарантий.

Это чистое враньё. Rust даёт вполне определённые гарантии, а C++ — никаких. Забытый virtual у деструктора тихо отправляет все "гарантии" в /dev/null. В остальном всё то же самое — циклическая зависимость каких-нибудь shared_ptr в плюсах тоже нарушает все "гарантии".

_>2. Управление параллелизмом. В C++ в рамках стандарта языка имеется бедненький набор самых базовых примитивов и никаких разговоров о гарантиях. Плюс к этому имеется огромное числе сторонних библиотек, решающих все возможные варианты данной проблемы, в том числе и с широчайшими гарантиями. В Rust'е ситуация практически аналогичная, разве что библиотеки послабее в силу меньшего времени развития. Плюс к этому в Rust'е сделана попытка в рамках языка добавить гарантии решения одной мелкой проблемы параллелизма (причём только в

Если для тебя undefined behaviour не является большой проблемой, то наверно да, можно так заявить.

_>определённых условиях) приблизительно по той же схеме, что и с памятью: объявляем все межпоточные (но и только!) взаимодействия как unsafe и соответственно заставляем локализовать их в специальных типах, через которых происходит всё взаимодействие. Разница в ситуации с памятью в том, что таким образом решается только маленькая часть проблем параллелизма. В то время как некоторые сторонние библиотеки (и в C++ и в Rust), основанные на других подходах к проблеме параллелизма, предлагают решения с гораздо большими гарантиями. Ну а в тех редких случаях, когда эти библиотеки не подходят, обычно подходит только "матёрый unsafe" (что в C++, что в Rust). В итоге пользы от такой попытки особой нет (проще сразу взять сильное решение с настоящими гарантиями), а вот вред (в точности такой же, как и в ситуации с памятью) никуда не девается.

Это какие же _гарантии_?

_>·>Начнём с того, что такой тип ты не сможешь написать _корректно_ без использования стандартизованных подходов в языке. В крайнем случае, это будет платформозависимый код, ffi по сути, т.е. к языку не имеет отношения. Иначе говоря, тебе придётся обеспечивать атомарность внешними по отношению к языку путями (aka unsafe в rust).

_>Эээ что? А std::mutex чем не подходит? )))
Выделил.

_>·>Во-вторых, вроде бы очевидно, что стандарт описывает типы в стандарте. Так что тип твой может и будет atomic, но это не стандартный тип. sig_atomic, volatile, int — и т.п. это мы всё осбуждали стандартные типы.

_>Ну т.е. получается что возможны атомарные типы не из того списка, да? )))
Выделил. _Стандартные_ типы — невозможны. Т.е. sig_atomic — стандартный тип, не гарантирующий atomic в многопоточном коде.

_>·>В-третьих, твой "внутри там мьютекс" это и есть стандартный способ избежать data race, т.к. обращение к памяти будет эксклюзивным, а не конкурентным.

_>Так о том и речь. Хотя конечно же это будет не lock-free атомарный тип, но это не суть.
Только зачем об этом вообще вести речь — непонятно. Я веду речь о немного другом.

_>>>Нет, у них это как раз свойство. А вот если взять это твоё переделанное определение, то под него легко подойдёт и sig_atomic_t. Конечно можно попытаться каким-то образом приделать сюда необходимость fence. Но тогда резко окажется, что и atomic<int> с опцией relaxed нельзя считать атомарным (а он же вроде считается, не так ли?). В общем с твоими тезисами у тебя никак не выходит стройной картины — одна муть какая-то.

_>·>Нет. Принципиальная разница между sig_atomic и atomic+relaxed в том, что поведение первого типа стандарт не определяет, а второго — определяет.
_>Вообще то поведение sig_atomic как раз определено в стандарте — это "целочисленый тип, обращения к которому атомарны".
Опять привираешь. Ты неполностью цитируешь, обрезая важный хвост "... при обработке асинхронных сигналов".

_>Возможно ты где-то там читал, что этого недостаточно (и мы даже знаем, чего не хватает) для спокойного использования в условиях многопоточности. Но это речь про произвольный окружающий код, а не про конкретный частный случай. Точно так же как и relaxed std::atomic<int> вообще говоря нельзя применять в случае произвольного многопоточного кода. Однако почему-то никто не пишет, что relaxed std::atomic<int> не подходит для многопоточности.

Можно применять, конечно. И он будет рабоать в точности с гарантиями relaxed ordering на любой платформе, в любых условиях — реальных или гипотетических. Это позволит, скажем, доказывать теоремы математическими методами для данного куска кода и делать выводы о том как код может себя вести. Использование sig_atomic в многопоточном коде не позволяет делать никакие рассуждения (reasoning) о поведении такого кода в общем случае.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[48]: Безопасность Rust
От: · Великобритания  
Дата: 11.06.19 18:03
Оценка:
Здравствуйте, alex_public, Вы писали:

_>>>Это не входит в стандарт, т.к. платформозависимо. И если ты помнишь, то ещё в самом начале нашей дискуссии я формулировал это как "int (подразумевая машинное слово)", специально на случай таких придирок. Если уж так хочется кроссплатформенности по стандарту, то есть sig_atomic_t. Но на практике он всегда определяется как typedef int.

_>·>Т.е. это undefined behaviour. Я тогда не понимаю о чём спор. Ты меня пытаешься убедить, что в некоторых случаях undefined behaviour может работать так как ожидается? Я с этим и не спорил. Я спорил лишь о том, что такой код писать не надо. Более того, не то что не надо, а в общем-то нет никакой необходимости. Соответствующий корректно написанный код будет работать всегда, и притом работать ровно так же, с точностью до ассемблерных инструкций, если его использовать в тех же условиях, в которых ты используешь тот некорректный код.
_>С учётом того, что эти "некоторые случаи"=="все известные платформы" (в том числе и по твоему признанию), то это конечно же весьма забавное утверждение. )))
Неужели ты предпочитаешь основывать рассуждения не о том, что прямо говорится в стандартне, а на том что я что-то не знаю?

_>>>С этой переменной там в любом случае не должно быть никаких операций, т.к. она передаётся как параметр в функцию.

_>·>Хм. Это вообще-то глобальная переменная, и ничто не мешает её использовать откуда угодно. Более того, там могут использоваться какие-то другие переменные и внезапно проблема реордеринга возникнет в полный рост.
_>Ну так глобальная она только ради простоты форумного примера. Понятно что в серьёзных проектах прямо такой код никогда не пишется (собственно там вообще глобальные переменные — это зло).
И что? Без borrow checker ты не сможешь гарантировать всё равно.

_>>>2. в тех случаях, когда оптимизация может навредить, ставят ключевое слово volatile и снова ничего не боятся.

_>>>Ещё какие-то комментарии?
_>·>В корректно написанном коде оптимизация не может навредить в принципе (если исключить баги компилятора). В худшем случае вред от оптимизации может выражаться лишь в ухудшении перформанса, вместо улучшения.
_>И к чему ты написал эти очевидные истины? )))
Так чего же ты тогда называешь вредом от оптимизации?

_>>>·>А, понял что ты имеешь в виду. Да, согласен. Другой вопрос, насколько это реально иметь не более одного обращения к памяти в сколько-нибудь нетривиальной программе.

_>>>Эммм, ну я надеюсь ты конечно же в курсе про то, что кроме искусственных (вставляемых человеком) барьеров памяти, есть и так сказать природные? )))
_>·>"природные"? Вставляемые богом что-ли? Нет уж, есть лишь определённые в стандарте, в соответствующих конструкциях. Например, создание треда ставит барьер.
_>Не вставляемые, а подразумеваемые компилятором (потому как они только для него, а не для железа). И похоже что ты не в курсе всего этого. ))) Даже про банальный вызов функции.... )))
Вызов функции это про sequence points, что никакого отношения к многопоточке не имеет. Или я не понял что ты имеешь в виду.

_>·>Это вроде эффект наличия кешей. Другое ядро увидит значение только после того как данное ядро выгрузит свой кеш в оперативку. Это происходит не при каждой модификации адреса и ещё веселее когда есть несколько пишущих ядер.

_>·>

A multi-processor system consists of four processors — P1, P2, P3 and P4, all containing cached copies of a shared variable S whose initial value is 0. Processor P1 changes the value of S (in its cached copy) to 10 following which processor P2 changes the value of S in its own cached copy to 20. If we ensure only write propagation, then P3 and P4 will certainly see the changes made to S by P1 and P2. However, P3 may see the change made by P1 after seeing the change made by P2 and hence return 10 on a read to S. P4 on the other hand may see changes made by P1 and P2 in the order in which they are made and hence return 20 on a read to S. The processors P3 and P4 now have an incoherent view of the memory.

_>Причём тут вообще кэши. У тебя было однозначное заявление, что некие процессоры в целях оптимизации не только меняют порядок выполнения инструкций, но и сливают/выкидывают некоторые из них. Так вот хотелось услышать что это за такие процессоры, поимённо.
Быстрое гугленье показало мне экзотику Intel SCC и Adapteva Epiphany. В отличие от x86 с cache coherency изкаропки которую ты считаешь абсолютным законом, в этих системах когерентность надо обеспечивать явно на софтварном уровне. Как я понимаю, когда ядер тысячи — по другому просто нельзя.

Далее, в x86 есть movnt-инструкции, которые тоже обладают weak consistency. И я не вижу никаких причин не использовать запись в volatile переменную такой инструкцией. Тогда как atomic+relaxed накладывает такое ограничение.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[64]: Безопасность Rust
От: vdimas Россия  
Дата: 11.06.19 23:49
Оценка:
Здравствуйте, Ikemefula, Вы писали:

V>>Ты процитировал не меня.

V>>Процитируй мои точные фразы.
I>Тебя можно долго цитировать

Пока что ты долго цитировал не меня.


I>Сначала ты утверждаешь, что volatile это для многопоточности.


Было сказано не так.
Причём, сильно не так.
Ты зачем вообще влез, если даже своими словами повторить не в состоянии?


I>Далее, ты сам же утверждаешь, что для взрослых кейсов нужен fence, т.к. volatile не хватит.


Юморист.
"Взрослый кейз" — это порядок обновления более одной переменной.


I>В целом, получается так volatile может ничего и не дать для меж-поточных флагов


Даже мьютекс может ничего не дать, прикинь.


I>Странная фича — вроде бы про многопоточность, а никаких гарантий не даёт.


Как и в любой инженерии, всё зависит от содержимого головы программиста.
Re[47]: Безопасность Rust
От: alex_public  
Дата: 12.06.19 00:32
Оценка:
Здравствуйте, ·, Вы писали:

_>>Это слишком большой и сложный вопрос, чтобы формулировать его в таких детских терминах как больше или меньше гарантий. Именно поэтому тут было такое большое обсуждение. Так уж и быть, попробую в последний раз сформулировать это в одном месте. Для начала надо поделить эту тему на два отдельных вопроса:

_>>1. Управление памятью. Здесь и C++ и Rust полностью решают проблему со всеми гарантиями, причём практически одинаковыми способами. Но есть небольшие отличия в нюансах. Самая идея решения заключается в том, что мы локализуем (это возможно только если в языке реализованы механизмы RAII и передачи владения) весь "опасный" код непосредственно управления памятью в нескольких специальных типах-контейнерах. Соответственно если в оставшейся части программы не будет использоваться "опасный код" напрямую, а только через эти контейнеры, то мы получим все нужные гарантии.
·>Это опять некое манипулирование фактами. Ты забыл важный аспект, borrow checker, который и обеспечивает довольно серьёзные гарантии. Rust предоставляет способ композиции безопасного кода в безопасный код. В C++ можно скомбинировать безопасные конструкции и получить хрен знает что и фиг поймёшь чего.

Ну раз ты так утверждаешь, то тебе конечно же будет не сложно привести конкретный пример такой комбинации? )

_>>А нюанс заключается в том, что в Rust этот самый опасный код описывается достаточно просто "код с блоками unsafe", в то время как в C++ это формулируется несколько более длинно.

·>Опять привираешь. Никак оно не формулируется. Или формулировка слишком длинна, чтобы её можно было поместить на полях этого форума?

Вообще то большинство необходимых элементов для данной формулировки уже озвучивались в данной темке.

_>>Это как бы маленький плюс Rust'а — неявное принуждение компилятором к написанию безопасного кода за пределами специальных типов. Но тут же есть и минус: при такой модели существует целый класс абсолютно безопасных кусков кода, которые при этом компилятор Rust не пропустит без unsafe. И соответственно при требование минимизировать количество unsafe это будет приводит к переписыванию части кода существенно более сложным образом (на самом деле там всё равно добавляется unsafe, но спрятанный в библиотечном коде и используемый например как лишний контейнер). Казалось бы наблюдается равенство (мелкий плюс на мелкий минус), однако если вспомнить про то, что

·>Это ты тоже немного загнался. Ты предложил переписать какой-то сишный код, в котором ты никак не сформулировал решаемую задачу. Притом очевидно, что этот код сам по себе не имеет смысла. Я тебе это продемонстрировал попросив переписать этот код на модель акторов.

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

_>>в Rust почему-то отсутствую гарантии (в том числе и в чистом safe коде) на работу RAII, то оказывается что C++ тут имеет даже больше гарантий.

·>Это чистое враньё. Rust даёт вполне определённые гарантии, а C++ — никаких.

Вот тебе простейший код на Rust без единого unsafe:
fn main()
{
    for i in 0..10 {
        let mut vec = vec![i; 250*1024*1024];
        println!("Box #{}", vec[0]);
        mem::forget(vec);
        thread::sleep(time::Duration::from_secs(1));
    }
}

Этот код вызывает утечку памяти (вследствие нарушения RAII), по гигабайту в секунду.

Его точный аналог (кроме одной строчки) на C++ выглядит так:
int main()
{
    for(int i=0; i<10; i++){
        auto vec=vector(250*1024*1024, i);
        cout<<"Box #"<<vec[0]<<endl;
        //???
        this_thread::sleep_for(1s);
    }
}

и при этом естественно не вызывает никаких утечек памяти. Так что, ты можешь указать какую строчку надо поставить на место знаков вопросов, чтобы в C++ появилась такая же утечка как в Rust'е?

·>В остальном всё то же самое — циклическая зависимость каких-нибудь shared_ptr в плюсах тоже нарушает все "гарантии".


Циклические зависимости в shared_ptr решаются в C++ с помощью https://en.cppreference.com/w/cpp/memory/weak_ptr. В точности так же, как и в Rust'е https://doc.rust-lang.org/std/sync/struct.Weak.html.

_>>2. Управление параллелизмом. В C++ в рамках стандарта языка имеется бедненький набор самых базовых примитивов и никаких разговоров о гарантиях. Плюс к этому имеется огромное числе сторонних библиотек, решающих все возможные варианты данной проблемы, в том числе и с широчайшими гарантиями. В Rust'е ситуация практически аналогичная, разве что библиотеки послабее в силу меньшего времени развития. Плюс к этому в Rust'е сделана попытка в рамках языка добавить гарантии решения одной мелкой проблемы параллелизма (причём только в

·>Если для тебя undefined behaviour не является большой проблемой, то наверно да, можно так заявить.

Проблема является небольшой и простой по решению, в сравнение с остальными проблемами параллелизма.

_>>определённых условиях) приблизительно по той же схеме, что и с памятью: объявляем все межпоточные (но и только!) взаимодействия как unsafe и соответственно заставляем локализовать их в специальных типах, через которых происходит всё взаимодействие. Разница в ситуации с памятью в том, что таким образом решается только маленькая часть проблем параллелизма. В то время как некоторые сторонние библиотеки (и в C++ и в Rust), основанные на других подходах к проблеме параллелизма, предлагают решения с гораздо большими гарантиями. Ну а в тех редких случаях, когда эти библиотеки не подходят, обычно подходит только "матёрый unsafe" (что в C++, что в Rust). В итоге пользы от такой попытки особой нет (проще сразу взять сильное решение с настоящими гарантиями), а вот вред (в точности такой же, как и в ситуации с памятью) никуда не девается.

·>Это какие же _гарантии_?

Эээ что? )

_>>Ну т.е. получается что возможны атомарные типы не из того списка, да? )))

·>Выделил. _Стандартные_ типы — невозможны. Т.е. sig_atomic — стандартный тип, не гарантирующий atomic в многопоточном коде.

Можно процитировать конкретную строчку из стандарта языка, в которой сказано что операции чтения/записи с sig_atomic не являются атомарными в многопоточном коде?

_>>Так о том и речь. Хотя конечно же это будет не lock-free атомарный тип, но это не суть.

·>Только зачем об этом вообще вести речь — непонятно. Я веду речь о немного другом.

Чтобы до тебя дошло, что существуют атомарные типы помимо std::atomic. )))

_>>·>Нет. Принципиальная разница между sig_atomic и atomic+relaxed в том, что поведение первого типа стандарт не определяет, а второго — определяет.

_>>Вообще то поведение sig_atomic как раз определено в стандарте — это "целочисленый тип, обращения к которому атомарны".
·>Опять привираешь. Ты неполностью цитируешь, обрезая важный хвост "... при обработке асинхронных сигналов".

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

_>>Возможно ты где-то там читал, что этого недостаточно (и мы даже знаем, чего не хватает) для спокойного использования в условиях многопоточности. Но это речь про произвольный окружающий код, а не про конкретный частный случай. Точно так же как и relaxed std::atomic<int> вообще говоря нельзя применять в случае произвольного многопоточного кода. Однако почему-то никто не пишет, что relaxed std::atomic<int> не подходит для многопоточности.

·>Можно применять, конечно. И он будет рабоать в точности с гарантиями relaxed ordering на любой платформе, в любых условиях — реальных или гипотетических. Это позволит, скажем, доказывать теоремы математическими методами для данного куска кода и делать выводы о том как код может себя вести. Использование sig_atomic в многопоточном коде не позволяет делать никакие рассуждения (reasoning) о поведении такого кода в общем случае.

Ну тебе, с твоей квалификацией, в особенности в области компьютерного железа (смотрю на твоё соседнее сообщение про оптимизации процессора), действительно не позволяет. А вот многие другие без проблем могут в деталях описать происходящее.
Re[48]: Безопасность Rust
От: · Великобритания  
Дата: 12.06.19 10:28
Оценка:
Здравствуйте, alex_public, Вы писали:

_>·>Это опять некое манипулирование фактами. Ты забыл важный аспект, borrow checker, который и обеспечивает довольно серьёзные гарантии. Rust предоставляет способ композиции безопасного кода в безопасный код. В C++ можно скомбинировать безопасные конструкции и получить хрен знает что и фиг поймёшь чего.

_>Ну раз ты так утверждаешь, то тебе конечно же будет не сложно привести конкретный пример такой комбинации? )
Ты прикалываешься что-ли? Как минимум я тебе в этом топике уже указывал на уплывание &-ссылок в массив лямбд. Ещё упоминали инвалидацию итераторов. Не говоря уж о тривиальном delete x; delete x;.

_>>>А нюанс заключается в том, что в Rust этот самый опасный код описывается достаточно просто "код с блоками unsafe", в то время как в C++ это формулируется несколько более длинно.

_>·>Опять привираешь. Никак оно не формулируется. Или формулировка слишком длинна, чтобы её можно было поместить на полях этого форума?
_>Вообще то большинство необходимых элементов для данной формулировки уже озвучивались в данной темке.
Ну так и скажи, что не формулируется (балабольство не считается), зачем завираться-то?

_>·>Это ты тоже немного загнался. Ты предложил переписать какой-то сишный код, в котором ты никак не сформулировал решаемую задачу. Притом очевидно, что этот код сам по себе не имеет смысла. Я тебе это продемонстрировал попросив переписать этот код на модель акторов.

_>А ты забавный. ))) Люди, которые ежедневно профессионально пишут на Rust сами высказывали данной темке утверждение о том, что существует множество абсолютно корректных приложений, которые непропускает компилятор Rust (естественно без unsafe). Но при этом ты, который насколько я помню, специализируется на совсем других инструментах, пытаешься опровергать это утверждение.
Писали они несколько другое.

_>и при этом естественно не вызывает никаких утечек памяти. Так что, ты можешь указать какую строчку надо поставить на место знаков вопросов, чтобы в C++ появилась такая же утечка как в Rust'е?

Мде. Ты решил выиграть у себя конкурс на самый идиотский аргумент в споре?
Если ты и в правду такой, открой для себя, например, unique_ptr.release, практически аналог forget.

_>·>В остальном всё то же самое — циклическая зависимость каких-нибудь shared_ptr в плюсах тоже нарушает все "гарантии".

_>Циклические зависимости в shared_ptr решаются в C++ с помощью https://en.cppreference.com/w/cpp/memory/weak_ptr. В точности так же, как и в Rust'е https://doc.rust-lang.org/std/sync/struct.Weak.html.
Ты издеваешься что-ли? Я знаю как они решаются. Но ты тут заикнулся о гарантиях, а теперь внезапно "решается". Так какие _гарантии_ даёт c++ от утечек?

_>>>2. Управление параллелизмом. В C++ в рамках стандарта языка имеется бедненький набор самых базовых примитивов и никаких разговоров о гарантиях. Плюс к этому имеется огромное числе сторонних библиотек, решающих все возможные варианты данной проблемы, в том числе и с широчайшими гарантиями. В Rust'е ситуация практически аналогичная, разве что библиотеки послабее в силу меньшего времени развития. Плюс к этому в Rust'е сделана попытка в рамках языка добавить гарантии решения одной мелкой проблемы параллелизма (причём только в

_>·>Если для тебя undefined behaviour не является большой проблемой, то наверно да, можно так заявить.
_>Проблема является небольшой и простой по решению, в сравнение с остальными проблемами параллелизма.
Это объективно не так. Хотя тебе может так не казаться.

_>>>определённых условиях) приблизительно по той же схеме, что и с памятью: объявляем все межпоточные (но и только!) взаимодействия как unsafe и соответственно заставляем локализовать их в специальных типах, через которых происходит всё взаимодействие. Разница в ситуации с памятью в том, что таким образом решается только маленькая часть проблем параллелизма. В то время как некоторые сторонние библиотеки (и в C++ и в Rust), основанные на других подходах к проблеме параллелизма, предлагают решения с гораздо большими гарантиями. Ну а в тех редких случаях, когда эти библиотеки не подходят, обычно подходит только "матёрый unsafe" (что в C++, что в Rust). В итоге пользы от такой попытки особой нет (проще сразу взять сильное решение с настоящими гарантиями), а вот вред (в точности такой же, как и в ситуации с памятью) никуда не девается.

_>·>Это какие же _гарантии_?
_>Эээ что? )
Ты обещал "сильное решение с настоящими гарантиями" — так какие же конкретно гарантии чего?

_>>>Ну т.е. получается что возможны атомарные типы не из того списка, да? )))

_>·>Выделил. _Стандартные_ типы — невозможны. Т.е. sig_atomic — стандартный тип, не гарантирующий atomic в многопоточном коде.
_>Можно процитировать конкретную строчку из стандарта языка, в которой сказано что операции чтения/записи с sig_atomic не являются атомарными в многопоточном коде?
А можно процитировать конкретную строчку из стандарта языка, в которой сказано что операции чтения/записи с std::string не являются атомарными в многопоточном коде?

_>>>Так о том и речь. Хотя конечно же это будет не lock-free атомарный тип, но это не суть.

_>·>Только зачем об этом вообще вести речь — непонятно. Я веду речь о немного другом.
_>Чтобы до тебя дошло, что существуют атомарные типы помимо std::atomic. )))
Straw man fallacy.

_>>>·>Нет. Принципиальная разница между sig_atomic и atomic+relaxed в том, что поведение первого типа стандарт не определяет, а второго — определяет.

_>>>Вообще то поведение sig_atomic как раз определено в стандарте — это "целочисленый тип, обращения к которому атомарны".
_>·>Опять привираешь. Ты неполностью цитируешь, обрезая важный хвост "... при обработке асинхронных сигналов".
_>Ну тогда уж надо совсем до конца цитировать. Там сказано "целочисленый тип, обращения к которому атомарны, даже при обработке асинхронных сигналов". Надо пояснять разницу? )))
Надо, и с учётом того, что этот тип появился фактически ещё до того как треды были придуманы в принципе.

_>·>Можно применять, конечно. И он будет рабоать в точности с гарантиями relaxed ordering на любой платформе, в любых условиях — реальных или гипотетических. Это позволит, скажем, доказывать теоремы математическими методами для данного куска кода и делать выводы о том как код может себя вести. Использование sig_atomic в многопоточном коде не позволяет делать никакие рассуждения (reasoning) о поведении такого кода в общем случае.

_>Ну тебе, с твоей квалификацией, в особенности в области компьютерного железа (смотрю на твоё соседнее сообщение про оптимизации процессора), действительно не позволяет. А вот многие другие без проблем могут в деталях описать происходящее.
Язык не предъявляет такие требования к железу. Так что это вообще не в тему.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[49]: Безопасность Rust
От: alex_public  
Дата: 12.06.19 10:45
Оценка:
Здравствуйте, ·, Вы писали:

_>>С учётом того, что эти "некоторые случаи"=="все известные платформы" (в том числе и по твоему признанию), то это конечно же весьма забавное утверждение. )))

·>Неужели ты предпочитаешь основывать рассуждения не о том, что прямо говорится в стандартне, а на том что я что-то не знаю?

Ладно бы только ты не знал. Так ведь и я тоже не знаю, а это совсем другое дело! )))

Ну а если серьёзно, то ПО написанное на C++ всегда компилируется под конкретную архитектуру, так что подобные проблемы никогда не стоят. Но если вдруг у тебя будет приступ паранойи и захочется 100% гарантий на любых платформах, то ты можешь элементарно добиться этого так:
#if __INT_WIDTH__ != __SIG_ATOMIC_WIDTH__
#error Bad platform
#endif

Только вот срабатываний не дождёшься... )))

_>>Ну так глобальная она только ради простоты форумного примера. Понятно что в серьёзных проектах прямо такой код никогда не пишется (собственно там вообще глобальные переменные — это зло).

·>И что? Без borrow checker ты не сможешь гарантировать всё равно.

Ты бредишь? ))) Вот такой простейший код:
int process(int p) {...}

int main()
{
    int v=1;
    v=process(v);
}

И ты хочешь сказать, что внутри process можно написать такой код, что он сможет каким-то образом обращаться непосредственно к переменной v? )

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

_>>И к чему ты написал эти очевидные истины? )))
·>Так чего же ты тогда называешь вредом от оптимизации?

Вред от оптимизации возможен в некорректном коде — в котором есть потребность в модификаторе volatile, но при этом его почему-то не поставили. Однако ни в одном из обсуждаем в данной темке примеров такого кода не было.

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

·>Вызов функции это про sequence points, что никакого отношения к многопоточке не имеет. Или я не понял что ты имеешь в виду.

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

_>>Причём тут вообще кэши. У тебя было однозначное заявление, что некие процессоры в целях оптимизации не только меняют порядок выполнения инструкций, но и сливают/выкидывают некоторые из них. Так вот хотелось услышать что это за такие процессоры, поимённо.

·>Быстрое гугленье показало мне экзотику Intel SCC и Adapteva Epiphany. В отличие от x86 с cache coherency изкаропки которую ты считаешь абсолютным законом, в этих системах когерентность надо обеспечивать явно на софтварном уровне. Как я понимаю, когда ядер тысячи — по другому просто нельзя.
·>Далее, в x86 есть movnt-инструкции, которые тоже обладают weak consistency. И я не вижу никаких причин не использовать запись в volatile переменную такой инструкцией. Тогда как atomic+relaxed накладывает такое ограничение.

Повторяю ещё раз, вроде как русским языком: причём тут вообще тема кэшей (которую ты зачем-то попытался приплести сюда) и т.п.? У тебя было конкретное утверждение о работе оптимизатора процессора. Что он может не только менять очерёдность исполнения инструкций, но и выкидывать некоторые из них (как это умеет делать компилятор, если ему не запретить через volatile). И мы все ждём подтверждения этого интересного утверждения. И кэши к подтверждению данного утверждения вообще никак не относятся.
Re[50]: Безопасность Rust
От: · Великобритания  
Дата: 12.06.19 12:19
Оценка:
Здравствуйте, alex_public, Вы писали:

_>>>С учётом того, что эти "некоторые случаи"=="все известные платформы" (в том числе и по твоему признанию), то это конечно же весьма забавное утверждение. )))

_>·>Неужели ты предпочитаешь основывать рассуждения не о том, что прямо говорится в стандартне, а на том что я что-то не знаю?
_>Ладно бы только ты не знал. Так ведь и я тоже не знаю, а это совсем другое дело! )))
_>Ну а если серьёзно, то ПО написанное на C++ всегда компилируется под конкретную архитектуру, так что подобные проблемы никогда не стоят.
Господин никогда не писал переиспользуемый портируемый код?

_>Но если вдруг у тебя будет приступ паранойи и захочется 100% гарантий на любых платформах, то ты можешь элементарно добиться этого так:

_>
_>#if __INT_WIDTH__ != __SIG_ATOMIC_WIDTH__
_>#error Bad platform
_>#endif
_>

_>Только вот срабатываний не дождёшься... )))
Неверно. Должно быть #error Programmer is a lazy moron, потому что с появлением std::atomic<int> это стало ненужным.

_>>>Ну так глобальная она только ради простоты форумного примера. Понятно что в серьёзных проектах прямо такой код никогда не пишется (собственно там вообще глобальные переменные — это зло).

_>·>И что? Без borrow checker ты не сможешь гарантировать всё равно.
_>Ты бредишь? ))) Вот такой простейший код:
Ты меня решил взять на измор идиотскими примерами кода?

_>И ты хочешь сказать, что внутри process можно написать такой код, что он сможет каким-то образом обращаться непосредственно к переменной v? )

Даже в таком тривиальном коде достаточно написать int process(int &p) и готово. А в реальном коде будет ещё 100500 способов нарваться на подобное.

_>>>И к чему ты написал эти очевидные истины? )))

_>·>Так чего же ты тогда называешь вредом от оптимизации?
_>Вред от оптимизации возможен в некорректном коде — в котором есть потребность в модификаторе volatile, но при этом его почему-то не поставили. Однако ни в одном из обсуждаем в данной темке примеров такого кода не было.
Это не вред от оптимизации. Это вред от некорректного кода.

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

_>·>Вызов функции это про sequence points, что никакого отношения к многопоточке не имеет. Или я не понял что ты имеешь в виду.
_>А причём тут многопоточность или ещё что-то? Нам важно чтобы не были переставлены местам обращения к памяти и всё. Внутри тела одной функции ты можешь это отследить сам, а внутрь других функций (вызываемых из твоей) тебе лазить не надо, т.к. за этим следит компилятор.
И как seq-points от этого помогут? Оптимизатор, особенно в WPO, может такое наворотить.

_>·>Быстрое гугленье показало мне экзотику Intel SCC и Adapteva Epiphany. В отличие от x86 с cache coherency изкаропки которую ты считаешь абсолютным законом, в этих системах когерентность надо обеспечивать явно на софтварном уровне. Как я понимаю, когда ядер тысячи — по другому просто нельзя.

_>·>Далее, в x86 есть movnt-инструкции, которые тоже обладают weak consistency. И я не вижу никаких причин не использовать запись в volatile переменную такой инструкцией. Тогда как atomic+relaxed накладывает такое ограничение.
_>Повторяю ещё раз, вроде как русским языком: причём тут вообще тема кэшей (которую ты зачем-то попытался приплести сюда) и т.п.? У тебя было конкретное утверждение о работе оптимизатора процессора. Что он может не только менять очерёдность исполнения инструкций, но и выкидывать некоторые из них (как это умеет делать компилятор, если ему не запретить через volatile). И мы все ждём подтверждения этого интересного утверждения. И кэши к подтверждению данного утверждения вообще никак не относятся.
Возможно ты "выкидывать инструкции" понял слишком буквально. Очевидно, CPU не может "выкидывать", т.к. не умеет править загруженные exe-шники. Он просто может не модифицировать память так, что другие ядра увидят модификации. Что по сути означает, что инструкция "ничего не делает".
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.