Здравствуйте, _NN_, Вы писали:
Ф>>>volatile запрещает оптимизации компилятора в отношении поля. В ином случае Disposed может вернуть false там, где должен быть true, потому что false может "застрять" в регистре, куда ранее была зачитана ячейка памяти. MH>>
MH>>когда будет вызван гетер для Disposed — вариантов кроме как прочитать _disposed из памяти нет. не будет он из регистра читаться. _NN>А как же инлайнинг и оптимизация? _NN>Разве есть гарантия, что компилятор и джит обязаны в случае свойства всегда читать из памяти?
Здравствуйте, MadHuman, Вы писали:
Ф>>а завтра может быть пара из mov и cmp. Например потому, что это другой процессор, и там джиттер работает немного иначе. MH>mov — всё равно из памяти будет читать MH>то есть другого варианта нет MH>дело не в том как джиттер работает. дело в том что при передаче управления в метод-геттера в теле метода надо читать филд, из памяти.
mov будет читать из памяти — других вариантов у него нет, но из ряда
mov -> cmp
mov -> cmp
mov -> cmp
можно исключить лишние мувы. Сам метод может быть заинлайнет, и тогда всё будет происходить не в get_IsDisposed, а в самом пользовательском методе, где как раз может быть поле для таких оптимизаций.
MH>
MH>internal bool Disposed => _disposed != 0;
MH>
MH>этот метод не жирный. здесь единственный случай чтения поля, и его можно только прочитать из памяти. не тот случай когда нужен volatile.
Проблема не в этом методе, а в том, что для этого поля оптимизации никто не запрещал.
MH>более того — если посмотреть в целом на ситуацию, то volatile не нужен. т.к. даже если мы гарантированно прочитали память, то в следующий момент ( может даже ещё до выхода из геттера) _disposed может оказаться выставленным, MH>и вызывающая сторона ошибочно посчитает что ещё не диспозед, хотя уже диспозед. то есть volatile никак не предотвратит на 100% вызывающую Disposed сторону от ошибочного действия.
Угу, а потом ты будешь удивляться, что по логам пы коммит для транзакции послали, а транзакция куда-то пропала, хотя на самом деле никуда мы ничего не послали, а в этот момент сокет кто-то задиспозил.
Понимаешь, вовремя кинутое исключение ObjectDisposedException может тебе сократить неделю отладки.
Всё сказанное выше — личное мнение, если не указано обратное.
Здравствуйте, MadHuman, Вы писали:
MH>этот метод не жирный. здесь единственный случай чтения поля, и его можно только прочитать из памяти. не тот случай когда нужен volatile.
MH>>более того — если посмотреть в целом на ситуацию, то volatile не нужен. т.к. даже если мы гарантированно прочитали память, то в следующий момент ( может даже ещё до выхода из геттера) _disposed может оказаться выставленным, MH>>и вызывающая сторона ошибочно посчитает что ещё не диспозед, хотя уже диспозед. то есть volatile никак не предотвратит на 100% вызывающую Disposed сторону от ошибочного действия.
Ф>Угу, а потом ты будешь удивляться, что по логам пы коммит для транзакции послали, а транзакция куда-то пропала, хотя на самом деле никуда мы ничего не послали, а в этот момент сокет кто-то задиспозил. Ф>Понимаешь, вовремя кинутое исключение ObjectDisposedException может тебе сократить неделю отладки.
согласен. но в том-то и дело, что в данном примере топик-стартера нет 100% гарантии, что после того как из Disposed получили false (даже не заинлайнёнными и/или с volatile для _disposed)
сокет всё ещё не задиспозен, и описанная вами ситуация всё равно может повториться.
то есть volatile возможно может уменьшить вероятность такой ситуации (и ещё вопрос насколько), но никак не исключит её.
Здравствуйте, Sharov, Вы писали:
S>Здравствуйте, MadHuman, Вы писали:
MH>>этот метод не жирный. здесь единственный случай чтения поля, и его можно только прочитать из памяти. не тот случай когда нужен volatile.
S>А почему только из памяти, какие гарантии?
а откуда ещё (при условии что метод не заинлайнен)? есть какие-то предположения откуда, кроме как из памяти?
Здравствуйте, vlp, Вы писали:
vlp>Здравствуйте, Sharov, Вы писали:
S>>Здравствуйте, VladCore, Вы писали:
VC>>>Прикольная задачка. Дело думаю в том что Disposed может переключится из false в true а обратно — нет.
S>>Ничего прикольного, и даже баг по-хорошему. Поскольку вызывать interlocked.read дороговато для когерентности кэшей, а это бы вызывалось для каждого вызова(проверка на dispose), то решили это убрать, один хрен будет исключение(скорее всего ObjectDisposedException). Но почему volatile не добавить :xz . vlp>а чем принципиально здесь поможет volatile?
Судя по сообщением выше, действительно ничем. А вообще,если к переменной возможен доступ из нескольких потоков, то
сама же ms рекомендует этой переменной добавлять модификатор volatile.
Здравствуйте, Философ, Вы писали:
Ф>Такие гарантии может дать только volatile, т.е. запрет оптимизаций. В ином случае только на везение можно рассчитывать. В большинстве случае везёт. Учитывай, что геттер Disposed вполне может быть заинлайнен.
Гарантий, которые дает volatile недостаточно, т.к. между инструкциями cmp [ecx + offset], 1 и последующей за ней je/jne запросто может случиться context switch.
И пока этот поток спит, параллельный поток устанавливает _isdisposed=1. После чего первый поток просыпается и выбирает не ту ветку, т.к. в регистре флагов сохранено устаревшее значение.
Здравствуйте, vlp, Вы писали:
vlp>Здравствуйте, _NN_, Вы писали: vlp> ? _NN>>Не нужен ли где-нибудь volatile, Volatile.Read или Interlocked.Read ?
vlp>Атомарная тут только запись, чтение теоретически может приводить к stale read и в этом случае не будет брошено исключение когда dispose вызван. Это в принципе нестрашно, т.к. в этом случае все равно код нормально работать уже не будет.
vlp>Interlocked.Read не сделан осознанно, чтобы не замедлять код. Dispose вызывается редко, в отличие от остальных операций на сокетах.
А нет ли здесь экономии на спичках? Т.е. мы не гоняем лишний раз когерентность кэшей для n потоков,
но при этом когда мы действительно освободим сокет, то n-1 поток обломятся на стадии системных вызовов ОС,
потеряв при этом не малое кол-во времени на обращении к той же памяти, при учете возможный прерываний отсутствующих
страниц VM и т.п. накладных расходов?
PS: Блин, ладно, удачно описался, и сам себе на вопрос и ответил. Оставлю как есть.
Действительно, проблемы тут нету, ибо Interlocked.CompareExchange как раз когеретность кэшей и вызывает, т.е.
все disposed поля у всех потоков будут одинаковы. Я ошибся в том, что Interlocked.Read также инициирует
когерентность кэшей, чего по идее может и не быть, это же чтение, а не запись. Т.е. Interlocked.CompareExchange
сделает disposed поле в кэше для всех одинаковым.
PPS: Блин, все-таки и вправду ошибка.А если disposed читается из регистра, а не из, хотя бы кэша? Т.е. вроде бы volatile
должно хватить, не говоря о Interlocked.Read...Так что, кмк, мой первоначальный вопрос в силе.
Здравствуйте, MadHuman, Вы писали:
MH>Здравствуйте, Sharov, Вы писали:
S>>Здравствуйте, MadHuman, Вы писали:
MH>>>этот метод не жирный. здесь единственный случай чтения поля, и его можно только прочитать из памяти. не тот случай когда нужен volatile.
S>>А почему только из памяти, какие гарантии? MH>а откуда ещё (при условии что метод не заинлайнен)? есть какие-то предположения откуда, кроме как из памяти?
А если метод заинлайнен, то регистр, верно? А если нет, то регистр исключен? Я, честно, сам не до конца понимаю.\
Т.е.если инлайна нет, то почему не возможно чтение из регистра?
Здравствуйте, Sharov, Вы писали:
S>>>А почему только из памяти, какие гарантии? MH>>а откуда ещё (при условии что метод не заинлайнен)? есть какие-то предположения откуда, кроме как из памяти?
S>А если метод заинлайнен, то регистр, верно? А если нет, то регистр исключен? Я, честно, сам не до конца понимаю.\ S>Т.е.если инлайна нет, то почему не возможно чтение из регистра?
Потому что функция генериутеся так, чтобы не делать никаких предположений о состоянии регистров. В случае экземплярного метода первый неявный параметр — this. Он передаётся в регистре rcx или ecx (x64 и x86 соответственно). Обращение к полям внутри метода — [rcx+FieldOffset], например:
cmp byte ptr [rcx+8],0
Если функция заинлайнена, то в коде могут быть предположения о том, что находится в регистрах.
Всё сказанное выше — личное мнение, если не указано обратное.
Здравствуйте, Sharov, Вы писали:
S>Здравствуйте, MadHuman, Вы писали:
MH>>Здравствуйте, Sharov, Вы писали:
S>>>Здравствуйте, MadHuman, Вы писали:
MH>>>>этот метод не жирный. здесь единственный случай чтения поля, и его можно только прочитать из памяти. не тот случай когда нужен volatile.
S>>>А почему только из памяти, какие гарантии? MH>>а откуда ещё (при условии что метод не заинлайнен)? есть какие-то предположения откуда, кроме как из памяти?
S>А если метод заинлайнен, то регистр, верно?
да, если метод заинлайнен и ранее в нём было чтение этого филда, то вот в этом случае возможна оптимизация, что прочитанное его значение
запомнится в переменной-регистре и последующие чтения будут оттуда.
но в отдельно взятом методе, 1-е чтение филда всегда будет из памяти. потому что других вариантов нет)
Здравствуйте, karbofos42, Вы писали:
K>ну, и не делали бы вовсе тогда проверку
проверка работает хорошо за исключением весьма и весьма редких случаев. На практике эти случаи возникают очень редко. Попробуйте на практике добиться того, чем тут всех пугают в этом треде — я уверен, что у вас не получится, а ObjectDisposedException вы будете получать стабильно. Профит для разработчика очевиден.
Здравствуйте, Философ, Вы писали:
Ф>Здравствуйте, vlp, Вы писали:
Ф>>>volatile это инструкция компилятору "не оптимизировать обращение к полю". Из-за таких оптимизаций чтение пожет быть "оптимизировано".
vlp>>Как именно нужно написать код, чтобы без volatile значение поля disposed было stale из-за "оптимизаций"? Этого и без volatile не происходит. Чтобы происходило нужна не одна проверка в начале функции (она точно из памяти читает), а хотя бы цикл с проверкой. Очень хотелось бы увидеть контрпример в виде кода и результаты дизассемблера.
Ф>Примеры дизассемблера тебе ничего не дадут: сегодня это один пример, а завтра версия джиттера поменяется.
Я просил пример того, как прямо сейчас, с текущей версией компилятора и джиттера, возможна ситуация, которой тут всех пугают и как volatile в ней помогает.
Тут, судя по ответам, в треде одни философы-теоретики, примеров нет.
>Для того, чтобы зачитанное значение могло застрять в регистре, достаточно несколько подряд вызовов, дёргающих ThrowIfDisposed(), например:
Ф>Connect() Ф>Read() Ф>Send()
Покажите скомпилированный код + дизассемблер кода, который в этом случае будет зачем-то записывать значение disposed в регистр и его потом переиспользовать. На практике и без volatile этого не делается, вместо этого значение всегда читается из памяти (почему-объяснять относительно долго)/
Ф>Такие вещи стреляют редко, но если стреляют, то хоть вешайся.
Чтобы это выстрелило, надо использовать disposed сокет. А race condition дебагать действительно тяжело, да.
Здравствуйте, Sharov, Вы писали:
S>Судя по сообщением выше, действительно ничем. А вообще,если к переменной возможен доступ из нескольких потоков, то S>сама же ms рекомендует этой переменной добавлять модификатор volatile.
Ну так не надо слепо следовать всем рекомендациям, не понимая их сути. В ряде контекстов эта рекомендация не имеет смысла, а иногда даже вредит.
Здравствуйте, vlp, Вы писали:
Ф>>Примеры дизассемблера тебе ничего не дадут: сегодня это один пример, а завтра версия джиттера поменяется. vlp>Я просил пример того, как прямо сейчас, с текущей версией компилятора и джиттера, возможна ситуация, которой тут всех пугают и как volatile в ней помогает. vlp>Тут, судя по ответам, в треде одни философы-теоретики, примеров нет.
Зачем тебе реальный пример? Тебя теоритическая возможность засесть на пару недель дебага не достаточно?
На x86 cейчас там обычный cmp с ячейкой, реально генерируемый код выглядит вот так:
00007ff9`6153096b 80790900 cmp byte ptr [rcx+9],0
00007ff9`6153096f 7549 jne 00007ff9`615309ba
Можешь теоретически объяснить, почему так? Почему не
mov rax, byte ptr [rcx+9]
cmp rax,0
jne SomeOffset
???
Полагаю, что потому что в системе команд присутствует cmp с ячейкой памяти? Ну ещё это может быть быстрее.
А если нет, если через регистр окажется быстрее!?
vlp>На практике и без volatile этого не делается, вместо этого значение всегда читается из памяти (почему-объяснять относительно долго)/
Даже на практике в диассемлерных листнингах встречается вот такое:
Здравствуйте, Философ, Вы писали:
Ф>Здравствуйте, vlp, Вы писали:
Ф>>>Примеры дизассемблера тебе ничего не дадут: сегодня это один пример, а завтра версия джиттера поменяется. vlp>>Я просил пример того, как прямо сейчас, с текущей версией компилятора и джиттера, возможна ситуация, которой тут всех пугают и как volatile в ней помогает. vlp>>Тут, судя по ответам, в треде одни философы-теоретики, примеров нет.
Ф>Зачем тебе реальный пример? Т
ну как зачем — чтобы спровоцировать кого-то подумать
>Тебя теоритическая возможность засесть на пару недель дебага не достаточно?
неа
Ф>Даже на практике в диассемлерных листнингах встречается вот такое: Ф>
Ф>Т.е. в этом случае действуют через промежуточный регистр.
в этом случае сначала из памяти читается адрес, а потом опять из памяти сравнивается значние по другому адресу. Это чтение поля класса, когда где-то есть указатель на инстанс (он грузится в ecx, а сам лежит в [rax+8]). Какое отношение это имеет к поведению volatile?
> Почему ты думаешь, что в случае ThrowIfDisposed() не может произойти тоже самое?
произойти что именно? Покажите пример того, что именно произойдет и от чего вас убережет volatile.
Здравствуйте, vlp, Вы писали:
S>>сама же ms рекомендует этой переменной добавлять модификатор volatile. vlp>Ну так не надо слепо следовать всем рекомендациям, не понимая их сути. В ряде контекстов эта рекомендация не имеет смысла, а иногда даже вредит.
Ну так я, например, реально натыкался на примеры того, что софтина зависает из-за кэширования значений в регистре. Т.е. там был выход из цикла по условию, а условие закэшировалось в регистре и реального чтения не происходило, т.е. вместо
осталось только две последних строчки, и чтения в регистр не происходило. Запрет на оптимизацию (volatile) решило эту проблему. И было реально тяжело догадаться, что там вот такая засада. У меня там пара седых волос прибавилась. С тех пор и пишу volatile в таких местах — следую рекомендации ms. А учитывая, что бывают вещи и похитрее, я стараюсь исключать вообще даже вероятность таких ошибок, иначе на всю жизнь останешься в дебаге...
Всё сказанное выше — личное мнение, если не указано обратное.
Ф>Ну так я, например, реально натыкался на примеры того, что софтина зависает из-за кэширования значений в регистре. Т.е. там был выход из цикла по условию, а условие закэшировалось в регистре и реального чтения не происходило, т.е. вместо
Ф>mov r6, [rcx + myVarOffset] Ф>cmp r7, [rcx + myVar2Offset] Ф>jne ..
Ф>осталось только две последних строчки, и чтения в регистр не происходило. Запрет на оптимизацию (volatile) решило эту проблему.
я знаю, что происходит в случае, когда есть цикл и сохранение значния в регистре. Я просил пример именно с кодом из socket.cs. Там проверка ровно один раз в начале каждого метода. Циклов нет.
Здравствуйте, vlp, Вы писали:
vlp>Ну так не надо слепо следовать всем рекомендациям, не понимая их сути. В ряде контекстов эта рекомендация не имеет смысла, а иногда даже вредит.
Я бы не сказал, что не понимаю сути-- запретить компилятору любые оптимизации с данным полем, чтобы где-то чего-то не переставить, закэшировать и тп. Просто этот конкретный случай хак ms -- не использовать volatile там, где в оф. документах и книжках использовать советуют.
Здравствуйте, _NN_, Вы писали:
_NN>Код Socket.cs имеет автомарную запись в Dispose: _NN> if (Interlocked.CompareExchange(ref _disposed, 1, 0) == 1)
Это не атомарная запись. А атомарный обмен. Бул всегда атомарно читается и пишется.
_NN>Гарантируется ли в C# работоспособность этого кода ? _NN>Не нужен ли где-нибудь volatile, Volatile.Read или Interlocked.Read ?
В общем, случае — нет. Но в CompareExchange происходит мемори борьер и если читать после него в том же потоке, то все будет ОК. Дучше добавить к _disposed volatile. Только это не про атомарность, а про сброс кэшей процессоров.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.