Чому деякі мови програмування "швидші" або "повільніші", ніж інші?


80

Я помітив, що деякі програми або алгоритми, побудовані на мові програмування, скажімо, C ++ / Rust запускаються швидше або швидше, ніж ті, що побудовані, скажімо, на Java / Node.js, що працюють на одній машині. У мене є кілька запитань щодо цього:

  1. Чому це відбувається?
  2. Що регулює "швидкість" мови програмування?
  3. Це щось пов’язане з управлінням пам’яттю?

Я дуже вдячний, якби хтось зламав це за мене.


18
Зауважте, що, зокрема, для JVM та CLR було проведено широке дослідження з оптимізації віртуальних машин, і вони можуть перевершити складений C ++ за багатьох обставин, - але наївний бенчмаркінг легко зробити наївно.
chrylis -на страйк-


26
Однак, поєднання Java та всього, що пов'язано з Javascript разом, є образливим.
Рафаель

5
Мене розриває між тим, як закрити це як занадто широке (підручники можна писати на відповідні теми!) Або дублювати . Голосування громади, будь ласка!
Рафаель

4
це питання також досить схоже на те, що визначає швидкість мови програмування
vzn

Відповіді:


95

У розробці та реалізації мови програмування існує велика кількість варіантів, які можуть вплинути на продуктивність. Я згадаю лише кілька.

Кожна мова в кінцевому підсумку повинна працювати за допомогою машинного коду. "Скомпільована" мова, така як C ++, аналізується, декодується та переводиться на машинний код лише один раз під час компіляції. "Інтерпретована" мова, якщо вона реалізована прямо, розшифровується під час виконання, на кожному кроці, кожен раз. Тобто, кожного разу, коли ми запускаємо оператор, інтепретер повинен перевіряти, чи це то інше, чи це завдання тощо, і діяти відповідно. Це означає, що якщо ми циркулюємо 100 разів, ми декодуємо той самий код 100 разів, витрачаючи час. На щастя, перекладачі часто оптимізують це за допомогою, наприклад, щойно вчасно складеної системи. (Більш правильно, немає такого поняття, як "складена" чи "інтерпретована" мова - це властивість реалізації, а не мови. Все ж,

Різні компілятори / перекладачі виконують різні оптимізації.

Якщо мова має автоматичне управління пам’яттю, її реалізація повинна здійснювати збирання сміття. Це має витрати на виконання, але позбавляє програміста від завдання, схильного до помилок.

Мова може бути ближче до машини, що дозволяє експертові-програмісту мікрооптимізувати все та видавити більшу продуктивність із процесора. Однак можна сперечатися, якщо це насправді вигідно на практиці, оскільки більшість програмістів не дуже мікрооптимізуються, і часто хороша мова вищого рівня може бути оптимізована компілятором краще, ніж те, що робив би середній програміст. (Однак, іноді віддаляючись від машини, це може мати і свої переваги! Наприклад, Haskell надзвичайно високого рівня, але завдяки своєму дизайну вибір може мати дуже легкі зелені нитки.)

Статистична перевірка типу також може допомогти в оптимізації. У динамічно типізованій інтерпретованій мові кожен раз, коли проводяться обчислення x - y, інтерпретатору часто доводиться перевіряти, чи x,yє обидва числа чи (наприклад) підвищувати виняток інакше. Цю перевірку можна пропустити, якщо типи вже перевірені під час компіляції.

Деякі мови завжди повідомляють про помилки виконуваного часу на розумний спосіб. Якщо ви пишете a[100]на Java, де aє лише 20 елементів, ви отримаєте виняток із виконання. У C це може спричинити невизначене поведінку, тобто програма може збоїти, перезаписати деякі випадкові дані в пам'ять або навіть виконати абсолютно будь-що інше (стандарт ISO C не має жодних обмежень). Це вимагає перевірки виконання, але надає програмісту набагато приємнішу семантику.

Однак майте на увазі, що при оцінці мови продуктивність - це не все. Не будьте одержимі з цього приводу. Це звичайна пастка, щоб спробувати мікрооптимізувати все, але все ж не вдалося помітити, що використовується неефективна структура алгоритму / даних. Кнут якось сказав, що "передчасна оптимізація - корінь усього зла".

Не варто недооцінювати , як важко написати програму право . Часто може бути краще вибрати "повільнішу" мову, яка має більш зручну для людини семантику. Крім того, якщо є якісь конкретні критичні характеристики продуктивності, вони завжди можуть бути реалізовані іншою мовою. Так само, як довідник, на конкурсі програмування ICFP 2016 року були перекладені такі мови:

1   700327  Unagi                       Java,C++,C#,PHP,Haskell
2   268752  天羽々斬                     C++, Ruby, Python, Haskell, Java, JavaScript
3   243456  Cult of the Bound Variable  C++, Standard ML, Python

Жоден із них не використовував єдиної мови.


81
Одна версія всієї цитати з Knuth : "Програмісти витрачають величезну кількість часу на роздуми або переживаючи про швидкість некритичних частин своїх програм, і ці спроби ефективності насправді мають сильний негативний вплив при налагодженні та технічному обслуговуванні. Ми повинні забути про малу ефективність, скажімо, про 97% часу: передчасна оптимізація - корінь усього зла. Але ми не повинні пропускати свої можливості в цих критичних 3% ".
Дерек Елкінс

6
Також деякі мови гарантують, здавалося б, невинні речі, які можуть вплинути на здатність компілятора оптимізувати, див. Суворий псевдонім у C ++ проти FORTRAN
PlasmaHH

9
Приєднався, щоб я міг підтримати коментар від @DerekElkins. Занадто часто контекст цієї цитати НЕ вистачає, іноді навіть призводить до висновку , що він виступає за те , все оптимізації є злом.
труба

9
@DerekElkins За іронією долі ви пропустили, мабуть, найважливішу частину: два наступні речення. "Хороший програміст не буде принаджений до самовдоволення такими міркуваннями. Він буде розумним уважно переглянути критичний код; але лише після того, як цей код буде визначений. Часто помилкою є апріорні судження про те, які частини Програма дуже важлива, оскільки універсальним досвідом програмістів, які користуються інструментами вимірювання, було те, що їхні інтуїтивні здогадки не вдається ". PDF p268
8bittree

9
@DerekElkins Щоб було зрозуміло, я не хочу вносити слова в рот, мовляв, ви це стверджували, але занадто часто я натрапляю на людей, які додають трохи до базової цитати, і думаю, що Кнут виступає за передчасну оптимізацію 3 % часу, коли в повній статті йдеться про те, що він завжди виступає проти передчасної оптимізації, майже завжди виступає проти невеликих оптимізацій, але виступає за невеликі оптимізації в розділах, оцінених як критичні.
8bittree

18

Що регулює "швидкість" мови програмування?

Немає такого поняття, як "швидкість" мови програмування. Існує лише швидкість певної програми, записана певним прогамером, виконана певною версією певної реалізації певного двигуна виконання, що працює в певному середовищі.

У роботі одного і того ж коду, написаного однією і тією ж мовою на одній машині з використанням різних реалізацій, можуть бути величезні відмінності. Або навіть використовувати різні версії однієї і тієї ж реалізації. Наприклад, запуск того самого еталону ECMAScript на цій же машині з використанням версії SpiderMonkey від 10 років тому проти версії цього року, ймовірно, призведе до підвищення продуктивності в будь-якому місці між 2 × –5 ×, можливо, навіть на 10 ×. Чи означає це тоді, що ECMAScript на 2 × швидше, ніж ECMAScript, тому що запуск нової програми на одній машині на 2 × швидше з новою реалізацією? Це не має сенсу.

Це щось пов’язане з управлінням пам’яттю?

Не зовсім.

Чому це відбувається?

Ресурси. Гроші. Microsoft, ймовірно, працює більше людей, які готують каву для своїх програмістів-компіляторів, ніж у всій спільноті PHP, Ruby та Python. У спільній роботі працюють люди, що працюють над їх віртуальними машинами.

Для більш-менш будь-якої функції мови програмування, яка певним чином впливає на продуктивність, також існує рішення. Наприклад, C (я використовую C тут як резервне використання для класу подібних мов, деякі з яких існували навіть до C) не є безпечним для пам'яті, тому кілька програм C, що працюють одночасно, можуть топтатись пам'ять один одного. Отже, ми вигадуємо віртуальну пам'ять і змушуємо всі програми C проходити через шар непрямості, щоб вони могли зробити вигляд, що вони єдині, що працюють на машині. Однак це повільно, і так, ми вигадуємо MMU і впроваджуємо віртуальну пам'ять в апаратне забезпечення, щоб прискорити її.

Але! Мови, безпечні для пам’яті, не потребують усього цього! Наявність віртуальної пам'яті не допомагає їм ні на один біт. Насправді, це ще гірше: не тільки віртуальна пам'ять не допомагає безпечним для пам’яті мовам, віртуальна пам’ять, навіть якщо вона реалізована в апаратному забезпеченні, все ще впливає на продуктивність. Це може бути особливо шкідливим для роботи сміттєзбірників (саме для цього використовується значна кількість реалізацій мов, безпечних для пам’яті).

Інший приклад: сучасні центральні процесори загального призначення використовують складні прийоми, щоб зменшити частоту пропусків кешу. Дуже багато цих хитрощів є спробою передбачити, який код буде виконуватися і яка пам'ять буде потрібна в майбутньому. Однак для мов з високим ступенем поліморфізму виконання (наприклад, мови OO) передбачити ці схеми доступу дуже важко.

Але є й інший спосіб: загальна вартість пропусків кешу - це кількість пропусків кешу, помножена на вартість окремої пропуски кешу. Основні процесори намагаються зменшити кількість пропусків, але що робити, якщо ви могли зменшити вартість окремих пропусків?

Процесор Azul Vega-3 був спеціально розроблений для роботи віртуалізованих JVM, і він мав дуже потужний MMU з деякими спеціалізованими інструкціями щодо сприяння збору сміття та виявлення втечі (динамічний еквівалент статичному аналізу втечі), потужними контролерами пам'яті та всією системою все-таки вдасться досягти прогресу з понад 20000 непогашених пропусків кешу в польоті. На жаль, як і у більшості мовних процесорів, його дизайн був просто витрачений та вимушений «гігантами» Intel, AMD, IBM тощо.

Архітектура процесора - це лише один приклад, який впливає на те, наскільки легко чи наскільки важко мати високопродуктивну реалізацію мови. Таку мову, як C, C ++, D, Rust, яка добре підходить для сучасної основної моделі програмування процесора, буде простіше зробити швидше, ніж мова, яка повинна "боротися" і обійти процесор, як Java, ECMAScript, Python, Ruby , PHP.

Дійсно, це все питання грошей. Якщо ви витрачаєте рівні кошти на розробку високопродуктивного алгоритму в ECMAScript, високопродуктивну реалізацію ECMAScript, високопродуктивну операційну систему, розроблену для ECMAScript, високопродуктивний процесор, розроблений для ECMAScript, який був витрачений протягом останнього Десятиліття, щоб швидко подібні мови проходили швидко, ви, швидше за все, побачите рівну продуктивність. Просто в цей час було витрачено набагато більше грошей, роблячи C-подібні мови швидшими, ніж швидкісні мови, подібні до ECMAScript, і припущення, що схожі на С мови, перекладаються на весь стек від MMU та процесорів до операційних систем і системи віртуальної пам'яті до бібліотек і фреймворків.

Особисто мені найбільше знайомий Рубі (який, як правило, вважається "повільною мовою"), тому я наведу два приклади: Hashклас (одна з центральних структур даних у Ruby, словник ключових значень) у Rubinius Реалізація Ruby написана на 100% чистому Ruby, і вона має приблизно таку ж продуктивність, як іHashклас у YARV (найбільш широко використовувана реалізація), написаний на C. І є бібліотека маніпуляцій із зображеннями, написана як розширення C для YARV, яка також має (повільну) чисту Ruby "резервну версію" для реалізацій, які не 't підтримка C, яка використовує тонну високодинамічних та рефлексивних трюків Ruby; експериментальна гілка JRuby, використовуючи структуру інтерпретатора TST Truffle та рамку компіляції Graal JIT від Oracle Labs, може виконати цю чисту Ruby "резервну версію" так само швидко, як YARV може виконати оригінальну високооптимізовану версію C. Це просто (ну, що завгодно) досягається деякими дійсно розумними людьми, які роблять дійсно розумні речі з динамічними оптимізаціями виконання, компіляцією JIT та частковою оцінкою.


4
Хоча технічно мови не є по суті швидкими, деякі мови мають більше уваги до того, щоб програміст міг робити швидкий код. C в першу чергу оптимізовано для процесорів, а не навпаки. Наприклад, C вибрав фіксований розмір ints з міркувань продуктивності, незважаючи на те, що необмежені цілі числа, такі як ті, які використовує Python, є набагато більш математично природними. Реалізація необмежених цілих чисел у апаратному забезпеченні не буде такою швидкою, як цілі цілі фіксованого розміру. Мови, які намагаються приховати деталі реалізації, потребують складної оптимізації, щоб наблизитися до наївних реалізацій C.
gmatht

1
C має історію - вона була створена для того, щоб зробити систему, написану мовою складання, переносною для інших систем. Тому первісною метою було створення «портативного асемблера» для Unix, і він був дуже добре розроблений. Це було так добре з 1980-1995-х років, що він мав критичну масу, коли Windows 95 з'явилася.
Thorbjørn Ravn Andersen

1
Я б не погодився з коментарем щодо ресурсів. Важлива не кількість людей у ​​команді, це рівень майстерності кращих людей у ​​команді.
Майкл Кей

"Microsoft, ймовірно, працює більше людей, які готують каву для своїх програмістів-компіляторів, ніж у всій спільноті PHP, Ruby та Python є люди, які працюють над своїми віртуальними машинами". Я думаю, це залежить від того, наскільки ви готові розтягнути термін "програміст-компілятор" і скільки ви включаєте з цим (Microsoft розробляє багато компіляторів). Наприклад, лише команда компілятора VS C ++ є відносно невеликою AFAIK.
Кубік

7

Теоретично, якщо ви пишете код, який робить абсолютно те саме двома мовами і компілюєте обидва, використовуючи "ідеальний" компілятор, продуктивність обох повинна бути однаковою.

На практиці є кілька причин, через які продуктивність буде різною:

  1. Деякі мови важче оптимізувати.

    Це включає особливості, які роблять код більш динамічним (динамічне введення тексту, віртуальні методи, покажчики функцій), але також, наприклад, збирання сміття.

    Існують різні способи швидкого використання коду за допомогою таких функцій, але він як і раніше закінчується хоча б дещо повільніше, ніж без їх використання.

  2. Деякі мовні реалізації повинні виконувати деякі компіляції під час виконання.

    Особливо це стосується мов з віртуальними машинами (наприклад, Java) та мов, які виконують вихідний код, без проміжного кроку для двійкового (наприклад, JavaScript).

    Такі мовні реалізації повинні зробити більше роботи під час виконання, що впливає на продуктивність, особливо час запуску / продуктивність незабаром після запуску.

  3. Мовні реалізації навмисно витрачають менше часу на оптимізацію, ніж могли.

    Усі компілятори дбають про роботу самого компілятора, а не лише про створений код. Це особливо важливо для компіляторів виконання (JIT компіляторів), де занадто багато часу для компіляції уповільнює виконання програми. Але це стосується дострокових компіляторів, як і для C ++. Наприклад, розподіл регістрів є повною проблемою, тому його не реально вирішити ідеально, а натомість використовується евристика, що виконується в розумні строки.

  4. Відмінності ідіом у різних мовах.

    Код, написаний у загальноприйнятому для певної мови стилі (ідіоматичний код) із використанням загальних бібліотек, може призвести до різниці в продуктивності, оскільки такий ідіоматичний код поводиться тонко по-різному в кожній мові.

    Як приклад розглянемо vector[i]в C ++, list[i]у C # та list.get(i)на Java. Код C ++, ймовірно, не перевіряє діапазон і не виконує віртуальних викликів, код C # здійснює перевірку діапазону, але немає віртуальних викликів і код Java виконує перевірку діапазону, і це віртуальний виклик. Всі три мови підтримують віртуальні та невіртуальні методи, і C ++ і C # можуть включати перевірку діапазону або уникати цього при доступі до базового масиву. Але стандартні бібліотеки цих мов вирішили писати ці еквівалентні функції по-різному, і, як наслідок, їх ефективність буде різною.

  5. У деяких компіляторах можуть бути відсутні деякі оптимізації.

    У авторів-компіляторів є обмежені ресурси, тому вони не можуть реалізувати всі можливі оптимізації, навіть ігноруючи інші проблеми. А ресурси, які вони мали, можуть бути зосереджені на одній області оптимізації більше, ніж на інших. Як наслідок, код, написаний різними мовами, може мати різну продуктивність через відмінності у їхніх компіляторах, навіть якщо немає принципових причин, чому одна мова повинна бути швидшою або навіть легшою для оптимізації, ніж інша.


Деяка мова не має компілятора. Для деяких мов компілятор ( посилання ) не може бути .
Рафаель

3
Для деяких мов не може бути статичного компілятора. Динамічна компіляція не впливає на нерозбірливість статичних властивостей.
Йорг W Міттаг

Якщо мови досить різні, у них немає "коду, який робить точно так само". У них може бути код, який створює той самий вихід або поведінку, яку можна спостерігати користувачеві, і це не одне і те ж. Тож я не згоден з вашою умовою.
einpoklum

@einpoklum Я не бачу різниці. За деякими винятками (наприклад, синхронізація з ниткою), які, на мою думку, тут не цікаві, мовні специфікації зазвичай дозволяють оптимізувати, що зберігає поведінку, що спостерігається. Яку поведінку ви маєте на увазі, яка не піддається спостереженню користувача, але її неможливо оптимізувати?
svick

Теоретично, будь-яку інтерпретовану мову можна "скласти", створивши файл EXE, що складається з інтерпретатора + вихідний код програми.
dan04

1

Це дуже загальне питання, але у вашому випадку відповідь може бути простим. C ++ компілюється в машинний код, де Java збирає в байтовий код Java, який потім запускається віртуальною машиною Java (хоча існує також і своєчасна компіляція, яка додатково компілює байтовий код Java до рідного машинного коду). Ще одна відмінність може полягати в збиранні сміття - це послуга, яку надає тільки Java.


2
Це занадто спрощена відповідь. Є налаштування, в яких Java швидша.
Рафаель

4
Існують також реалізації Java, які компілюють в машинний код та інтерпретують реалізацію C ++. І що таке "машинний код" у будь-якому випадку, якщо у мене є процесор picoJava, який виконує байт-код JVM на самому собі, і у мене інтерпретатор JPC x86 працює на JVM, то що робить об'єктний код x86 "машинним кодом" і байт-код JVM ні?
Йорг W Міттаг

2
Nitpick: Не тільки Java забезпечує збирання сміття ... і навіть якщо ви враховуєте лише C ++ та Java, деякі C ++ рамки / бібліотечні програми розміщують засоби для збору сміття.
einpoklum

Компіляція до рідного машинного коду загалом покращує продуктивність. Однак у деяких обмежених випадках високоякісний перекладач може вибити наївного вчасно компілятора.
Пригнічений

@ JörgWMittag Трюк полягає в тому, щоб компілювати до власного машинного коду - машинного коду, який розуміє хост-процесор, а потім безпосередньо перейти до названого коду, щоб його можна було виконати без тлумачення. Це буде x86 на чіпі x86 та байт-кодах JVM на процесорі picoJava.
Депресія

-5

Ваше запитання занадто загальне, тому я боюся, що я не можу дати точну відповідь, яка вам потрібна.

Для найкращого пояснення давайте подивимось на платформи C ++ та .Net.

C ++, дуже близький до машинного коду та однієї з прихильності програмування у старіші часи (як понад 10+ років тому?) Через продуктивність. Для розробки та виконання програми C ++ навіть з IDE не так багато ресурсів, вони вважаються дуже легкими і дуже швидкими. Ви також можете записати код C ++ в консоль і розробити звідти гру. Щодо пам’яті та ресурсів, я приблизно забув, яку потужність займає комп'ютер, але ємність - це те, що ви не можете порівняти з поточним поколінням мови програмування.

Тепер давайте розглянемо .Net. Обов’язковою умовою розробки .Net є одна гігантська IDE, яка складається не лише з одного типу мов програмування. Навіть якщо ви просто хочете розробника, скажімо, C #, сам IDE включає за замовчуванням багато платформ програмування, такі як J #, VB, mobile та ін. Якщо ви не власноруч встановите і встановите лише те, що вам потрібно, за умови, що у вас є достатній досвід, щоб грати з установкою IDE

Крім установки самого програмного забезпечення IDE.

Розвиток в .Net може бути цікавим досвідом, оскільки багато спільних функцій та компонентів доступні за замовчуванням. Ви можете перетягувати і використовувати і використовувати багато методів перевірки, читання файлів, доступ до бази даних та багато іншого в IDE.

Як результат, це компроміс між комп’ютерними ресурсами та швидкістю розвитку. Ці бібліотеки та рамки займають пам'ять та ресурси. Коли ви виконуєте програму в .Net IDE, це може зайняти багато часу, намагаючись зібрати бібліотеки, компоненти та всі ваші файли. А під час налагодження комп'ютер вимагає багато ресурсів для управління процесом налагодження у вашому IDE.

Зазвичай для розробки .Net потрібен хороший комп'ютер з деяким процесором Ram і процесором. В іншому випадку ви можете взагалі не програмувати. У цьому аспекті платформа C ++ значно краща за .Net. Хоча вам все-таки потрібен хороший комп'ютер, але його потужність не буде надто турбувати порівняно з .Net.

Сподіваюся, моя відповідь могла допомогти у вашому запитанні. Повідомте мене, якщо ви хочете дізнатися більше.

Редагувати:

Лише уточнення мого основного моменту я в основному відповідаю на питання "Що регулює швидкість програмування".

У точки зору IDE використання мови C ++ або .Net у відносному IDE не впливає на швидкість написання коду, але це вплине на швидкість розробки, тому що компілятору Visual Studio потрібно більше часу для виконання програми, але C ++ IDE набагато легше і споживати менше комп’ютерних ресурсів. Таким чином, у перспективі ви можете програмувати швидше за допомогою C ++ типу IDE порівняно з .Net IDE, що сильно залежить від бібліотеки та основи. Якщо ви забираєте час очікування IDE на запуск, компіляцію, виконання програми тощо, це вплине на швидкість програмування. Якщо "швидкість програмування" насправді зосереджена лише на "швидкості написання коду".

Кількість бібліотек та фреймворку у Visual Studio також споживає ємність комп'ютера. Я не впевнений, чи відповідає це на питання "управління пам'яттю", але я просто хочу зазначити, що Visual Studio IDE може зайняти багато ресурсів при його запуску, і тим самим уповільнити загальну "швидкість програмування". Звичайно, це не має нічого спільного з "швидкістю написання коду".

Як ви могли здогадатися, я не знаю занадто багато C ++, оскільки я просто використовую його як приклад, моя головна думка про тип важких IDE для Visual Studio, що споживають комп'ютерні ресурси.

Якщо я отримав ідею і взагалі не відповів на запитання про початковий потік, вибачте за довгий пост. Але я б порадив початковому ланцюжку, щоб зрозуміти питання і точно запитати, що він / вона повинен знати про "швидше" та "повільніше"


6
Тільки для запису, для розробки .NET потрібен лише .NET SDK (а для виконання - сам час виконання .NET). Можна скористатися редактором простого тексту та викликати компілятор з командного рядка. IDE (я вважаю, що ви посилаєтесь на Visual Studio) НЕ потрібен, хоча він точно допомагає у будь-якому масштабному проекті (Intellisense, налагоджувач тощо) тощо. Також є більш легкі IDE, такі як SharpDevelop з меншими дзвіночками.
MetalMikester

16
Ви, безумовно, можете розвиватися в .Net без IDE. Але я не бачу, наскільки IDE має відношення до швидкості коду, записаної мовою.
svick

8
@MetalMikester: І звичайно, Visual Studio - це ідеальний IDE для розробки C ++ у Windows.
Йорг W Міттаг

3
І компіляція C ++ до .Net-коду - це лише один перемикач компілятора; передбачувана прірва - це мости одним натисканням миші у візуальній студії.
MSalters

1
Я зовсім не впевнений, що назвав би сучасний C ++ "дуже близьким до машинного коду". Оригінал C, так; він був задуманий як портативний асемблер і залишився дуже близьким до цього (хоча стандартна бібліотека зросла, і різні компілятори пропонують додатки до належної мови, що відводить її далі від її витоків). Оригінальний C ++ (C З класами), можливо; переписання варіанту C, що включає класи на чистий C, не так складно, і тоді застосовується попереднє обговорення. Але сучасний C ++, із шаблонами, поліморфізмом та всім іншим під сонцем? Це навряд чи "дуже близький до машинного коду".
CVn
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.