Данная статья представляет собой в некотором роде краткое изложение того, что вы могли узнать, просмотрев различные видеоролики господина Шипилева, Елизарова, Смирнова.
На самом деле мы даже собрали для вас список воспроизведения если вы хотите пойти «трудным путем».
В этой статье я лишь попытаюсь донести до вас некоторые основные мысли/идеи, которые при желании вы можете изучить гораздо глубже в первоисточниках.
Итак, давайте теперь перейдем к самой теме.
Всего пять лет назад можно было без особой возни производить однопоточные программы, которые сложно запускать на топовом железе, и знать, что через год-два эта часть «софта» (простите за аллегорию) начнет нормально работать .
Сегодня этот «бесплатный обед» закончился.
На картинке хорошо видно, что количество транзисторов все еще растет, но по частотам мы почти достигли потолка.
«Кривость» рук разработчиков уже сложно компенсировать тем, что через год железо будет работать в два раза быстрее.
Хотя не все так печально, процессоры все равно растут, только по количеству ядер.
В итоге, чтобы «программа», написанная кривым орком, хоть как-то нормально работала на новом железе, ей нужно нормально работать в многопроцессорной среде.
А производительность напрямую зависела от количества ядер на оборудовании.
Именно о том, что «нормально работало в многопроцессорной среде», мы и поговорим дальше.
Ответов может быть два.
Во-первых, если вы пишете для конкретного оборудования, для Intel Xeon, или не дай бог Itanium или Эlbrus, ваш код будет совершенно другим и адаптированным под конкретное оборудование.
Такой код выжмет из железа все, но будет очень плохо масштабируемым.
Это подходит, когда вы пишете 3D-игры под конкретную железку и вам нужно выжать максимум из сегодняшнего железа, например PS4, а не ждать мифического завтра.
Что же делать обычным программистам, штопающим «Хайль Мир» под абстрактными платформами в вакууме и не очень понимающим разницу между архитектурой VLIW CPU и RISC? Еще им нужно как-то программировать многопоточные программы, чтобы они продуктивно работали на сегодняшнем железе, а на завтрашнем (новом железе) чтобы они работали еще быстрее.
И этот кусок программы, который мог работать на швейной машине Singer, после «умелой» реализации все равно можно было запустить на кластере.
Разве не такими должны быть программисты? Как можно решить любую проблему? Правильно, добавив новый уровень абстракции.
Конечно, данная ситуация не является исключением.
На помощь программистам приходит абстрактная машина (АМ), под которой большинство программистов собственно и пишут код. АМ по сути является компромиссом между желанием инженеров знать меньше и использовать максимально упрощенные, высокоуровневые концепции (привет ФП), в которых живут пони и какают радугой, и конкретной реальной аппаратной реализацией.
Она (АМ) говорит, что отныне вы, программисты, пишете не для конкретного железа, а для абстрактной машины (Вы все пишете для абстрактной машины!), которая имеет абстрактную память и эта память работает по описанной модели памяти.
И компилятор, JIT, интерпретатор или бог знает кто еще будет нести ответственность за сопоставление кода/байт-кода и т. д., созданного для абстрактной машины, с конкретной реализацией.
То есть если вы пишете, например, на С++ код, замечу, для абстрактной машины (я упомянул С++, потому что в его спецификации, по крайней мере С++11, очень стройно и понятно описана модель памяти), и уже Задача компилятора будет транслировать это в рабочий машинный код для конкретного оборудования, например, на Inantium (с архитектурой VLIW) или на процессорах CISC. Понятно, что разные части вашей программы будут показывать разную производительность на разных платформах.
Конечно, если вы хотите выжать из своего кода максимум производительности, то вам так или иначе придется выкинуть к черту все эти уровни абстракции и переписать все на то, что максимально приближено к железу в данный момент времени.
Конечно, это работает, если проблемы на уровне, близком к аппаратному, ведь если у вас медленный алгоритм, то добавление к нему усложнений путем переписывания на низком уровне вряд ли спасет ситуацию.
Но речь не идет о тех задачах, где необходим такой «рем-метал».
Мы все еще говорим об «абстрактных программистах».
Возвращаясь к абстракции, вопрос в том, почему мы выделяем ММ в абстрактной машине и почему в последние годы это вдруг стало актуальным? Описать абстрактную машину можно было уже давно и велись работы по этим темам, но это мало кому было нужно.
Дело в том, что раньше, когда преобладала однопоточная среда, все программы, так или иначе, были детерминированными.
Как сказал Шипилев, для программиста-прагматика ММ должна ответить всего на один вопрос: если я сейчас прочитаю переменную А в потоке, то результат какой из последних записей (если записей было больше одной) я смогу увидеть? В детерминированной однопоточной программе все это довольно легко определить даже не зная ММ языка, на котором пишешь.
Программа может быть полностью сломана для многоядерной архитектуры, но кого это волнует, если на дворе суровые девяностые и машины с более чем одним процессором — это суперкомпьютеры? Более того, когда появились первые возможности распараллеливания программы P4HT, они дали свои библиотеки и свое видение параллелизма и соответственно пионеры, как правило, писали код не для 2-3 абстрактных потоков, а для P4HT, что из курс не был масштабируемым.
Давайте подойдем ближе к телу.
Если мы собираемся дать ответы на вопросы о том, что мы можем прочитать по памяти, то мы посмотрим на наивную ММ, какой ее себе представляют многие из нас.
Эта система не учитывает, насколько медленно распространяется свет! Именно медленно, несмотря на то, что нас всегда учили, что свет достаточно быстр, есть пара условий.
Это не так быстро, и если мы не говорим об идеальных условиях в вакууме, то.
За один такт процессора с частотой 3 ГГц свет проходит в вакууме 10 см! Роман ЕлизаровЕсли у вас процессор с частотой 3 ГГц, то за один такт свет проходит 10 см, а по проводникам и того меньше.
В результате процессоры физически не могут вносить изменения в переменную и передавать информацию об этих изменениях другим процессорам.
На практике мы можем получить следующие варианты (это без предыдущих вариантов, но они тоже действительны).
Самый запутанный вариант — 0x0.
На самом деле это очень забавно, потому что мы, по сути, наблюдаем, в некоторой сложной форме, эффект искажения времени на высоких скоростях.
Конечно, странные результаты появляются из-за того, как ядра процессора синхронизируют данные друг с другом.
Но как круто осознавать, что у нас, программистов, есть свои процессы, чем-то напоминающие релятивистскую физику =) У нас есть два процессора, они как бы расположены независимо друг от друга и физический обмен данными происходит внутри них и за счет разные ограничения они видят друг друга по-разному, что один видит как настоящее (переменная «а» равна нулю, второй ЦП видит как прошлое, так как для него эта переменная уже равна -1).
Конечно, десинхронизация не вызвана скоростью; такие проблемы появились из-за увеличения сложности процессоров.
И на самом деле, если Мы думаем с этой стороны, нам очень легко представить себе все эти парадоксы на несколько видоизмененном примере.
Если вы посмотрите, как поток 2 видит поток 1, то он сможет увидеть что угодно.
Во вселенной потока 2 поток 1 может, например, записывать только a, то есть запись b может вообще не происходить.
Видна только запись b, запись a может вообще не произойти.
Он может видеть запись a и запись b, но их последовательность может быть совершенно другой, не такой, как внутри потока 1.
Как нетрудно догадаться, результаты будут совершенно непредсказуемыми.
В ранних версиях Java было довольно много так называемых «ошибок».
На самом деле толпы наивных леммингов, совершенно не понимая ограничений ММ, писали код, который на практике уходил в вечные циклы.
Хотя на самом деле Java просто применила ряд оптимизаций, так как наивно думала, что разработчик знает, что пишет. Мы написали отличную программу, протестировали ее на x86_64, затем запустили на 32-битном ARM (или PowerPC) и получили ситуации с сюрпризами.
А всё потому, что наш Вася Пупкин писал для конкретного железа, на котором тестировал, хотя на самом деле, сам того не понимая, писал для Абстрактной машины, спецификация которой даёт другие гарантии (более слабые), чем конкретная машина, для которой он писал свой код .
Конечно, осознание того, что он пишет для абстрактной машины, характеристики которой вам нужно знать, придет после таких поломок, но придет очень поздно, когда ваш код вдруг перестанет работать на машине клиента.
Как ММ может помочь нам разобраться в возникшем беспорядке и хаосе? Одним из самых фундаментальных понятий в ММ является концепция HappensBefore, введенная Лэмпортом еще в 1978 году.
Говоря простым языком, он говорит: да, у нас бардак, беспредел и мастурбация, но выделим из всей этой путаницы и шатания несколько операций.
что будет заказано между собой.
Это означает, что у нас будет две операции между потоками, в которых мы точно можем сказать, что одна операция будет строго упорядочена со второй операцией во втором потоке.
Чтобы сделать эту операцию более понятной, мы можем вернуться к нашему поддельному примеру.
И наконец, чтобы яснее понимать, как часто встречаются такие примеры и на каких платформах, четко видеть именно ту ситуацию, о которой я говорил, когда ты работаешь на своей локальной машине и все работает, просто потому, что ты не пишешь для абстрактной машины, а для конкретного железа рекомендую посмотреть доклад Глеба Смирнова: «Многопоточность под капотом» .
Хотя перед этим взгляните на общетеоретический доклад Романа Елизарова и, конечно же, Шипилева.
Все эти отчеты мы бережно собрали для вас в список воспроизведения , так что вы можете просто просмотреть все это.
Мы даже отсортировали отчеты для вас.
Теги: #Модель памяти #java
-
Бэби-Бумеры… Что Мы Будем С Ними Делать?
19 Oct, 24 -
Веб-Безопасность И Ваша Информация
19 Oct, 24 -
Взлет И Падение Blackberry
19 Oct, 24 -
Смартфон Huawei Honor Представлен Официально
19 Oct, 24