Здравствуйте, dsalodki, Вы писали:
D>Поясните пожалуйста самым простым способом, я гуглю, но что-то не понятно. Толи это позволяет отменить кеширование переменно, толи ещё что-то
Вот смотри, ты пишешь:
a = 5;
b = 10;
И рассчитываешь, что если в b уже 10, то в a точно 5.
Однако процессор может, по разным причинам, исполнить эти команды в другом порядке. В частности потому, что доступ в память, он не так уж и просто устроен, до одних мест может оказаться в данный момент быстрее дотянуться, до других — наоборот.
Причем в пределах простого линейного кода это все будет незаметно, процессор сам позаботится о том, чтобы все выстроить в нужном порядке, создав иллюзию, что с памятью все ОК. А вот если на другом ядре, в другом потоке исполняется код, который тоже пользуется этими переменнымы, то эти два ядра могут промеж собой и не договориться.
Вот тут-то и нужны явные барьеры памяти, чтобы сказать процессору, "доделай, пожалуйста, все операции с памятью, которые я заказывал до этого места, потому что дальше логика моей программы будет рассчитывать на то, что все отложенные/параллельные операции к этому месту уже доделаны"
Здравствуйте, dsalodki, Вы писали:
D>Поясните пожалуйста самым простым способом, я гуглю, но что-то не понятно. Толи это позволяет отменить кеширование переменно, толи ещё что-то
Это инструкция, запрещающая перенос инструкций через себя при переупорядочивании.
Здравствуйте, Sharov, Вы писали:
S>Здравствуйте, samius, Вы писали:
S>>Здравствуйте, dsalodki, Вы писали:
D>>>Поясните пожалуйста самым простым способом, я гуглю, но что-то не понятно. Толи это позволяет отменить кеширование переменно, толи ещё что-то S>>Это инструкция, запрещающая перенос инструкций через себя при переупорядочивании.
S>Это вроде volatile делает, а барьер на уровне железа просит закоммитить все возможные S>изменения в памяти и привести кэши в порядок. Просто инструкции может перенести компилятор и S>volatile ему это не даст.
volatile как модификатор переменной — запрет на кэширование. volatile как инструкция — запрет на чтение/запись из кэша в конкретном месте, т.е. немного не о том в общем случае, но в частном (C# и .NET) volatile фактически генерирует вокруг обращений еще и барьеры. В языках, где volatile не генерирует барьеры, проще представить одно без другого.
Барьер же — это идея, а не команда кэшам, которую они выполняют. Идея о том, что порядок операций должен быть вот такой, меняться он может "так" и "этак", но не "вот так", и эффекты от этих операций "ощутимы вот таким образом". А железо, в свою очередь (даже не само железо, а модель памяти, скорее), должно соответствовать и обеспечивать корректную работу этой идеи. Если для этого требуется сбросить кэши — будет исполнено. Если потребуется затормозить ядро — будет так.
Здравствуйте, Философ, Вы писали:
Ф>SpinLock не стоило относить к высокоуровневым.
Spin locks активно используются в ядрах ОС, в тех случаях, когда чисто технически невозможно выполнить ожидание с переключением контекста, а можно лишь поциклить какое-то разумное время (единицы-десятки микросекунд) в надежде, что параллельный, ядерный же, код, который не может быть прерван в любой момент на сколь угодно долгое время, быстро сделает свои дела и освободит блокировку. Если тщательно не следить за временем, проводимым в циклах, легко наплодить глюков — например, виндовые драйверы нередко проводят в ожидании сотни микросекунд, хотя положено не более 25.
Использовать такое на пользовательском уровне нужно с осторожностью, поскольку поток, вошедший в такой цикл, может быть прерван, и другой поток, пытающийся его дождаться, исчерпает свой квант, и тоже будет вытеснен. До какой-то частоты использования такие блокировки позволяют сэкономить на переключениях в ядро, но дальше они могут привести к резкому росту накладных расходов. Особенно в том случае, если код отлаживался на восьмиядерном процессоре, а затем запущен на двухъядерном, где вероятность прерывания потока примерно вчетверо выше.
Здравствуйте, dsalodki, Вы писали:
D>Поясните пожалуйста самым простым способом, я гуглю, но что-то не понятно. Толи это позволяет отменить кеширование переменно, толи ещё что-то
Суть решаемой проблемы:
У нас есть многопроцессорный компьютер с общей памятью. У каждого процессора имеются кеши. Когда несколько процессоров работают с одним адресом памяти, содержимое этой памяти находится в нескольких кешах у каждого процессора. Каждый процессор может обновлять эту память. При этом обновляется кеш у этого процессора и позже будет обновлена сама память. Также процессоры по определённому протоколу синхронизируют кеши между ядрами. Но вообще этот процесс небыстрый.
Иными словами когда одно ядро меняет содержимое оперативной памяти, другие ядра могут это обновление увидеть непонятно когда. Также, что ещё важней — когда меняются несколько разных участков в оперативной памяти, то в каком порядке эти изменения увидят другие ядра — тоже не очевидно.
Барьеры памяти это специальные инструкции для процессора, которые позволяют управлять этим процессом синхронизации. К примеру остановить работу процессора, пока не будут обновлены кеши у всех остальных процессоров, соответствующие данном адресу памяти.
В разных архитектурах разные гарантии, разные барьеры памяти, разные инструкции, поэтому более детально это можно обсуждать только в привязке к конкретной архитектуре.
Кроме того языки программирования обычно привязаны не к конкретной архитектуре, а к абстрактной модели памяти. А компилятор и рантайм уже обеспечивают, чтобы поведение реальной архитектуры соответствовало этой абстрактной модели памяти.
PS то, что я называю процессорами, в современном оборудовании чаще называется ядрами, но суть не меняется.
Здравствуйте, vsb, Вы писали:
vsb>Ядра не знают про кеши других ядер. Существует некий протокол, который синхронизирует кеши, но это происходит как бы асинхронно. Процессор не ждёт окончания этой синхронизации для следующих операций. Он записал в кеш значение и потом специальное оборудование, которое управляет этим кешом, начинает рассылку сообщений по межпроцессорной шине, что по такому-то адресу значение поменялось. Эти сообщения через некоторое количество тактов достигнут другого ядра, другое ядро проверит, есть ли в его кеше такой адрес, если есть, то обновит в нём значение и так далее. То бишь если ты меняешь значение в памяти на одном процессоре, то другой процессор когда-нибудь его увидит. Но когда и в каком порядке — тут гарантий никаких без дополнительных инструкций не будет.
Однако определённый эффект на соседние ядра это тоже оказывает, поэтому, например, нельзя "бесплатно" (с точки зрения производительности) делать активную запись двумя потоками в две соседних ячейки памяти, если они попадают в одну кэш-линию. По каждому сигналу инвалидации кэш-линии, контроллер памяти будет ставить другое ядро на холд при попытке обращения к соседней ячейке, пока не синхронизирует два экземпляра этой кеш-линии. Хотя казалось бы, зачем тут синхронизация исполнения — ведь каждый поток пишет только в свою ячейку и никак не работает с соседней (гуглить false sharing).
Здравствуйте, okman, Вы писали:
O>sfence/lfence/mfence — это малость для другого. В машинный код для x86/x64 практически всегда вставляется инструкция типа xchg, xadd или что-то подобное.
Это разные вещи.
Представь, два потока на разных ядрах CPU у тебя одновременно инкрементируют одну переменную. Пусть даже с помощью одной инструкции процессора, а не трех (прочитать, проинкрементировать, сохранить).
Величина, на которую увеличится переменная, будет меньше, чем суммарное количество операций по ее увеличению, сделанных обеими потоками. Вот смотри, что делает инструкция INC, если ее аргумент — адрес в памяти, с учетом кеширования:
1. Загрузить значение в ALU. Значение само по себе, скорее всего, в кеше
2. Прибавить 1
3. Сохранить значение в кеш.
Поскольку потоков два, а переменная одна, на 3-м шаге возникает синхронизация кешей. На 3-м, заметь, а не на 1-м. Поэтому запросто может быть, что на первом шаге оба потока прочтут в ALU одно и то же значение (кеш в этот момент синхронен и процессору не о чем беспокоиться), прибавят к нему 1 и сохранят на место. Т.е., в итоге, переменная увеличится на 1, хотя оба потока ее увеличивали.
Чтобы этого избежать, существует префикс LOCK. Он делает всю цепочку 1-2-3 атомарной в том смысле, что с того момента, пока один поток сделал шаг 1 и до того, как он сделал 3, второму потоку просто не дадут туда войти.
У некоторых команд, типа XCHG, префикс LOCK встроенный, некоторым другим приходится приделывать его явно.
Это совсем не то же самое, что reordering инструкций, от которого "защищают" барьеры памяти.
Здравствуйте, Философ, Вы писали:
Ф>Видел что? Во что превращается c++ intrinsic _mm_mfence?
Подчеркну: я описываю лишь то, что многократно видел сам за последние несколько лет. Без претензий на что-либо, просто наблюдения и выводы,
сделанные в контексте использования MS C++ под 32- и 64-битные версии Windows.
Если мы пишем 'MemoryBarrier' — компилятор впихнет туда xchg или какой-нибудь lock or.
Если используем Interlocked-функции или работаем через <atomic> из C++ (memory_order_seq_cst) — снова получаем на выходе xchg, cmpxchg, xadd и т.д.
Если заглянем в реализацию каких-нибудь примитивов синхронизации — там тоже много где используется "xchg и компания".
И т.д.
Почему там отсутствует (или исчезающе мало) mfence — я не берусь утверждать со 100% определенностью. Но подозреваю, что все очень прозаично и на
современных процессорах locked instructions вроде xchg выполняются быстрее, чем mfence.
Ну а sfence/lfence, если не рассматривать кейсы с "non-temporal hint", на x86 и x64 вообще бессмысленны, потому что там для "load-load" и
"store-store" порядок гарантируется архитектурой. И вставлять, например, sfence между двумя записывающими mov — только терять время.
Ф>Это потому, что указанные инструкции используются для атомарных операций, при этом суть их — блокировки. Все приведённые инструкции неявно выставляют LOCK# на шину. Барьеры же используются для lock-free алгоритмов.
Давайте рассмотрим xchg как пример. В контексте обсуждения эта инструкция обладает как минимум тремя полезными свойствами:
1. она атомарна
2. она работает как барьер, запрещая "перепрыгивать" через нее операциям записи или чтения
3. она сбрасывает буферы записи (store buffer), действуя аналогично инструкциям сериализации вроде cpuid
8.2.2 Memory Ordering in P6 and More Recent Processor Families
...
Reads or writes cannot be reordered with I/O instructions, locked instructions, or serializing instructions.
...
Locked instructions have a total order.
11.10 STORE BUFFER
Intel 64 and IA-32 processors temporarily store each write (store) to memory in a store buffer. The store buffer
improves processor performance by allowing the processor to continue executing instructions without having to
wait until a write to memory and/or to a cache is complete. It also allows writes to be delayed for more efficient use
of memory-access bus cycles.
In general, the existence of the store buffer is transparent to software, even in systems that use multiple processors.
The processor ensures that write operations are always carried out in program order. It also insures that the
contents of the store buffer are always drained to memory in the following situations:
...
* When a LOCK operation is performed.
...
22.34 STORE BUFFERS AND MEMORY ORDERING
The Pentium 4, Intel Xeon, and P6 family processors provide a store buffer for temporary storage of writes (stores)
to memory (see Section 11.10, “Store Buffer”). Writes stored in the store buffer(s) are always written to memory
in program order, with the exception of “fast string” store operations (see Section 8.2.4, “Fast-String Operation and
Out-of-Order Stores”).
The Pentium processor has two store buffers, one corresponding to each of the pipelines. Writes in these buffers
are always written to memory in the order they were generated by the processor core.
It should be noted that only memory writes are buffered and I/O writes are not. The Pentium 4, Intel Xeon, P6
family, Pentium, and Intel486 processors do not synchronize the completion of memory writes on the bus and
instruction execution after a write. An I/O, locked, or serializing instruction needs to be executed to synchronize
writes with the next instruction (see Section 8.3, “Serializing Instructions”).
И теперь вопрос от меня: чем xchg хуже mfence в плане использования в качестве барьера?
O>>В обычном прикладном коде такие вещи практически не встречаются. Ну и сам по себе mfence, если верить дискуссиям в Гугле, занимает больше тактов CPU по сравнению с O>>locked-инструкциями типа xchg.
Ф>Весьма примерно представляю, как это проверить. Буду рад примеру кода, который подтвердит или опровергнет эту гипотезу.
У меня есть только ссылка, ведущая на обсуждение в группы linux kernel, где, насколько я понял, там в итоге отказались от mfence в пользу lock:
O>>Например, очень часто компилятор не вставляет никаких специальных O>>барьеров туда, где ты вроде бы их ожидаешь. Например потому, что определенные гарантии предоставляет сама архитектура: "stores are not reordered with other stores" и т.д.
Ф>Не понимаю о чём идёт речь. Что и где должно теоретически быть, но не вставляет? Я думал, что синхронизация целиком и полностью ответственность программиста.
Ну например, примерно такой код обычно приводится в качестве примера использования acquire/release семантики:
Здесь, мол, у нас в producer между записью в data и ready будет барьер, гарантирующий, что первое будет завершено строго до второго. И то же самое в consumer —
поток сначала увидит ready, и только потом начнет читать data. Но если посмотреть сгенерированный ассемблерный код, там нет никаких явно выставленных барьеров
при работе с переменной 'ready', просто mov на чтение и mov на запись:
void producer(void) PROC
mov DWORD PTR int data, 123
npad 1
mov BYTE PTR std::atomic<bool> ready, 1
ret 0
void producer(void) ENDP
void consumer(void) PROC
npad 2
$LL2@consumer:
movzx eax, BYTE PTR std::atomic<bool> ready
npad 1
test al, al
je SHORT $LL2@consumer
...
void consumer(void) ENDP
Здравствуйте, Философ, Вы писали:
Ф>Интересно, откуда инфа? Можно какие-нибудь доказательства [...] или дизасм получающегося кода, или объяснение того, что именно будет делать функция типа Ф>
Ф>void Foo(){
Ф>Thread.MemoryBarrier();
Ф>}
Ф>
Ф>Я ОООчень бы хотел узнать, почему вы так думаете, откуда вы это взяли и где тут логика. Ф>Т.е. если платформа не AMD64 будет действительно xchg. Но логики я тут всё равно не вижу.
Прежде всего, откуда я это взял. Работа такая
:-)
Да просто видел уже много-много раз при работе с крэш-дампами, в отладчике, в IDA Pro, при анализе ситуаций с зависаниями и другими проблемами.
Частенько приходится работать непосредственно с ассемблерными листингами, в основном это 32- и 64-битный код под архитектуры Intel/AMD (до ARM пока руки не дошли).
Также иногда интересно бывает написать что-то с использованием atomic или memory_order_xxx (C++) и посмотреть, во что компилятор превратит написанное.
И я вообще не припомню, чтобы хоть раз где-то видел sfence, lfence или mfence в многопоточке, в основном там всегда именно xchg, cmpxchg, xadd и т.п.
sfence/lfence/mfence, насколько я знаю, появились в SSE/SSE2 для поддержки non-temporal операций вроде movntdq, когда запись идет напрямую в память, не попадая в кэш CPU.
Т.е. они дают возможность упорядочивать такие операции более эффективным способом по сравнению с cpuid, например.
В обычном прикладном коде такие вещи практически не встречаются. Ну и сам по себе mfence, если верить дискуссиям в Гугле, занимает больше тактов CPU по сравнению с
locked-инструкциями типа xchg. Поэтому, а также ввиду определенных причин, связанных с особенностями работы Intel/AMD в этих аспектах, в "обычном" коде чаще
используется именно xchg, а не fence.
Вообще, тема эта очень обширная и сложная, в двух-трех предложениях и малой части не описать. Например, очень часто компилятор не вставляет никаких специальных
барьеров туда, где ты вроде бы их ожидаешь. Например потому, что определенные гарантии предоставляет сама архитектура: "stores are not reordered with other stores" и т.д.
Здравствуйте, Философ, Вы писали:
Ф>С другой стороны, он просто не понимает зачем вся эта фигня — просто никогда не слышал о lock-free алгоритмах. И при этом очень вероятно, что и не видел никогда в многопоточном коде *fence инструкций
Или слышал и знает, что на x86/x64 атомарные операции создают full memory barrier...
Здравствуйте, dsalodki, Вы писали:
D>Поясните пожалуйста самым простым способом, я гуглю, но что-то не понятно. Толи это позволяет отменить кеширование переменно, толи ещё что-то
Здравствуйте, Marty, Вы писали:
M>На пальцах вроде всё понятно, но вот в те же плюсики завезли чуть ли не штук пять различных типов барьеров. Я пока плотно вопрос не изучал, но по диагонали — нифига не понял, чем отличаются
Ну, я описал самый простой случай полного барьера, который полностью отделяет операции "до барьера" от операций "после барьера". Он очень дорогой, и в реальной жизни обычно столько не нужно. Нам, например, может быть важно, чтобы все записи, которые по тексту программы стоят до барьера, до него и произошли, а что там с чтениями, может быть и не важно. Кроме того, у разных процессоров разная модель памяти, а стандарт C++, он, типа, всехний. Поэтому пытается покрыть все возможные варианты. На каком-то конкретном железе разные варианты могут и совпадать, но это не значит, что они будут совпадать на другом железе.
Здравствуйте, fdn721, Вы писали:
F>Ох уж эти объясняторы...
F>На самом деле ни кто ни чего не переупорядочивает.
На самом деле переупорядочивает. В Pentium-Pro появился буфер переупорядочивания, тогда ещё размером в 40 мопов и теперь он решает в каком порядке выполнять мопы, а не ты.
Всё сказанное выше — личное мнение, если не указано обратное.
Здравствуйте, MadHuman, Вы писали:
MH>вы утверждаете что при вытесняющей многозадачности невозможно что шедулер ОС продолжит выполнение потока на другом ядре??
Нет, конечно. Я утверждаю, что невозможны эффекты, которые Вы описывали.
MH>возможно при перекидывании потока на другое ядро кроме восттановления регистров ОС и делает ещё что-то чтоб обеспечить консистентность кэшей.
Не "возможно", а достоверно. Любая многопоточная ОС всегда гарантирует, что все данные отдельного потока, выполняемого на разных процессорах, будут выглядеть так же, как и на одном процессоре — если, конечно, поток специально не пытается отследить переключения. Проблемы могут возникать только при доступе к одним и тем же данным со стороны разных потоков.
Здравствуйте, syrompe, Вы писали:
S>Поправьте где неправ: S>1. volatile в С# — какая-то шляпа
Нет, оно работает так как должно. Штука очень полезная, особенно если ты шаришь память между между процессами (например с помощью MMF). Например, у тебя выделена память с помощью VirtualAlloc(), в которой ты сделал окно. Ты на неё смотришь через стуктуру (приводишь к указателю на структуру). Все поля этой структуры должны быть volatile — компилятор никогда не сможет угадать, кто и когда поменяет эту память. Фактически это указание компилятору, что к памяти имеет доступ кто-то ещё.
S>2. lock — вроде как ведет к Memory Barier, правда неявно
Да, это так.
S>3. у нас есть Interlocked, которого хватает в 99%
Нет: кому-то хватает, а кому-то нет. Interlocked в конечном счёте выставляет префикс lock на операции обращения к памяти. Это приодит к сигналу Lock на шине процессора при исполнение — производительность на таких штуках серьёзно деградирует.
В принципе любые примитивы синхронизации приводят к деградации производительности. Чем более они высокоуровневые — тем сильнее, хотя с походами в ядро им конечно не сравниться.
Насчёт Interlocked: в циклах ожидания нужно использовать процессорную инструкцию pause, но из C# этого сделать невозможно. Вот пример:
while (1 == Interlocked.CompareExchange(ref m_dwBusy, 1, 0))
{
//здесь должна быть pause
}
Т.е. я не знаю — по-моему это ещё не ушло в релиз.
Всё сказанное выше — личное мнение, если не указано обратное.
Здравствуйте, Pauel, Вы писали:
Pzz>>Вот тут-то и нужны явные барьеры памяти, чтобы сказать процессору, "доделай, пожалуйста, все операции с памятью, которые я заказывал до этого места, потому что дальше логика моей программы будет рассчитывать на то, что все отложенные/параллельные операции к этому месту уже доделаны"
P>А что они физически представляют? Функцию, инструкцию процессора или что?
В Си — функцию. Которая, фактически, превращается в инструкцию процессора.
Здравствуйте, Pzz, Вы писали:
Pzz>Здравствуйте, dsalodki, Вы писали:
D>>Поясните пожалуйста самым простым способом, я гуглю, но что-то не понятно. Толи это позволяет отменить кеширование переменно, толи ещё что-то
Pzz>Вот смотри, ты пишешь:
Pzz>
Pzz>a = 5;
Pzz>b = 10;
Pzz>
Pzz>И рассчитываешь, что если в b уже 10, то в a точно 5.
Pzz>Однако процессор может, по разным причинам, исполнить эти команды в другом порядке. В частности потому, что доступ в память, он не так уж и просто устроен, до одних мест может оказаться в данный момент быстрее дотянуться, до других — наоборот.
а законченный код тут с тем какая проблема может быть есть возможность написать? сейчас вроде любой код может выполнять на разных ядрах и поэтому не совсем ясна проблемная область
Здравствуйте, paradok, Вы писали:
>>>Когда несколько процессоров работают с одним адресом памяти P>это надо специально запрграммировать... сама по себе такая ситуация не может возникнуть... или давайте пример кода!
class Ab {
public int a;
public int b;
}
void thread1(Ab ab) {
ab.a = 5;
ab.b = 10;
}
void thread2(Ab ab) {
if (ab.a != 5 && ab.b == 10) {
impossible();
}
}
Вот пример кода. Без барьеров памяти на некоторых архитектурах (вроде ARM) функция impossible может быть вызвана, хотя это противоречит интуиции. Могут быть и более тонкие баги, к примеру конструктор вызывается в одном потоке, указатель на сконструированный объект передаётся в другой поток, но другой поток видит не до конца сконструированный объект и будут очень странные баги. В Java в конструкторы специально вставляются нужные барьеры, чтобы такой ситуации не возникло, про C# не знаю. В С++ такое точно возможно.
P>если же вы сами запрограммировали создание шаред мемори и сами ее меняете из разных процессов то и сами должны позаботиться о синхронизации и никакие барьеры вам не нужны
Общая память чаще встречается при многопоточном программировании. Внутри одной программы все потоки имеют доступ к общей памяти процесса.
Если использовать синхронизацию, дополнительно никакие барьеры действительно не нужны. Барьеры нужны, когда кажется, что использовать синхронизацию — слишком дорого и хочется ускорить подобный код. Т.н. lock-free алгоритмы используют подобный подход.
Здравствуйте, Философ, Вы писали:
Ф>Здравствуйте, Pauel, Вы писали:
P>>На одном процессоре такого не будет, процессор следит за зависимостями, и подкладывает результаты в соответствии с зависимостями.
Ф>В разных ядрах — запросто.
Эти операции будут выполняться в рамках определенного (одного) потока, поэтому на другое ядро сама по себе операция сравнения не перекинется.
Здравствуйте, Sharowarsheg, Вы писали:
S>Ну да. Если ты намеренно не запрограммировал это на разных потоках, то барьер не нужен. Вообще, мне кажется, если программировать всё в одном потоке, то вообще можно не знать, что барьеры существуют. По крайней мере, в "обычных" языках и процессорах.
Безотносительно многопоточности, возможно переупорядочивание инструкций внутри одного ядра (АЛУ).
Погуглите
* cpu instruction reordering
* out of order execution
Если отдельно взятый поток/АЛУ решит, что заданные две инструкции можно исполнить в любом порядке (поскольку зависимостей между ними не наблюдает), то он выберет тот, который даст бОльшую эффективность по заданному policy: energy consumption, или memory throughput или idle cycle count, или что там сейчас считается важным.
Само собой, что возможны случаи, когда сделанное предположение об отсутствии зависимостей является неверным, и АЛУ тем самым вносит side-effect, который не ожидался автором программы — лезут плавающие баги. Особенно сложно правильно определять такие зависимости, когда имеем дело с managed-платформами, когда есть некоторый промежуточный уровень абстракции (рантайм C# или Java VM). Именно поэтому там вводится понятие "модели памяти" (memory model), чтобы хоть как-то очертить правила игры и дать базовые гарантии, работающие одинаково на всех поддерживаемых аппаратных платформах. Ну а если базовых гарантий недостаточно или мы хотим объехать их для какого-то выигрыша — тогда в бой идут явные задания барьеров памяти.
vsb>>Могут быть и более тонкие баги, к примеру конструктор вызывается в одном потоке, указатель на сконструированный объект передаётся в другой поток, но другой поток видит не до конца сконструированный объект и будут очень странные баги. В Java в конструкторы специально вставляются нужные барьеры, чтобы такой ситуации не возникло, про C# не знаю. В С++ такое точно возможно.
S>Ага, в шарпе все также как и в Яве. Т.е. нельзя "опубликовать" не до конца сконтруированный объект.
А если в середине конструктора вызвать виртуальный метод, который передаст this в другой поток?
Здравствуйте, MadHuman, Вы писали:
MH>как шедулер ОС может знать с какой памятью (ну кроме стэка) поток работал и как?
Ну вот как планировщик может знать, с какими регистрами поток работал, и с какими будет работать следующий? Может, какие-то регистры и не нужно сохранять, ибо их не испортят. Но, поскольку он этого не знает, то сохраняет все регистры — на всякий случай. Затратно, зато надежно. В служебных потоках виндового ядра, например, автоматически сохраняются только регистры общего назначения, а всякие FPU/SSE, с которыми в ядре работать не принято, надо сохранять и восстанавливать вручную.
MH>вероятно это в железе обеспечено, но возможно ОС при продолжении работы потока на другом ядре даёт команду что-то типа — синхронизировать/сбросить кэши ядра (тот самый мемори барьер?).
Сама по себе синхронизация кэшей в x86/x64 обеспечена аппаратно, так что планировщику достаточно лишь создать барьер, чтобы исключить перестановку операций. Ничего специального для этого делать не нужно, поскольку любому планировщику приходится использовать атомарные операции, каждая из которых в x86/x64 создает полный барьер.
Здравствуйте, Sharov, Вы писали:
S>lock-free на уровне железе нифига не lock-free
Так не все ж алгоритмы, требующие атомарности и исключительности, подразумевают более долгое ожидание, чем требуется на банальную синхронизацию доступа к одной-двум ячейкам памяти. Даже обычный инкремент разделяемого счетчика традиционно было принято заворачивать в критическую секцию, которая традиционно же работала через примитивы синхронизации ОС, влекущие обращение к планировщику ядра. Замена на аппаратно атомарную операцию как раз и превращает действие в lock-free, подразумевая, что никто не тормозится дольше, чем это требуется на аппаратное выполнение команды в самом неудачном случае.
Кроме этого, многие lock-free алгоритмы не требуют, чтобы действие непременно выполнялось определенным потоком. В ряде случаев достаточно, чтобы его выполнил любой поток, который первым сумел получить доступ к ресурсу.
Ну а продвинутые lock-free алгоритмы еще и подразумевают сотрудничество между потоками. Если один выполнил команду типа test-and-set и получил нуль (ресурс был свободен), он выбирает одну последовательность действий, а другой, получив не нуль — другую. Задача в том, чтобы завершить операцию за некоторое разумное количество таких шагов, по времени заведомо выигрывающее у алгоритмов с ожиданием.
Здравствуйте, snaphold, Вы писали:
S>сначала вроде понял и уложил в памяти а потом вот это и непонятно тогда зачем мемори барьер нужен если ядря знают про кэши других ядер?
Тебя обманули: синхронизация кэшей между ядрами существует, но кэши тут не при чём. Процессор может переупорядочивать инструкции, и в этом проблема. Существуют специальные инструкции, запрещающие переупорядочивание доступа к памяти:
1)sfence — store fence (запрет переупорядочивания записи
2)lfence — load fence (запрет переупорядочивания загрузки)
3)mfence — full fence (запрет переупорядочивания и записи и загрузки)
барьер ставит одну из них.
Всё сказанное выше — личное мнение, если не указано обратное.
Здравствуйте, snaphold, Вы писали:
F>>PS Это справедливо для абстрактного процессора в вакууме. Все(кроме NUMA) процессоры с архитектура x86/x64 сделаны так, что все ядра знают про кэши соседних ядер, и соответственно ни каких барьеров памяти в них нет.
S>сначала вроде понял и уложил в памяти а потом вот это и непонятно тогда зачем мемори барьер нужен если ядря знают про кэши других ядер?
Ядра не знают про кеши других ядер. Существует некий протокол, который синхронизирует кеши, но это происходит как бы асинхронно. Процессор не ждёт окончания этой синхронизации для следующих операций. Он записал в кеш значение и потом специальное оборудование, которое управляет этим кешом, начинает рассылку сообщений по межпроцессорной шине, что по такому-то адресу значение поменялось. Эти сообщения через некоторое количество тактов достигнут другого ядра, другое ядро проверит, есть ли в его кеше такой адрес, если есть, то обновит в нём значение и так далее. То бишь если ты меняешь значение в памяти на одном процессоре, то другой процессор когда-нибудь его увидит. Но когда и в каком порядке — тут гарантий никаких без дополнительных инструкций не будет.
PS да, есть ещё проблема, что компилятор может "соптимизировать" обращения в память и вообще туда ничего не писать, используя регистр для этого. Тогда, конечно, ничего синхронизировать никто не будет, регистры это полностью приватные данные для процессора. Но это уже другой разговор.
PPS всё, что писали про то, что процессоры могут переупорядочивать запись в память тоже верно и тоже может вызывать неожиданное поведение у другого процессора, читающего ту же память. В общем тема довольно запутанная и моё имхо — лучше всего пользоваться примитивами синхронизации, которые на современных процессорах весьма быстрые, а про барьеры пускай думают те, кто пишет их реализации. Ну и про volatile не забывать, чтобы компилятор не слишком увлекался оптимизациями.
Здравствуйте, Философ, Вы писали:
Ф>Это потому, что указанные инструкции используются для атомарных операций, при этом суть их — блокировки. Все приведённые инструкции неявно выставляют LOCK# на шину. Барьеры же используются для lock-free алгоритмов.
Я что-то думал, что lock-free алгоритмы — это про то, что не используется никаких объектов синхронизации, даже таких легких, как spin lock/CriticalSection. Без атомарных операций lock-free вряд ли получиться сделать, и префикс команды LOCK он не про то совсем
Pzz>Вот тут-то и нужны явные барьеры памяти, чтобы сказать процессору, "доделай, пожалуйста, все операции с памятью, которые я заказывал до этого места, потому что дальше логика моей программы будет рассчитывать на то, что все отложенные/параллельные операции к этому месту уже доделаны"
На пальцах вроде всё понятно, но вот в те же плюсики завезли чуть ли не штук пять различных типов барьеров. Я пока плотно вопрос не изучал, но по диагонали — нифига не понял, чем отличаются
Здравствуйте, Pzz, Вы писали:
Pzz>Вот тут-то и нужны явные барьеры памяти, чтобы сказать процессору, "доделай, пожалуйста, все операции с памятью, которые я заказывал до этого места, потому что дальше логика моей программы будет рассчитывать на то, что все отложенные/параллельные операции к этому месту уже доделаны"
А что они физически представляют? Функцию, инструкцию процессора или что?
Здравствуйте, Pauel, Вы писали:
P>Здравствуйте, Pzz, Вы писали:
Pzz>>Вот тут-то и нужны явные барьеры памяти, чтобы сказать процессору, "доделай, пожалуйста, все операции с памятью, которые я заказывал до этого места, потому что дальше логика моей программы будет рассчитывать на то, что все отложенные/параллельные операции к этому месту уже доделаны"
P>А что они физически представляют? Функцию, инструкцию процессора или что?
a=10
b=5
но a + b = любое число? или вовсе не инициализировано?
Здравствуйте, Marty, Вы писали:
M>На пальцах вроде всё понятно, но вот в те же плюсики завезли чуть ли не штук пять различных типов барьеров. Я пока плотно вопрос не изучал, но по диагонали — нифига не понял, чем отличаются
Разные типы барьеров используется для достижения дополнительных выигрышей в производительности. Самый базовый тип — это полный барьер (на чтение и на запись), его используют всегда по-умолчанию. Но если у разработчика есть достаточная необходимость и квалификация, то он может выбирать и более легкие подтипы барьера для конкретных задач где это уместно.
Каждый конкретный язык/среда предоставляет свой набор примитивов для барьеров, поэтому их API могут отличатся, но базовый принцип один и тот же.
Здравствуйте, samius, Вы писали:
S>Здравствуйте, dsalodki, Вы писали:
D>>Поясните пожалуйста самым простым способом, я гуглю, но что-то не понятно. Толи это позволяет отменить кеширование переменно, толи ещё что-то S>Это инструкция, запрещающая перенос инструкций через себя при переупорядочивании.
Это вроде volatile делает, а барьер на уровне железа просит закоммитить все возможные
изменения в памяти и привести кэши в порядок. Просто инструкции может перенести компилятор и
volatile ему это не даст.
Здравствуйте, Sharov, Вы писали:
S>Это вроде volatile делает, а барьер на уровне железа просит закоммитить все возможные S>изменения в памяти и привести кэши в порядок. Просто инструкции может перенести компилятор и S>volatile ему это не даст.
ЕМНИП volatile — один из частных видов барьера памяти.
Здравствуйте, Codealot, Вы писали:
S>>Это вроде volatile делает, а барьер на уровне железа просит закоммитить все возможные S>>изменения в памяти и привести кэши в порядок. Просто инструкции может перенести компилятор и S>>volatile ему это не даст. C>ЕМНИП volatile — один из частных видов барьера памяти.
Скорее способ сказать компилятору, чтобы он не занимался соотв. оптимизациями с соотв.
переменной. Ну и вроде соотв. доп. инструкции барьеров генерятся, хотя для х86 вроде бы
особого смысл они не имеют. Тут в тему должен ворваться netch80 и все объяснить.
Здравствуйте, Codealot, Вы писали:
C>Здравствуйте, samius, Вы писали:
S>>Это инструкция, запрещающая перенос инструкций через себя при переупорядочивании.
C>Не только. Барьеры также касаются синхронизации кэшей разных ядер.
соглашусь с формулировкой "регламентируют поведение", которое кэши (железо) и модель памяти должны обеспечивать.
Здравствуйте, Sharowarsheg, Вы писали:
S>Здравствуйте, paradok, Вы писали:
P>>а в реальности что бывает P>>a=5 P>>b=10 S>a > b=true
S>Да, если a=5 b = 10 выполняется на одном процессоре, а сравнение на другом.
не-е-е-е, если вы намерено не запрограммировали и не выполнили сравнение в другом потоке или нити никогда не будет.
покажите плиз работающий пример кода где такое происходит без преднамеренного програминга сравнения в другом потоке или нити или процессе!
vsb>Суть решаемой проблемы:
vsb>У нас есть многопроцессорный компьютер с общей памятью.
>>Когда несколько процессоров работают с одним адресом памяти
это надо специально запрграммировать... сама по себе такая ситуация не может возникнуть... или давайте пример кода!
ведь изначально код был такой
a=5
b=10 a>b=true
если же вы сами запрограммировали создание шаред мемори и сами ее меняете из разных процессов то и сами должны позаботиться о синхронизации и никакие барьеры вам не нужны
На самом деле ни кто ни чего не переупорядочивает. Это всего лишь следствие того как разные ядра скидывают свой локальный кэш в ОЗУ.
Вот к примеру есть три переменные A, B, С.
A = C + 1;
B = C + 1;
Чтобы выполнить эти присваивания, Ядро процессора №1 выполняет следующие действия.
1) Загружает переменную C из ОЗУ в Кэш ядра.
...
5) Загружает переменную C из Кэша в Регистр.
6) Увеличивает значение в Регистре на 1.
7) Запишет значение из Регистра в Кэш.
8) Увеличивает значение в Регистре на 1
7) Запишет значение из Регистра в Кэш.
...
20) Вытолкнет из Кэша значение переменной B в ОЗУ.
...
25) Вытолкнет из Кэша значение переменной A в ОЗУ.
Ядра №2,3,4,..N при этом ни чего не знают про значение в Кэше Ядра №1, они видит только ОЗУ. И для них с начало изменится переменная B, а потом через некоторое время переменная А.
Соответствен если вставить барьерную инструкцию, то это заставит Ядро 1 вытолкнуть значение переменно A из Кэша в ОЗУ, до переменной B.
A = C + 1;
memory_barrier();
B = C + 1;
PS Это справедливо для абстрактного процессора в вакууме. Все(кроме NUMA) процессоры с архитектура x86/x64 сделаны так, что все ядра знают про кэши соседних ядер, и соответственно ни каких барьеров памяти в них нет.
Здравствуйте, samius, Вы писали:
S>volatile как модификатор переменной — запрет на кэширование. volatile как инструкция — запрет на чтение/запись из кэша в конкретном месте, т.е. немного не о том в общем случае, но в частном (C# и .NET) volatile фактически генерирует вокруг обращений еще и барьеры. В языках, где volatile не генерирует барьеры, проще представить одно без другого.
volatile в шарпе не генерирует никаких барьеров.
Слово volatile запрещает оптимизации компялитора в отношении поля, например компилятор всегда будет обращаться памяти где лежит эта переменная, не кешируя её в регистре. Ещё: компилятор не исключит обращения к этой переменной при чтении.
Новое значение в переменную компилятор будет писать там, где это написал программист, а не там где это оказалось удобно компилятору.
Всё!
Больше volatile не делает ничего. Барьеры при обращении к нескольким volatile полям по-прежнему нужны.
Всё сказанное выше — личное мнение, если не указано обратное.
F>PS Это справедливо для абстрактного процессора в вакууме. Все(кроме NUMA) процессоры с архитектура x86/x64 сделаны так, что все ядра знают про кэши соседних ядер, и соответственно ни каких барьеров памяти в них нет.
сначала вроде понял и уложил в памяти а потом вот это и непонятно тогда зачем мемори барьер нужен если ядря знают про кэши других ядер?
Здравствуйте, Pauel, Вы писали:
P>А что они физически представляют? Функцию, инструкцию процессора или что?
В коннечном машинном коде будет mfence или как вариант одна из sfence/lfence инструкций — они запрещают переупорядочивание доступа к памяти вокруг себя.
Всё сказанное выше — личное мнение, если не указано обратное.
Здравствуйте, Pauel, Вы писали:
P>На одном процессоре такого не будет, процессор следит за зависимостями, и подкладывает результаты в соответствии с зависимостями.
В разных ядрах — запросто.
Всё сказанное выше — личное мнение, если не указано обратное.
Здравствуйте, vsb, Вы писали:
vsb>Если использовать синхронизацию, дополнительно никакие барьеры действительно не нужны.
Это если любая синхронизация автоматически создает барьер для всего, что еще не дозаписано в память. В типовых системах именно так и делается, чтобы не морочиться с отслеживанием того, какие данные действительно нужно записывать, а какие нет. Но строгая модель параллельных вычислений требует явно указывать каждую зависимость по данным, иначе правильность не гарантируется.
vsb>Могут быть и более тонкие баги, к примеру конструктор вызывается в одном потоке, указатель на сконструированный объект передаётся в другой поток, но другой поток видит не до конца сконструированный объект и будут очень странные баги. В Java в конструкторы специально вставляются нужные барьеры, чтобы такой ситуации не возникло, про C# не знаю. В С++ такое точно возможно.
Ага, в шарпе все также как и в Яве. Т.е. нельзя "опубликовать" не до конца сконтруированный объект.
Здравствуйте, 4058, Вы писали:
4>Эти операции будут выполняться в рамках определенного (одного) потока, поэтому на другое ядро сама по себе операция сравнения не перекинется.
Ну естественно речь о многопоточности идёт.
Всё сказанное выше — личное мнение, если не указано обратное.
Здравствуйте, dsalodki, Вы писали:
D>Поясните пожалуйста самым простым способом, я гуглю, но что-то не понятно. Толи это позволяет отменить кеширование переменно, толи ещё что-то
Поправьте где неправ:
1. volatile в С# — какая-то шляпа
2. lock — вроде как ведет к Memory Barier, правда неявно
3. у нас есть Interlocked, которого хватает в 99%
Здравствуйте, Философ, Вы писали:
S>>3. у нас есть Interlocked, которого хватает в 99%
Ф>Нет: кому-то хватает, а кому-то нет. Interlocked в конечном счёте выставляет префикс lock на операции обращения к памяти. Это приодит к сигналу Lock на шине процессора при исполнение — производительность на таких штуках серьёзно деградирует.
Чтобы почувствовать эту "серьезную деградацию", на современном железе нужно дергать эти операции с частотой хотя бы в сотни тысяч, а лучше — миллионы раз в секунду. Соответственно, чтоб это стало видно на фоне работы полезного кода, он должен быть идеально вылизан — и не на уровне языка, а на уровне машинного кода.
Ф>В принципе любые примитивы синхронизации приводят к деградации производительности. Чем более они высокоуровневые — тем сильнее, хотя с походами в ядро им конечно не сравниться.
Какие высокоуровневые примитивы синхронизации обходятся без походов в ядро?
P>не-е-е-е, если вы намерено не запрограммировали и не выполнили сравнение в другом потоке или нити никогда не будет.
Ну да. Если ты намеренно не запрограммировал это на разных потоках, то барьер не нужен. Вообще, мне кажется, если программировать всё в одном потоке, то вообще можно не знать, что барьеры существуют. По крайней мере, в "обычных" языках и процессорах.
Здравствуйте, Философ, Вы писали:
Ф>Здравствуйте, samius, Вы писали:
S>>volatile как модификатор переменной — запрет на кэширование. volatile как инструкция — запрет на чтение/запись из кэша в конкретном месте, т.е. немного не о том в общем случае, но в частном (C# и .NET) volatile фактически генерирует вокруг обращений еще и барьеры. В языках, где volatile не генерирует барьеры, проще представить одно без другого.
Ф>volatile в шарпе не генерирует никаких барьеров.
Если так, то с помощью какоюй таблетки обеспечивается семантика, заявленная в спеке C# в общем случае, а не про Intel X86/X64?
A read of a volatile field is called a volatile read. A volatile read has “acquire semantics”; that is, it is guaranteed to occur prior to any references to memory that occur after it in the instruction sequence.
A write of a volatile field is called a volatile write. A volatile write has “release semantics”; that is, it is guaranteed to happen after any memory references prior to the write instruction in the instruction sequence.
Ф>Слово volatile запрещает оптимизации компялитора в отношении поля, например компилятор всегда будет обращаться памяти где лежит эта переменная, не кешируя её в регистре. Ещё: компилятор не исключит обращения к этой переменной при чтении. Ф>Новое значение в переменную компилятор будет писать там, где это написал программист, а не там где это оказалось удобно компилятору. Ф>Всё!
Ф>Больше volatile не делает ничего. Барьеры при обращении к нескольким volatile полям по-прежнему нужны.
С оговорками про конкретную платформу — да. А вообще есть другие мнения. А так же указание полного барьера в методах VolatileRead/Write.
Здравствуйте, samius, Вы писали:
Ф>>volatile в шарпе не генерирует никаких барьеров. S>Если так, то с помощью какоюй таблетки обеспечивается семантика, заявленная в спеке C# в общем случае, а не про Intel X86/X64?
с помощью Thread.VolatileRead() и Thread.VolatileWrite()
S>С оговорками про конкретную платформу — да. А вообще есть другие мнения. А так же указание полного барьера в методах VolatileRead/Write.
Фишка в том, что барьеры нужны там где они нужны: там где есть опасность перестановок — нужны, там где пофиг — не нужны.
Всё сказанное выше — личное мнение, если не указано обратное.
Здравствуйте, Евгений Музыченко, Вы писали:
ЕМ>Чтобы почувствовать эту "серьезную деградацию", на современном железе нужно дергать эти операции с частотой хотя бы в сотни тысяч, а лучше — миллионы раз в секунду.
И тем не менее достичь этого легко: даже Thread.SpinWait(Int32) используется частенько. Достичь 100К итераций ожидания не так сложно как тебе кажется.
ЕМ>Какие высокоуровневые примитивы синхронизации обходятся без походов в ядро?
Ну например SpinLock, ну а если повезёт, то например ManualResetEventSlim тоже может на ожидании без походов в ядро обойтись.
Всё сказанное выше — личное мнение, если не указано обратное.
Здравствуйте, Философ, Вы писали:
Ф>Здравствуйте, samius, Вы писали:
Ф>>>volatile в шарпе не генерирует никаких барьеров. S>>Если так, то с помощью какоюй таблетки обеспечивается семантика, заявленная в спеке C# в общем случае, а не про Intel X86/X64?
Ф>с помощью Thread.VolatileRead() и Thread.VolatileWrite()
Разумеется, работа с volatile полями не приводит к задействованию этих методов. И, прямо в документации по предъявленным ссылкам, написано, что барьеры вставляются при необходимости.
S>>С оговорками про конкретную платформу — да. А вообще есть другие мнения. А так же указание полного барьера в методах VolatileRead/Write.
Ф>Фишка в том, что барьеры нужны там где они нужны: там где есть опасность перестановок — нужны, там где пофиг — не нужны.
А разве кто-то утверждал обратное? Что они не нужны там, где нужны и нужны там, где не нужны?
Здравствуйте, samius, Вы писали:
S>...И, прямо в документации по предъявленным ссылкам, написано, что барьеры вставляются при необходимости.
А в коде BCL написано что, там безусловно вставляется барьер, без всяких "по необходимости". Сходи и посмотри.
S>А разве кто-то утверждал обратное? Что они не нужны там, где нужны и нужны там, где не нужны?
В этой теме кто-то говорил, что для volatile полей генерируются барьеры. На деле же барьеры ты должен вставлять сам.
Всё сказанное выше — личное мнение, если не указано обратное.
Здравствуйте, Философ, Вы писали:
Ф>Здравствуйте, samius, Вы писали:
S>>...И, прямо в документации по предъявленным ссылкам, написано, что барьеры вставляются при необходимости.
Ф>А в коде BCL написано что, там безусловно вставляется барьер, без всяких "по необходимости". Сходи и посмотри.
Я об этом пару сообщений назад написал.
Здравствуйте, Философ, Вы писали:
Ф>И тем не менее достичь этого легко: даже Thread.SpinWait(Int32) используется частенько. Достичь 100К итераций ожидания не так сложно как тебе кажется.
Ну да, если вставлять, где ни попадя, то легко. Если хоть немного с умом (то есть, сообразуясь с реальной потребностью), то достаточно сложно.
ЕМ>>Какие высокоуровневые примитивы синхронизации обходятся без походов в ядро?
Ф>Ну например SpinLock
Он только с виду "высокоуровневый". Внутри там примитивный цикл из InterlockedCompareExchange (которая раскрывается в команду CmpXchg), InterlockedIncrement/InterlockedDecrement (которые раскрываются в lock xadd), или чего-то подобного. Реально же высокоуровневые примитивы, подразумевающие ожидание с переключением потоков, без походов в ядро не реализуемы.
Ф>если повезёт, то например ManualResetEventSlim тоже может на ожидании без походов в ядро обойтись.
И это на той же технике, как и критические секции со spin count. Как раз костыли для тех, кто их использует бездумно, "по учебнику".
Здравствуйте, Философ, Вы писали:
ЕМ>>Какие высокоуровневые примитивы синхронизации обходятся без походов в ядро?
Ф>Сейчас глянул сорцы CLR — Monitor.Enter() в конечном счёте тоже может без походов в ядро обойтись.
Из каких соображений Вы причисляете такие средства к "высокоуровневым"?
Здравствуйте, Философ, Вы писали:
ЕМ>>Из каких соображений Вы причисляете такие средства к "высокоуровневым"?
Ф>Из тех соображений, что иногда синхронизация делается вообще без каких-либо примитивов, пример рукопашной синхронизации.
То есть, если программа использует InterlockedCompareExchange, что фактически просто вставляет команду CmpXchg, то это "низкоуровневое" средство, а если вызывает некую библиотечную функцию, в которую завернута та же команда, то средство враз становится "высокоуровневым"?
Здравствуйте, Евгений Музыченко, Вы писали:
ЕМ>То есть, если программа использует InterlockedCompareExchange, что фактически просто вставляет команду CmpXchg, то это "низкоуровневое" средство, а если вызывает некую библиотечную функцию, в которую завернута та же команда, то средство враз становится "высокоуровневым"?
Ну в общем-то да: даже ReaderWriterLock в конечном счёте ведёт к CompareExchange.
Признаю, что пожалуй ты прав: SpinLock не стоило относить к высокоуровневым.
Всё сказанное выше — личное мнение, если не указано обратное.
Здравствуйте, Философ, Вы писали:
Ф>В конечном машинном коде будет mfence или как вариант одна из sfence/lfence инструкций — они запрещают переупорядочивание доступа к памяти вокруг себя.
sfence/lfence/mfence — это малость для другого. В машинный код для x86/x64 практически всегда вставляется инструкция типа xchg, xadd или что-то подобное.
Здравствуйте, okman, Вы писали:
O>sfence/lfence/mfence — это малость для другого. В машинный код для x86/x64 практически всегда вставляется инструкция типа xchg, xadd или что-то подобное.
Интересно, откуда инфа? Можно какие-нибудь доказательства, типа вот такой ссылки, ну или вот такой, или дизасм получающегося кода, или объяснение того, что именно будет делать функция типа
void Foo(){
Thread.MemoryBarrier();
}
Я ОООчень бы хотел узнать, почему вы так думаете, откуда вы это взяли и где тут логика.
ЗЫ: прежде чем отвечать сходите по приведённым ссылкам.
Здравствуйте, Евгений Музыченко, Вы писали:
ЕМ>Использовать такое на пользовательском уровне нужно с осторожностью, поскольку поток, вошедший в такой цикл, может быть прерван, и другой поток, пытающийся его дождаться, исчерпает свой квант, и тоже будет вытеснен. До какой-то частоты использования такие блокировки позволяют сэкономить на переключениях в ядро, но дальше они могут привести к резкому росту накладных расходов. Особенно в том случае, если код отлаживался на восьмиядерном процессоре, а затем запущен на двухъядерном, где вероятность прерывания потока примерно вчетверо выше.
Я знаю это. Спинлоки у меня там, где ожидаемое время WaitForSingleObject() на сигнальном объекте, например на свободном мьютексе, больше чем время ожидания. Второй случай, когда я такое использую — протокол реализован на MMF, где одно из полей — признак занятости. Там просто без вариантов: Sleep(1) ждёт дольше чем нужно.
Всё сказанное выше — личное мнение, если не указано обратное.
Здравствуйте, Философ, Вы писали:
Ф>Здравствуйте, paradok, Вы писали:
P>>пример кода чтобы можно было запустить и увидеть эти самые плавающие ошибки?
Ф>Можно. https://eric-lo.gitbook.io/lock-free-programming/memory-fence Ф>Там в статье пример кода, который это демонстрирует.
В данной статье автор в одном потоке пишет в глобальные переменные, а в другом из них читает без обеспечения должным образом критической секции при обращении к этим переменным (т.е. нарочито демонстрирует поведение ССЗБ):
, при этом настойчиво пытается продемонстрировать побочный эффект от переупорядочивания присвоений в обоих потоках разбросанных по разным ядрам.
Здравствуйте, 4058, Вы писали:
4>В данной статье автор в одном потоке пишет в глобальные переменные, а в другом из них читает без обеспечения должным образом критической секции при обращении к этим переменным (т.е. нарочито демонстрирует поведение ССЗБ):..
В данной статье автор рассказывает о lock-free алгоритмах, поэтому там никаких локов быть в принципе не может.
4>, при этом настойчиво пытается продемонстрировать побочный эффект от переупорядочивания присвоений в обоих потоках разбросанных по разным ядрам.
Так ведь продемонстировал же! И демонстрашка там эта очень в тему: раз уж мы разбираемся в lock-free, то должны бы знать и о буферах переупорядочивания, и о спекулятивном исполнении и ещё много чего.
Всё сказанное выше — личное мнение, если не указано обратное.
Здравствуйте, Pzz, Вы писали:
Pzz>Здравствуйте, okman, Вы писали:
Pzz>Чтобы этого избежать, существует префикс LOCK... Pzz>Это совсем не то же самое, что reordering инструкций, от которого "защищают" барьеры памяти.
Ты не находишь странным такое объяснять системному программисту, который разбирает дампы и ковыряется в ассемблерных листингах!? По-моему он просто стебётся.
С другой стороны, он просто не понимает зачем вся эта фигня — просто никогда не слышал о lock-free алгоритмах. И при этом очень вероятно, что и не видел никогда в многопоточном коде *fence инструкций: lock-free всё-таки редкость. Даже сомневаюсь, что в ВУЗах такое учат.
Всё сказанное выше — личное мнение, если не указано обратное.
Здравствуйте, okman, Вы писали:
O>Да просто видел уже много-много раз при работе с крэш-дампами, в отладчике, в IDA Pro, при анализе ситуаций с зависаниями и другими проблемами.
Видел что? Во что превращается c++ intrinsic _mm_mfence?
O>И я вообще не припомню, чтобы хоть раз где-то видел sfence, lfence или mfence в многопоточке, в основном там всегда именно xchg, cmpxchg, xadd и т.п.
Это потому, что указанные инструкции используются для атомарных операций, при этом суть их — блокировки. Все приведённые инструкции неявно выставляют LOCK# на шину. Барьеры же используются для lock-free алгоритмов.
O>sfence/lfence/mfence, насколько я знаю, появились в SSE/SSE2 для поддержки non-temporal операций вроде movntdq, когда запись идет напрямую в память, не попадая в кэш CPU.
О таков варианте использования в мануале по оптимизации для Pentium-4, где они впервые появились. К несчастью полную докуменацию по Pentium-4 мне не удалось найти — не могу ни подтвердить, ни опровергнуть
O>В обычном прикладном коде такие вещи практически не встречаются. Ну и сам по себе mfence, если верить дискуссиям в Гугле, занимает больше тактов CPU по сравнению с O>locked-инструкциями типа xchg.
Весьма примерно представляю, как это проверить. Буду рад примеру кода, который подтвердит или опровергнет эту гипотезу.
O>Вообще, тема эта очень обширная и сложная, в двух-трех предложениях и малой части не описать.
Ооо! Это точно.
O>Например, очень часто компилятор не вставляет никаких специальных O>барьеров туда, где ты вроде бы их ожидаешь. Например потому, что определенные гарантии предоставляет сама архитектура: "stores are not reordered with other stores" и т.д.
Не понимаю о чём идёт речь. Что и где должно теоретически быть, но не вставляет? Я думал, что синхронизация целиком и полностью ответственность программиста.
Всё сказанное выше — личное мнение, если не указано обратное.
Здравствуйте, okman, Вы писали:
O>2. она работает как барьер, запрещая "перепрыгивать" через нее операциям записи или чтения O>3. она сбрасывает буферы записи (store buffer), действуя аналогично инструкциям сериализации вроде cpuid
O>
O>8.2.2 Memory Ordering in P6 and More Recent Processor Families
O>Locked instructions have a total order.
O>
O> * When a LOCK operation is performed.
O> ...
Спасибо. Это то, что я упустил — забыл, или не внимательно читал. Я аж полез и перепроверил — это действительно так.
Сорри, просто не знал, об эффектах префикса LOCK.
O>И теперь вопрос от меня: чем xchg хуже mfence в плане использования в качестве барьера?
Не знаю.
Всё сказанное выше — личное мнение, если не указано обратное.
Здравствуйте, vmpire, Вы писали:
S>>Ага, в шарпе все также как и в Яве. Т.е. нельзя "опубликовать" не до конца сконтруированный объект. V>А если в середине конструктора вызвать виртуальный метод, который передаст this в другой поток?
Интересно, не пробовал. Зачем для этого виртуальный метод, любой пойдет же?
S>>>Ага, в шарпе все также как и в Яве. Т.е. нельзя "опубликовать" не до конца сконтруированный объект. V>>А если в середине конструктора вызвать виртуальный метод, который передаст this в другой поток? S>Интересно, не пробовал. Зачем для этого виртуальный метод, любой пойдет же?
Виртуальный интереснее. Так может быть ещё не до конца инициализирован даже базовый класс
P>>>а в реальности что бывает P>>>a=5 P>>>b=10 S>>a > b=true
S>>Да, если a=5 b = 10 выполняется на одном процессоре, а сравнение на другом.
P>не-е-е-е, если вы намерено не запрограммировали и не выполнили сравнение в другом потоке или нити никогда не будет.
выше речь шла про процессор, и вообще-то такое возможно, если перед сравнением шедулер потоков ОС переключил выполнение потока на другой процессор.
Здравствуйте, Философ, Вы писали:
Ф>Здравствуйте, paradok, Вы писали:
MD>>>Само собой, что возможны случаи,
P>>пример кода чтобы можно было запустить и увидеть эти самые плавающие ошибки?
Ф>Можно. https://eric-lo.gitbook.io/lock-free-programming/memory-fence Ф>Там в статье пример кода, который это демонстрирует.
нет. там просто создаются две нити и их пускают обрабатывать одну и туже память.
речь же шла о коде без принудительного создания нитей
a=2
b=3 a>b=true
ЕМ>Здравствуйте, MadHuman, Вы писали:
MH>>такое возможно, если перед сравнением шедулер потоков ОС переключил выполнение потока на другой процессор.
ЕМ>Невозможно.
вы утверждаете что при вытесняющей многозадачности невозможно что шедулер ОС продолжит выполнение потока на другом ядре??
очевидно что такое возможно. планировщик по возможности старается конечно сохранить выполнение треда на одном ядре, но не всегда это возможно.
ЕМ>Если железо не обеспечивает синхронизации кэшей само, планировщик делает это при переключении.
планировщик при восстановлении потока, восстанавливает регистры, он не знает что делал поток и к какой памяти обращался.
хотя тут деталями не владею, возможно при перекидывании потока на другое ядро кроме восттановления регистров ОС и делает ещё что-то
чтоб обеспечить консистентность кэшей. а может это в железе как-то обеспечено.
ЕМ>Не "возможно", а достоверно. Любая многопоточная ОС всегда гарантирует, что все данные отдельного потока, выполняемого на разных процессорах, будут выглядеть так же, как и на одном процессоре
это да, без этого никак.
я выразил неуверенность, в том каким образом это достигается. как шедулер ОС может знать с какой памятью (ну кроме стэка) поток работал и как? никак.
поэтому вероятно это в железе обеспечено, но возможно ОС при продолжении работы потока на другом ядре даёт команду что-то типа — синхронизировать/сбросить кэши ядра (тот самый мемори барьер?).
Здравствуйте, Философ, Вы писали:
Ф>Это потому, что указанные инструкции используются для атомарных операций, при этом суть их — блокировки. Все приведённые инструкции неявно выставляют LOCK# на шину. Барьеры же используются для lock-free алгоритмов.
Дополню ответ Марти выше -- lock-free алгоритмы это суть алгоритмы на оснвое CAS, т.е. на основе атомарных инструкций процессора. Т.е. программист не использует никакие примитивы синхронизации и походы в ядро, вместо этого lock используеются на уровне железа. Т.е. lock-free на уровне железе нифига не lock-free, но судя
по всему, быстрее чем с примитивами синхронизации. Как-то так.
Здравствуйте, Mr.Delphist, Вы писали:
MD>Однако определённый эффект на соседние ядра это тоже оказывает, поэтому, например, нельзя "бесплатно" (с точки зрения производительности) делать активную запись двумя потоками в две соседних ячейки памяти, если они попадают в одну кэш-линию. По каждому сигналу инвалидации кэш-линии, контроллер памяти будет ставить другое ядро на холд при попытке обращения к соседней ячейке, пока не синхронизирует два экземпляра этой кеш-линии. Хотя казалось бы, зачем тут синхронизация исполнения — ведь каждый поток пишет только в свою ячейку и никак не работает с соседней (гуглить false sharing).
А контроллер памяти что-то знает про L1\L2\L3, т.е. он ли решает, что какой-то процессор\ядро надо на холд
поставить? Мне казалось, это какая-то процессораная логика внутри цпу решает.
Здравствуйте, Sharov, Вы писали:
S>А контроллер памяти что-то знает про L1\L2\L3, т.е. он ли решает, что какой-то процессор\ядро надо на холд S>поставить? Мне казалось, это какая-то процессораная логика внутри цпу решает.
Думаю, в современных процах очень тяжело провести чёткую границу где заканчивается контроллер и начинается сам CPU, но для простоты картины да, можно считать контроллер отдельным модулем. Иначе как бы разные процессоры узнавали об изменениях в данных со стороны своих соседей? Речь не про многоядерность (там можно эту логику на тот же чип вкрячить), а про многопроцессорные конфигурации. Собственно, поэтому ранние двухпроцессорные сборки на Pentium и были бессмысленными для большинства применений — процессорам приходилось играть по максимально пессимистичному варианту синхронизации из-за неразвитой на тот момент механики согласования доступа к данным.
Здравствуйте, Mr.Delphist, Вы писали:
MD>можно считать контроллер отдельным модулем. Иначе как бы разные процессоры узнавали об изменениях в данных со стороны своих соседей?
Обмениваясь друг с другом по магистрали.
MD>ранние двухпроцессорные сборки на Pentium
Ранние были на 486. В нем и появилась команда cmpxchg.
MD>были бессмысленными для большинства применений