Які прості методи ви використовуєте для підвищення продуктивності?


21

Я говорю про те, як ми пишемо прості підпрограми з метою поліпшення продуктивності, не ускладнюючи читання вашого коду ... наприклад, це типове для ми дізналися:

for(int i = 0; i < collection.length(); i++ ){
   // stuff here
}

Але я зазвичай роблю це, коли а foreachне застосовується:

for(int i = 0, j = collection.length(); i < j; i++ ){
   // stuff here
}

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


34
+1 лише для того, щоб мати дівчину, яка скаже вам, коли ваш код не зрозумілий.
Крісто,

76
Ви просто публікуєте це, щоб сказати нам, що у вас є дівчина.
Josh K

11
@Christian: Не забувайте, що існують оптимізації компілятора, які можуть зробити це для вас, щоб ви могли лише впливати на читабельність і зовсім не впливати на продуктивність; передчасна оптимізація - корінь усього зла ... Намагайтеся уникати більше однієї декларації чи завдання в одному рядку, не змушуйте людей читати її двічі ... Вам слід скористатися звичайним способом (ваш перший приклад) або поставити друге оголошення поза циклом for (хоча це також зменшує читабельність, оскільки вам потрібно буде прочитати назад, щоб побачити, що означає j).
Тамара Війсман

5
@TomWij: Правильна (і повна) цитата: "Ми повинні забути про малу ефективність, скажімо, про 97% часу: передчасна оптимізація - корінь усього зла. Але ми не повинні пропускати свої можливості в цих критичних 3%. "
Роберт Харві

3
@tomwij: Якщо ви витрачаєте три відсотки, то за визначенням ви повинні робити це в критичному для часу коді, а не витрачати свій час на інші 97%.
Роберт Харві

Відповіді:


28

вставити передчасну дискусію-це-корінь-все-лихої лекції

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

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

Знай свою велику-О

Це, мабуть, має бути об'єднане у тривалу дискусію вище. Це досить здоровий глузд, що цикл всередині циклу, де внутрішня петля повторює обчислення, буде повільніше. Наприклад:

for (i = 0; i < strlen(str); i++) {
    ...
}

Це займе жахливу кількість часу, якщо рядок дійсно довгий, тому що довжина перераховується під час кожної ітерації циклу. Зауважте, що GCC фактично оптимізує цей випадок, оскільки strlen()позначений як чиста функція.

При сортуванні мільйона 32-бітних цілих чисел сортування міхурів було б неправильним шляхом . Взагалі, сортування може бути виконано за часом O (n * log n) (або, краще, у разі радіусного сортування), тому, якщо ви не знаєте, що ваші дані будуть невеликими, шукайте алгоритм, який принаймні O (n * журнал n).

Так само, працюючи з базами даних, будьте в курсі індексів. Якщо у вас немає SELECT * FROM people WHERE age = 20, а у вас немає індексу людей (вік), знадобиться O (n) послідовне сканування, а не набагато швидше O (log n) сканування індексу.

Цілісна арифметична ієрархія

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

  • + - ~ & | ^
  • << >>
  • *
  • /

Звичайно, компілятор зазвичай Оптимізувати речі , як n / 2до n >> 1автоматично , якщо ви орієнтуєтеся на основний комп'ютер, але якщо ви орієнтуєтеся вбудований пристрій, ви можете не отримати таку розкіш.

Також % 2і & 1мають різну семантику. Ділення та модуль зазвичай округляються до нуля, але це визначено реалізацією. Добре >>і &завжди кругляє до негативної нескінченності, що (на мій погляд) має набагато більше сенсу. Наприклад, на моєму комп’ютері:

printf("%d\n", -1 % 2); // -1 (maybe)
printf("%d\n", -1 & 1); // 1

Отже, використовуйте те, що має сенс. Не думай, що ти хороший хлопчик, використовуючи, % 2коли ти спочатку збирався писати & 1.

Дорогі операції з плаваючою комою

Уникайте великих операцій з плаваючою комою, таких як pow()і log()в коді, який їм насправді не потрібен, особливо при роботі з цілими числами. Візьмемо, наприклад, читання числа:

int parseInt(const char *str)
{
    const char *p;
    int         digits;
    int         number;
    int         position;

    // Count the number of digits
    for (p = str; isdigit(*p); p++)
        {}
    digits = p - str;

    // Sum the digits, multiplying them by their respective power of 10.
    number = 0;
    position = digits - 1;
    for (p = str; isdigit(*p); p++, position--)
        number += (*p - '0') * pow(10, position);

    return number;
}

Це не тільки використання pow()int<-> doubleперетворення , необхідне для використання його) досить дорого, але це створює можливість для втрати точності (до речі, наведений вище код не має проблем з точністю). Ось чому я хотіла, коли бачу цей тип функції, що використовується в нематематичному контексті.

Також зауважте, як алгоритм "розумного" нижче, який множиться на 10 на кожній ітерації, насправді більш стислий, ніж код, наведений вище:

int parseInt(const char *str)
{
    const char *p;
    int         number;

    number = 0;
    for (p = str; isdigit(*p); p++) {
        number *= 10;
        number += *p - '0';
    }

    return number;
}

Дуже ретельна відповідь.
Paddyslacker

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

Зауважте, що GCC фактично оптимізує цей випадок, оскільки strlen () позначений як чиста функція. Я думаю, ти маєш на увазі, що це функція const, а не чиста.
Енді Лестер

@Andy Lester: Насправді я мав на увазі чистий. У документації GCC вказується, що const трохи суворіший, ніж чистий, оскільки функція const не може читати глобальну пам'ять. strlen()вивчає рядок, на який вказує його аргумент вказівника, тобто не може бути const. Крім того, strlen()дійсно позначений як чистий у глібціstring.h
Джої Адамс

Ти маєш рацію, моя помилка, і я повинен був перевірити ще раз. Я працюю над проектом Parrot, коментуючи функції, як pureабо, так constі навіть задокументував це у файлі заголовка через тонку різницю між ними. docs.parrot.org/parrot/1.3.0/html/docs/dev/c_functions.pod.html
Енді Лестер

13

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

Я фанат філософії Кента Бека :

"Зробіть це, зробіть це правильно, зробіть це швидко".

Моя техніка для підвищення продуктивності коду - це спочатку отримати код, який проходить одиничні тести та добре враховується, а потім (особливо для циклічних операцій) написати одиничний тест, який перевіряє продуктивність, а потім перефактурує код або придумає інший алгоритм, якщо один я ' Вибраний не працює, як очікувалося.

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

Використовуючи щось на зразок атрибута тайм-ауту NUnit з прикладом коду, який ви подали (і велику кількість ітерацій циклу), ви могли насправді довести, чи дійсно ваше «поліпшення» коду допомогло у виконанні цього циклу.

Одне застереження: Хоча це ефективно на "мікро" рівні, це, звичайно, не єдиний спосіб перевірити продуктивність і не враховувати проблеми, які можуть виникнути на рівні "макро" - але це хороший початок.


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

Не обов’язково потрібні одиничні тести, але завжди варто витратити ці 20 хвилин, щоб перевірити, чи є якийсь міф про ефективність правдивим чи ні, тим більше, що відповідь часто залежить від компілятора та стану прапора -O і -g (або Debug / Відпустіть у разі ВС).
mbq

+1 Ця відповідь доповнює мій відповідний коментар до самого питання.
Тамара Війсман

1
@AShelly: якщо ми говоримо про прості переформулювання синтаксису циклу, змінити його після цього дуже просто. Крім того, те, що ви однаково читаєте, може бути не таким для інших програмістів. Найкраще максимально використовувати «стандартний» синтаксис і змінювати лише тоді, коли доведено, що це необхідно.
Joeri Sebrechts

@AShelly напевно, якщо ви можете придумати два не менш читаних методу, і ви вибрали менш ефективний, який ви просто не виконуєте свою роботу? Хтось насправді це зробив би?
Гленатрон

11

Майте на увазі, що ваш компілятор цілком може отримати:

for(int i = 0; i < collection.length(); i++ ){
   // stuff here
}

в:

int j = collection.length();
for(int i = 0; i < j; i++ ){
   // stuff here
}

або щось подібне, якщо collectionвоно не змінюється над циклом.

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

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

Зі сторони: якщо ви змінюєте collectionвсередині циклу, додаючи або видаляючи елементи (так, я знаю, що це погана ідея, але це трапляється), то ваш другий приклад або не переверне всі елементи, або спробує отримати доступ до минулого кінець масиву.


1
Чому б просто не зробити це явно?

3
На деяких мовах, які перевіряють межі, ви повільно знизите свій код, якщо це буде зроблено явно. З циклом для collection.length компілятор переміщує його для вас і не допускає перевірки меж. З циклом на деяку константу з інших місць у вашій програмі, ви будете перевіряти межі кожної ітерації. Тому важливо виміряти - інтуїція щодо продуктивності майже ніколи не підходить.
Кейт Григорій

1
Ось чому я сказав "варто було б дізнатися".
ChrisF

Як компілятор C # може знати, що collection.length () не змінює колекцію, як це робить stack.pop ()? Я думаю, що було б краще перевірити ІЛ, а не припускати, що компілятор це оптимізує. У C ++ ви можете позначити метод як const ("не змінює об'єкт"), тому компілятор може зробити цю оптимізацію безпечно.
JBRWilkinson

1
Оптимізатори @JBRW, які роблять це, також знають про методи "колекцій", що називаються, називають-це-простості-навіть-хоча-це-не-С ++. Зрештою, ви можете лише перевірити межі, чи можете ви помітити, що щось є колекцією та знаєте, як отримати її довжину.
Кейт Григорій

9

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


1
Подаруйте їй книгу про програмування;)
Joeri Sebrechts

1
+1, оскільки більшість наших подруг, швидше за все, більше зацікавлені в Леді Газі, ніж чіткість коду.
гаплоїдний

Чи можете ви пояснити, чому це не рекомендується?
Macneil

@macneil добре ... цей трюк робить коди не такими поширеними і повністю не працюють, цей фрагмент оптимізації повинен робити компілятор.
тактот

@macneil, якщо ви працюєте мовою вищого рівня, подумайте на тому ж рівні.
тактот

3

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

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

В якості конкретного прикладу для нашого поточного процесора гілки спорожняють конвеєр, затримуючи процесор на 8 циклів. Компілятор приймає цей код:

 bool isReady = (value > TriggerLevel);

і перетворює його в збірний еквівалент

isReady = 0
if (value > TriggerLevel)
{
  isReady = 1;
}

Це займе 3 циклу, або 10, якщо він перестрибне isReady=1;. Але процесор має одноциклічну maxінструкцію, тому набагато краще написати код для створення цієї послідовності, яка гарантовано завжди триватиме три цикли:

diff = value-TriggerLevel;
diff = max(diff, 0);
isReady = min(1,diff);

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

#define BOOL_GT(a,b) min(max((a)-(b),0),1)

//isReady = value > TriggerLevel;
isReady = BOOL_GT(value, TriggerLevel);

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


3

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

Наприклад, у C # компілятор буде оптимізувати код, якщо ви циклічате довжину масиву, оскільки він знає, що йому не потрібно діапазону перевіряти індекс, коли ви отримуєте доступ до масиву. Якщо ви спробуєте оптимізувати його, поставивши довжину масиву в змінну, ви перервете зв'язок між циклом і масивом, і фактично зробите код набагато повільніше.

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


3

У мене дуже проста техніка.

  1. Я змушую свій код працювати.
  2. Я тестую це на швидкість.
  3. Якщо це швидко, я повертаюсь до кроку 1 для ще однієї функції. Якщо це повільно, я профілюю це, щоб знайти вузьке місце.
  4. Я виправляю вузьке місце. Поверніться до кроку 1.

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


2

Скористайтеся коротким замиканням:

if(someVar || SomeMethod())

для кодування потрібно стільки ж часу, скільки читається, як:

if(someMethod() || someVar)

але це буде оцінюватися швидше з часом.


1

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


6
Е-е ... А як щодо ефективності, яку бачать ваші клієнти? Ви достатньо заможні, щоб також купувати нові комп’ютери для них?
Роберт Харві

2
І ми майже потрапили у стіну виступу; багатоядерні обчислення - єдиний вихід, але очікування не змусить ваші програми використовувати його.
mbq

+1 Ця відповідь доповнює мій відповідний коментар до самого питання.
Тамара Війсман

3
Жоден час програмування не дорожчий за апаратний, коли у вас є тисячі або мільйони користувачів. Час програміста НЕ важливіше, ніж час користувача, отримуйте це через голову якомога швидше.
HLGEM

1
Займіться хорошими звичками, тоді програміст не потребує часу, як це ви робите весь час.
Домінік МакДоннелл

1

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

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

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

Щоправда, до вашого сенсу, я вважаю, що багато разів роблячи булеві протилежні підходу за замовчуванням, іноді допомагає:

for(int i = 0, j = collection.length(); i < j; i++ ){
// stuff here
}

може стати

for(int i = collection.length(); i > 0; i-=1 ){
// stuff here
}

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

наприклад, c #:

        string[] collection = {"a","b"};

        string result = "";

        for (int i = 0, j = collection.Count() - 1; i < j; i++)
        {
            result += collection[i] + "~";
        }

можна також записати як:

        for (int i = collection.Count() - 1; i > 0; i -= 1)
        {
            result = collection[i] + "~" + result;
        }

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

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

Єдиний інший по-справжньому загальний випадок, який я б міг застосувати скрізь, - застосувати деяку булеву алгебру до складних умовних умов, щоб спробувати перетворити рівняння на щось таке, що має кращі шанси скористатися умовою короткого замикання або перетворити складний набір вкладених висловлювань if-then або case в рівняння цілком. Жоден із них не працює у всіх випадках, але вони можуть значно економити час.


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

Але, повернувши індекс, сама ітерація може стати більш складною.
Тамара Війсман

@stijn Я думав про c #, коли я це писав, але, можливо, ця пропозиція також потрапляє в категорію конкретної мови з цієї причини - див. редагувати ... @ToWij звичайно, я не думаю, що є багато, якщо є якісь пропозиції такого характеру які не ризикують цим. Якщо ваш // матеріал був якоюсь маніпуляцією зі стеком, можливо, навіть неможливо правильно змінити логіку, але в багатьох випадках це є і не надто заплутано, якщо це робити обережно в більшості випадків IMHO.
Білл

ти правий; у C ++ я все-таки віддаю перевагу циклу "нормального", але з викликом length (), виведеним із ітерації (як у const size_t len ​​= collection.length (); for (size_t i = 0; i <len; ++ i) {}) з двох причин: я вважаю, що "звичайний" цикл перерахунку вперед є більш читабельним / зрозумілим (але це, мабуть, тільки тому, що він є більш поширеним), і він займає виклик циклу з інваріантною довжиною ().
Стейн

1
  1. Профіль. У нас навіть є проблеми? Де?
  2. У 90% випадків, коли це якимось чином пов'язане з IO, застосуйте кешування (і, можливо, отримайте більше пам'яті)
  3. Якщо це пов'язано з процесором, застосуйте кешування
  4. Якщо продуктивність все ще є проблемою, ми залишили сферу простих прийомів - займайтеся математикою.

1

Використовуйте найкращі інструменти, які ви можете знайти - хороший компілятор, хороший профілер, хороші бібліотеки. Налаштуйте алгоритми правильно, а ще краще - використовуйте потрібну бібліотеку, щоб зробити це за вас. Тривіальна оптимізація циклу - це маленька картопля, плюс ви не такі розумні, як компілятор оптимізації.


1

Для мене найпростішим є використання стека, коли це можливо, коли звичайний випадок використання випадків відповідає діапазону, скажімо, [0, 64), але має рідкісні випадки, які не мають невеликої верхньої межі.

Простий приклад C (раніше):

void some_hotspot_called_in_big_loops(int n, ...)
{
    // 'n' is, 99% of the time, <= 64.
    int* values = calloc(n, sizeof(int));

    // do stuff with values
    ...
    free(values);
}

І після:

void some_hotspot_called_in_big_loops(int n, ...)
{
    // 'n' is, 99% of the time, <= 64.
    int values_mem[64] = {0}
    int* values = (n <= 64) ? values_mem: calloc(n, sizeof(int));

    // do stuff with values
    ...
    if (values != values_mem)
        free(values);
}

Я узагальнив це так, оскільки такі види гарячих точок багато в процесі формування профілю:

void some_hotspot_called_in_big_loops(int n, ...)
{
    // 'n' is, 99% of the time, <= 64.
    MemFast values_mem;
    int* values = mf_calloc(&values_mem, n, sizeof(int));

    // do stuff with values
    ...

    mf_free(&values_mem);
}

Вищезазначене використовує стек, коли дані, що виділяються, є досить малими в цих 99,9% випадків, і він використовує купу в іншому випадку.

У C ++ я узагальнив це стандартною сумісною невеликою послідовністю (подібно до SmallVectorреалізацій там), яка обертається навколо тієї ж концепції.

Це не епічна оптимізація (я скоротив, скажімо, 3 секунди, щоб операція завершилася до 1,8 секунди), але для цього потрібні такі банальні зусилля. Коли ви можете трохи знизитись від 3 секунд до 1,8 секунди, просто ввівши рядок коду і змінивши два, це дуже непоганий удар для такого невеликого долара.


0

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

Просто швидкий зразок підвищення ефективності для SQL Server: Використовуйте відповідні індекси, уникайте курсорів - використовуйте задану логіку, використовуйте зручні місця, де клаузи, не накопичуйте перегляди зверху, не повертайте більше даних, ніж вам потрібно чи більше стовпчиків, ніж вам потрібно, не використовуйте відповідні підзапити.


0

Якщо це C ++, вам слід звикнути, ++iа не i++. ++iніколи не буде гірше, це означає точно так само, як окреме твердження, а в деяких випадках це може бути підвищення продуктивності.

Змінювати існуючий код не варто на той випадок, що він допоможе, але це хороша звичка потрапляти.


0

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

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

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

Можливо, ви зможете дещо виправити ці проблеми без великих змін у коді.

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

Те, як ви пишете циклі, насправді не має нічого спільного.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.