Дана стаття в деякому пологи витискання того, що Ви могли б дізнатися переглянувши різні відео ролики пана Шипілєва, Єлізарова, Смирнова. Власне ми навіть зібрали для Вас плейлист якщо Ви захочете піти «the hard way». У статті я лише спробую Вам передати деякі основні думки/ідеї, які при бажанні Ви зможете набагато більш глибоко вивчити в першоджерелах.
Отже, давайте тепер перейдемо до власне сабжу. Ще років п'ять тому можна було не сильно «парячись» видавати на-гора однопоточні програми, які насилу запускалися на топовому залізі і знати, що через рік-два цей шматочок «програмки» (вибачте за алегорію) почне працювати нормально. Сьогодні подібний «безкоштовний обід» закінчився.
На картинці чітко видно, що кількість транзисторів все ще зростає, але з точки зору частот ми практично досягли стелі. «Кривизну» рук розробників вже важко компенсувати тим, що через рік залізо стане працювати в два рази швидше. Хоча не все так сумно, процесори поки ще зростають, тільки з точки зору кількість ядер. Як наслідок, для того щоб «програмка», написана криворуким орком, змогла хоч якось нормально працювати на новому залізі, потрібно щоб вона нормально працювала в багатопроцесорному середовищі. А продуктивність була безпосередньо пов'язана з кількістю ядер на залізі. Ось власне від того, а що ж таке «нормально працювала багатопроцесорному середовищі» ми і поговоримо далі.
Відповіді може бути дві. Перший, якщо Ви пишете під конкретне залізо, під Intel Xeon, або не дай Бог Itanium або Ельбрус, Ваш код буде абсолютно різний і заточений під конкретне залізо. Такий код буде вичавлювати всі з залізки, але буде дуже слабо масштабуємо. Це підходить, коли пишіть 3D гри під конкретну залізницю і потрібно вичавити все з сьогоднішнього заліза, PS4 наприклад, і не чекати міфічного завтра. А що робити звичайним програмістам, які топають «Хайль Світ» під абстрактні платформи у вакуумі і не сильно розуміють різницю між VLIW архітектурою ЦПУ і RISC? Їм же теж потрібно якось програмувати багатопоточні програми, щоб вони і на сьогоднішньому залізі працювали продуктивно, а на завтрашньому (новому залізі) щоб працювали ще швидше. І цей шматок програми, який зміг би працювати і на швейній машинці «Зінгер», все ж після «вмілої» імплементації міг би бути запущений на кластері.
Ні так як бути програмістам? Будь-яку проблему можна вирішити як? Правильно, шляхом додавання нового рівня абстракції. Само собою ця ситуація не виняток. На допомогу програмістам приходить абстрактна машина (АМ), під яку насправді і пише код велика частина програмістів. АМ це за фактом трейдофф між бажаннями інженерів менше знати і використовувати максимально спрощено-високорівневі концепції (привіт FP), в яких живуть поні і какають веселками і конкретною реальною залізною реалізацією. Вона (АМ) каже, що відтепер, Ви програмісти не пишіть під конкретне залізо, Ви пишіть під абстрактну машину (Ви всі пишіть під абстрактну машину!) у якої є абстрактна пам'ять і ця пам'ять працює за описаною memory model. А вже компілятор, JIT, інтерпретатор або Бог знає хто ще буде відповідати за те, щоб замапити код/байткод/etc, створений для абстрактної машини, на конкретну імплементацію. Тобто, пишіть Ви, наприклад, на С++ код, зауважу, під абстрактну машину (я згадав С++ тому що в його спіку, як мінімум С + 11, дуже струнко і чітко описана memory model), а вже у компілятора завдання буде перевести це в робочий машинний код під конкретне залізо, ну наприклад на Inantium (з архітектурою VLIW на CCC. ISC. Ясна річ, що різні частини Вашої програми будуть при цьому показувати різну продуктивність на різних платформах.
Саме собою, якщо Ви хочете вичавити максимум продуктивності з Вашого коду, то так чи інакше, Вам доведеться викинути до всіх рис всі ці рівні абстракції і переписати все на тому, що максимально близько до заліза в даний момент часу. Звичайно ж це працює якщо проблеми саме на рівні близькому до заліза, бо якщо у Вас повільний алгоритм то додавати в нього ускладнення переписуючи це на низькому рівні навряд чи врятує становище. Але мова якраз і не про ті завдання де такий «бер метал» необхідний. Мова все ж про «абстрактних програмістів». Повертаючись до абстракції, питання, чому в абстрактній машині ми виділяємо саме ММ і чому це раптом стало актуально саме в останні роки? Описати абстрактну машину виходило давно і роботи на ці теми велися, але мало кому були потрібні. Справа в тому, що раніше при переважанні однопоточного середовища всі програми, так чи інакше, були детерміновані. Як говорив Шипілєв, для прагматичного програміста ММ повинна дати відповідь на всього одне питання: якщо я зараз прочитаю в потоці змінну А, то результат якого з останніх записів (якщо запис був не один) я зможу побачити?
У детермінованій однопоточній програмі все це досить просто визначити навіть не знаючи ММ мови на якій пишеш. Програма може бути повністю поламана для багатоядерної архітектури, але кому це важливо, якщо на дворі суворі дев'яності, а машини з більш ніж одним процесором це суперкомп'ютери? Більше того, коли з'являлися перші можливості розпараллелити програму P4HT, то вони давали свої ліби і своє бачення паралельності і відповідно першопрохідці, як правило, писали код не під 2-3 абстрактних потоки, а під P4HT, що само собою було не масштабовано.
Давайте «ближче до тіла». Якщо ми збираємося давати відповіді на запитання про те, що ж ми зможемо прочитати з пам'яті, то подивимося на наївну ММ, як її собі уявляють багато хто з нас.
Ця система не враховує, наскільки повільно поширюється світло! Саме повільно, незважаючи на те, що нас завжди вчили, що світло досить швидке, є пару умов. Він не такий вже й швидкий, а якщо ми говоримо не про ідеальні умови у вакуумі то...
За один такт 3ГГЦ процесора, світло у вакуумі проходить 10см!
Роман Єлізаров
Якщо у Вас є 3 ГГц процесор, то світло за його один такт проходить 10 см, у провідниках і того менше. Як результат, процесори фізично не здатні зробивши зміни зі змінною донести інформацію про ці зміни до інших процесорів. На практиці ж ми можемо отримати наступні варіанти (це без минулих варіантів, але вони теж в силі). Самий, зазвичай збиває з пантелику, варіант це 0х0.
Це взагалі супер забавно так як ми, по суті, спостерігаємо в деякій витонченій формі ефект спотворення часу на великих швидкостях. Само собою дивні результати з'являються через те як ядра процесорів синхронізують дані між собою. Але наскільки це круто усвідомлювати що у нас, програмістів є свої процеси які в деякому роді нагадують релятивістську фізику =) У нас є два ЦПУ, вони як би знаходяться незалежно один від одного і всередині їх відбувається фізичний обмін даними і через різні обмеження вони бачать один одного по різному, те що один бачить як справжнє (змінна «a» дорівнює нулю, другий ЦПУ бачить як минуле, оскільки для нього ця змінна дорівнює вже -1). Само собою, розсинхронізація викликана не такими швидкостями, подібні проблеми з'явилися через ускладнення процесорів. І справді, якщо ми будемо думати з цього боку нам всі ці парадокси дуже легко візуалізувати на злегка модифікованому прикладі.
Якщо подивитися на те, як потік 2 бачить потік 1, то він може побачити що завгодно. У всесвіті потоку 2, потік 1 може, наприклад, зробити тільки write a, тобто write b може взагалі не наступити. Може побачити тільки write b, write a може взагалі не наступити. Може побачити write a write b, але при цьому їх послідовність може зовсім іншою, не такою як всередині потоку 1.
Як не важко здогадатися, результати будуть абсолютно непередбачувані.
У ранніх версіях Java було досить багато того, що називалося «багами». Насправді, натовпи наївних лемінгів абсолютно не розуміючи обмежень ММ писали код, який на практиці йшов у вічні цикли. Хоча насправді Java просто застосовувала ряд оптимізацій, так як наївно думала, що розробник знає, що він пише.
Написали прекрасну програмку, протестили на х86 _ 64, після запускаєте на 32х бітному ARM (або PowerPC) і отримуєте ситуації з сюрпризами. А все тому, що писав наш Вася Пупкін під конкретне залізо на якому він і тестував, хоча насправді, не розуміючи він писав під Абстрактну Машину, специфікація якої дає інші гарантії (більш слабкі), ніж конкретна машинка під яку він писав свій код. Само собою, усвідомлення того, що він пише під абстрактну машину, спіку якої треба знати, після таких поломок прийде, але прийде дуже пізно, коли Ваш код раптом перестане працювати у клієнта на машині.
Як нам ММ допоможе власне розібратися з ось цією створюваною кашею і свавіллям? Одне з фундаментальних понять в ММ це поняття HappensBefore, введене Лампортом ще в далекому 1978 році. По простому він говорить, так у нас каша, свавілля і рукоблуддя, але давайте з усього цього розбріду і хитання виділимо кілька операцій, які між собою будуть впорядковані. Це означає, що у нас будуть дві операції між потоками при яких ми точно можемо сказати, що одна операція буде суворо впорядкована з другою операцією в другому потоці. Для наочності цієї операції можна повернутися до нашого бутафорського прикладу.
І нарешті, для того щоб більш наочно зрозуміти, як часто подібні приклади таки відбуваються і на яких платформах, побачити наочно рівно ту ситуацію, про яку я говорив, коли Ви працюєте на своїй локальній машинці і все працює, просто тому що ви пишете не для абстрактної машини, а для конкретного заліза, рекомендую подивитися доповідь Гліба Смирнова: «Багатопоточність під капотом». Хоча перед цим погляньте на загальнотеоретичну доповідь Романа Єлізарова і само собою Шипілєва. Всі ці доповіді ми акуратно зібрали для Вас в плей-лист, щоб Ви могли просто весь його переглянути. Ми для Вас навіть відсортували доповіді.








