Чому читання рядків із stdin набагато повільніше в C ++, ніж у Python?


1839

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


(Відповідь TLDR: включити оператор: cin.sync_with_stdio(false)або просто використовувати fgetsзамість цього.

Результати TLDR: прокрутіть до кінця мого питання і подивіться на таблицю.)


Код C ++:

#include <iostream>
#include <time.h>

using namespace std;

int main() {
    string input_line;
    long line_count = 0;
    time_t start = time(NULL);
    int sec;
    int lps;

    while (cin) {
        getline(cin, input_line);
        if (!cin.eof())
            line_count++;
    };

    sec = (int) time(NULL) - start;
    cerr << "Read " << line_count << " lines in " << sec << " seconds.";
    if (sec > 0) {
        lps = line_count / sec;
        cerr << " LPS: " << lps << endl;
    } else
        cerr << endl;
    return 0;
}

// Compiled with:
// g++ -O3 -o readline_test_cpp foo.cpp

Еквівалент Python:

#!/usr/bin/env python
import time
import sys

count = 0
start = time.time()

for line in  sys.stdin:
    count += 1

delta_sec = int(time.time() - start_time)
if delta_sec >= 0:
    lines_per_sec = int(round(count/delta_sec))
    print("Read {0} lines in {1} seconds. LPS: {2}".format(count, delta_sec,
       lines_per_sec))

Ось мої результати:

$ cat test_lines | ./readline_test_cpp
Read 5570000 lines in 9 seconds. LPS: 618889

$cat test_lines | ./readline_test.py
Read 5570000 lines in 1 seconds. LPS: 5570000

Слід зазначити, що я спробував це як під Mac OS X v10.6.8 (Snow Leopard), так і під Linux 2.6.32 (Red Hat Linux 6.2). Перший - це MacBook Pro, а останній - дуже приємний сервер, не те, що це занадто доречно.

$ for i in {1..5}; do echo "Test run $i at `date`"; echo -n "CPP:"; cat test_lines | ./readline_test_cpp ; echo -n "Python:"; cat test_lines | ./readline_test.py ; done
Test run 1 at Mon Feb 20 21:29:28 EST 2012
CPP:   Read 5570001 lines in 9 seconds. LPS: 618889
Python:Read 5570000 lines in 1 seconds. LPS: 5570000
Test run 2 at Mon Feb 20 21:29:39 EST 2012
CPP:   Read 5570001 lines in 9 seconds. LPS: 618889
Python:Read 5570000 lines in 1 seconds. LPS: 5570000
Test run 3 at Mon Feb 20 21:29:50 EST 2012
CPP:   Read 5570001 lines in 9 seconds. LPS: 618889
Python:Read 5570000 lines in 1 seconds. LPS: 5570000
Test run 4 at Mon Feb 20 21:30:01 EST 2012
CPP:   Read 5570001 lines in 9 seconds. LPS: 618889
Python:Read 5570000 lines in 1 seconds. LPS: 5570000
Test run 5 at Mon Feb 20 21:30:11 EST 2012
CPP:   Read 5570001 lines in 10 seconds. LPS: 557000
Python:Read 5570000 lines in  1 seconds. LPS: 5570000

Додаток та резюме крихітного базового еталону

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

Implementation      Lines per second
python (default)           3,571,428
cin (default/naive)          819,672
cin (no sync)             12,500,000
fgets                     14,285,714
wc (not fair comparison)  54,644,808

14
Ви кілька разів проводили тести? Можливо, є проблема кеш-диска.
— Вон Катон,

9
@JJC: Я бачу дві можливості (якщо припустити, що ви усунули проблему кешування, запропоновану Девідом): 1) <iostream>продуктивність відмовляється. Не перший раз це трапляється. 2) Python досить розумний, щоб не копіювати дані в цикл for, тому що ви їх не використовуєте. Ви можете повторно спробувати використати scanfта char[]. Крім того, ви можете спробувати переписати цикл, щоб щось було зроблено з рядком (наприклад, збережіть 5-ту букву і з'єднайте її в результаті).
— JN

15
Проблема - синхронізація зі stdio - дивіться мою відповідь.
— Вон Катон

19
Оскільки, схоже, ніхто не згадував, чому ви отримуєте додаткову лінійку на C ++: Не тестуйте cin.eof()!! Покладіть getlineвиклик у вислів "якщо".
— Xeo

21
wc -lшвидко, тому що він читає потік одночасно більше ніж один рядок (це може бути fread(stdin)/memchr('\n')комбінація). Результати Python в тому ж порядку за величиною, наприклад,wc-l.py
— jfs

Відповіді:


1644

За замовчуванням cinсинхронізується зі stdio, що змушує уникати буферизації вхідних даних. Якщо ви додасте це до початку вашої основної, ви повинні побачити набагато кращі показники:

std::ios_base::sync_with_stdio(false);

Зазвичай, коли вхідний потік буферизований, замість того, щоб читати по одному символу, потік читатиметься більшими шматками. Це зменшує кількість системних дзвінків, які зазвичай є досить дорогими. Однак, оскільки в FILE*основі stdioі iostreamsчасто є окремі реалізації та, отже, окремі буфери, це може призвести до проблеми, якщо обидва використовуються разом. Наприклад:

int myvalue1;
cin >> myvalue1;
int myvalue2;
scanf("%d",&myvalue2);

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

Щоб цього уникнути, потоки синхронізуються за замовчуванням stdio. Один загальний спосіб досягти цього - cinчитати кожного персонажа по одному за потребою, використовуючиstdio функцій. На жаль, це вносить багато накладних витрат. За невеликих обсягів введення це не є великою проблемою, але коли ви читаєте мільйони рядків, покарання за ефективність є суттєвим.

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


142
Це має бути вгорі. Це майже напевно правильно. Відповідь не може полягати в заміні прочитаного на fscanfдзвінок, оскільки це просто не робить стільки роботи, як це робить Python. Python повинен виділити пам'ять для рядка, можливо, кілька разів, оскільки існуючий розподіл вважається недостатнім - точно так само, як і для C ++ підходу std::string. Це завдання майже напевно пов'язане з введенням / виводом, і є занадто багато FUD навколо витрат на створення std::stringоб'єктів в C ++ або використання <iostream>в собі.
— Карл Кнечтел

51
Так, додавши цей рядок безпосередньо над моїм оригіналом, поки цикл проскочив код, щоб перевершити навіть python. Я збираюся опублікувати результати як остаточне редагування. Знову дякую!
— JJC

6
Так, це стосується також cout, cerr та clog.
— Вон Катон

2
Щоб швидше зробити cout, cin, cerr та clog, зробіть це таким чином std :: ios_base :: sync_with_stdio (false);
— 01100110

56
Зауважте, що sync_with_stdio()це статична функція члена, і виклик цієї функції на будь-якому об'єкті потоку (наприклад cin) вмикає або вимикає синхронізацію для всіх стандартних об'єктів iostream.
— Джон Цвінк

170

Я просто з цікавості подивився, що відбувається під кришкою, і я використовував dtruss / strace на кожному тесті.

C ++

./a.out < in
Saw 6512403 lines in 8 seconds.  Crunch speed: 814050

систематичні дзвінки sudo dtruss -c ./a.out < in

CALL                                        COUNT
__mac_syscall                                   1
<snip>
open                                            6
pread                                           8
mprotect                                       17
mmap                                           22
stat64                                         30
read_nocancel                               25958

Пітон

./a.py < in
Read 6512402 lines in 1 seconds. LPS: 6512402

систематичні дзвінки sudo dtruss -c ./a.py < in

CALL                                        COUNT
__mac_syscall                                   1
<snip>
open                                            5
pread                                           8
mprotect                                       17
mmap                                           21
stat64                                         29

159

Я тут кілька років позаду, але:

У "Редагуванні 4/5/6" початкової публікації ви використовуєте конструкцію:

$ /usr/bin/time cat big_file | program_to_benchmark

Це неправильно двома різними способами:

  1. Ви насправді терміни виконання cat, а не ваш орієнтир. Використання процесора 'user' та 'sys' відображається у timeтих cat, що не є вашою орієнтирною програмою. Ще гірше, що "реальний" час також не обов'язково є точним. Залежно від впровадження catта конвеєрів у вашій локальній ОС можливо, що він catзаписує остаточний гігантський буфер і виходить задовго до того, як процес читання закінчить свою роботу.

  2. Використання catнепотрібних і насправді контрпродуктивних; ви додаєте рухомі частини. Якщо ви працювали на досить старій системі (тобто з одним процесором і - у певних поколінь комп'ютерів - введення / виведення швидше, ніж процесор) - сам факт catзапущеної роботи може суттєво покращити результати. Ви також підлягаєте будь-якому буферизації вводу та виводу та іншій обробці cat. (Це, швидше за все, отримає нагороду "Безкорисне використання кота", якби я був Рандалом Шварцом.

Кращою конструкцією буде:

$ /usr/bin/time program_to_benchmark < big_file

У цій заяві саме оболонка відкриває big_file, передаючи його вашій програмі (ну, власне, до timeякої потім виконує вашу програму як підпроцес) як уже відкритий дескриптор файлу. За 100% читання файлів суто відповідальність програми, яку ви намагаєтеся орієнтувати. Це дає вам справжнє прочитання його виконання без помилкових ускладнень.

Я згадаю два можливі, але насправді помилкові, «виправлення», які також можна розглядати (але я «нумерую» їх по-різному, оскільки це не речі, які були неправильні в первісному дописі):

A. Ви можете "виправити" це, призначаючи лише свою програму:

$ cat big_file | /usr/bin/time program_to_benchmark

B. або тимчасом всього трубопроводу:

$ /usr/bin/time sh -c 'cat big_file | program_to_benchmark'

Вони неправильні з тих же причин, що і №2: вони все ще використовують catбез потреби. Я згадую їх з кількох причин:

  • вони більш "природні" для людей, яким не зовсім зручні можливості перенаправлення вводу / виводу оболонки POSIX

  • можуть бути випадки , коли cat це необхідно (наприклад: файл для читання вимагає будь - то привілеї доступу, і ви не хочете , щоб надати цей привілей програми , яка буде протестовані: sudo cat /dev/sda | /usr/bin/time my_compression_test --no-output)

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

Але я кажу це останнє з деяким ваганням. Якщо ми вивчимо останній результат у "Правка 5" -

$ /usr/bin/time cat temp_big_file | wc -l
0.01user 1.34system 0:01.83elapsed 74%CPU ...

- це твердження, що catпід час тесту споживало 74% процесора; і дійсно 1,34 / 1,83 - це приблизно 74%. Можливо, пробіг:

$ /usr/bin/time wc -l < temp_big_file

зайняло б лише решту .49 секунд! Напевно, ні: catтут довелося платити за read()системні дзвінки (або еквівалент), які передавали файл з «диска» (фактично кеш-пам’яті), а також труба пише, щоб доставити їх wc. Правильний тест все-таки повинен був би зробити цеread() дзвінки; збереглись би лише дзвінки "запис на трубу" та "читання з труби", і це було б досить дешево.

Тим НЕ менше, я передбачаю , ви могли б виміряти різницю між cat file | wc -lі wc -l < fileі знайти помітну (2-значний відсоток) різницю. Кожен з повільних тестів заплатив би аналогічний штраф у абсолютний час; що, однак, становило б меншу частку від його більшого загального часу.

Насправді я зробив декілька швидких тестів із 1,5-гігабайтним файлом сміття в системі Linux 3.13 (Ubuntu 14.04), отримавши ці результати (це насправді "найкращі з 3-х" результатів; звичайно після завантаження кеша):

$ time wc -l < /tmp/junk
real 0.280s user 0.156s sys 0.124s (total cpu 0.280s)
$ time cat /tmp/junk | wc -l
real 0.407s user 0.157s sys 0.618s (total cpu 0.775s)
$ time sh -c 'cat /tmp/junk | wc -l'
real 0.411s user 0.118s sys 0.660s (total cpu 0.778s)

Зауважте, що результати двох конвеєрів стверджують, що вони зайняли більше часу CPU (користувач + sys), ніж реальний настінний час. Це тому, що я використовую вбудовану команду 'time' оболонки (bash), яка пізнає конвеєр; і я перебуваю на багатоядерній машині, де окремі процеси в конвеєрі можуть використовувати окремі ядра, накопичуючи час процесора швидше, ніж у режимі реального часу. Використовуючи, /usr/bin/timeя бачу менший час процесора, ніж у режимі реального часу - показує, що він може лише час передавати йому один елемент конвеєра у своєму командному рядку. Також вихід оболонки дає мілісекунди, тоді /usr/bin/timeяк дає лише соті частини секунди.

Тож на рівні ефективності wc -l,cat велика різниця: 409/283 = 1.453 або 45.3% більше в режимі реального часу, і 775/280 = 2.768, або колосальних на 177% більше використовуваних процесорів! У моєму випадковому тестовому вікні це було там.

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

Під час запуску cat big_file | /usr/bin/time my_programваша програма отримує вхід з труби, точно в темпі, що надсилається cat, і шматками не більше, ніж написано cat.

Під час запуску /usr/bin/time my_program < big_fileпрограма отримує відкритий дескриптор файлу фактичного файлу. Ваша програма - або в багатьох випадках бібліотеки вводу-виводу мови, якою вона написана - може вчинити різні дії, коли вона представлена ​​дескриптором файлу, що посилається на звичайний файл. Він може використовувати mmap(2)для відображення вхідного файлу в його адресному просторі, а не використовувати явні read(2)системні виклики. Ці відмінності можуть мати набагато більший ефект на ваших результатних показниках, ніж невелика вартість запуску catдвійкового файлу.

Звичайно, це цікавий орієнтирний результат, якщо одна і та ж програма виконує значно по-різному між двома випадками. Це показує, що дійсно програма або її бібліотеки вводу / виводу є робити що - то цікаве, як використання mmap(). Тож на практиці може бути корисно запустити орієнтири обома способами; можливо, дисконтування catрезультату якимось невеликим фактором, щоб "пробачити" вартість самого запуску cat.


26
Ого, це було досить проникливо! Хоча я усвідомлював, що кішка не потрібна для введення в програму stdin програм, і що <перенаправлення оболонки є кращим, я, як правило, притримувався кота через потік даних зліва направо, що попередній метод візуально зберігає. коли я міркую про трубопроводи. Різниці в роботі в таких випадках я вважаю незначними. Але я дуже ціную, що ви нас виховуєте, Бела.
— JJC

11
Перенаправлення аналізується на командному рядку оболонки на ранньому етапі, що дозволяє зробити одне з них, якщо він надає більш приємний вигляд потоку зліва направо: $ < big_file time my_program $ time < big_file my_program Це має працювати в будь-якій оболонці POSIX (тобто не `csh `і я не впевнений у екзотиці, як` rc`:)
— Бела Любкін

6
Знову ж таки, окрім, мабуть, нецікавої різниці в продуктивності, зумовленої одночасно бінарним котом `cat`, ви відмовляєтесь від можливості тестованої програми змогти mmap () вхідний файл. Це може змінити глибокі результати. Це справедливо навіть у тому випадку, якщо ви самі писали орієнтири на різних мовах, використовуючи лише ідіому «вхідних рядків з файлу». Це залежить від детальної роботи різних їх бібліотек вводу / виводу.
— Бела Любкін

2
Бічна примітка: Вбудований Bash timeвимірює весь трубопровід замість першої програми. time seq 2 | while read; do sleep 1; doneдрукує 2 сек, /usr/bin/time seq 2 | while read; do sleep 1; doneдрукує 0 сек.
— фольклор

1
@folkol - так, << Зауважте, що два конвеєра результати [показують] більше процесора [, ніж] в режимі реального часу [за допомогою вбудованої команди "time" (Bash); ... / usr / bin / time ... може лише час, коли один елемент трубопроводу перейшов до нього у своєму командному рядку. >> '
— Бела Любкін

90

Я відтворив оригінальний результат на своєму комп’ютері, використовуючи g ++ на Mac.

Додавання наступних висловлювань до версії C ++ безпосередньо перед тим, як whileцикл приводить його у відповідність до версії Python :

std::ios_base::sync_with_stdio(false);
char buffer[1048576];
std::cin.rdbuf()->pubsetbuf(buffer, sizeof(buffer));

sync_with_stdio покращив швидкість до 2 секунд, а встановлення більшого буфера знизило його до 1 секунди.


5
Ви можете спробувати різні розміри буфера, щоб отримати більш корисну інформацію. Я підозрюю, що ви побачите швидко зменшувані прибутки.
— Карл Кнечтел,

8
Я був надто поспішним у своїй відповіді; встановлення розміру буфера на щось інше, ніж за замовчуванням, не спричинило помітної різниці.
— karunski

109
Я також уникав би встановлення буфера 1МБ на стек. Це може призвести до потокового потоку (хоча, мабуть, це гарне місце для дискусій про це!)
— Матьє М.

11
Матьє, Mac за замовчуванням використовує стек 8MB процесів. Linux використовує 4MB за нитку за замовчуванням, IIRC. 1 Мб не є великою проблемою для програми, яка перетворює введення з відносно малою глибиною стека. Що ще важливіше, але std :: cin відмінить стек, якщо буфер вийде за межі області.
— SEK

22
@SEK Windows За замовчуванням розмір стека становить 1 Мб.
— Етьєн

39

getline, оператори потоку, scanf , можуть бути зручними, якщо вам не байдуже час завантаження файлів або ви завантажуєте невеликі текстові файли. Але якщо продуктивність вас хвилює, вам слід просто просто запамповувати весь файл в пам’ять (припускаючи, що він буде відповідним).

Ось приклад:

//open file in binary mode
std::fstream file( filename, std::ios::in|::std::ios::binary );
if( !file ) return NULL;

//read the size...
file.seekg(0, std::ios::end);
size_t length = (size_t)file.tellg();
file.seekg(0, std::ios::beg);

//read into memory buffer, then close it.
char *filebuf = new char[length+1];
file.read(filebuf, length);
filebuf[length] = '\0'; //make it null-terminated
file.close();

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

std::istrstream header(&filebuf[0], length);

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


20

Наступний код був для мене швидшим, ніж інший код, розміщений тут поки що: (Visual Studio 2013, 64-розрядний файл, 500 Мб, довжина рядка рівномірно в [0, 1000)).

const int buffer_size = 500 * 1024;  // Too large/small buffer is not good.
std::vector<char> buffer(buffer_size);
int size;
while ((size = fread(buffer.data(), sizeof(char), buffer_size, stdin)) > 0) {
    line_count += count_if(buffer.begin(), buffer.begin() + size, [](char ch) { return ch == '\n'; });
}

Усі мої спроби Python перевищують коефіцієнт 2.


Ви можете отримати навіть швидше, ніж це за допомогою крихітної користувальницької, але абсолютно прямої програми C, яка ітеративно перетворює або незабуферовані readсистематичні виклики в статичний буфер довжини BUFSIZEабо через еквівалентні відповідні mmapsyscalls, а потім пропускає цей буфер, рахуючи нові рядки à la for (char *cp = buf; *cp; cp++) count += *cp == "\n". BUFSIZEОднак вам доведеться налаштувати свою систему, що stdio вже зробило б для вас. Але цей forцикл повинен складатись до приголомшливо кричущих швидких мовних інструкцій асемблера для обладнання вашої коробки.
— tchrist

3
count_if і лямбда також збирається до "дивовижно кричучого швидкого асемблера".
— Петтер

17

До речі, причина кількості версій для версії C ++ на одну більшу, ніж кількість для версії Python, полягає в тому, що прапор eof встановлюється лише тоді, коли робиться спроба зчитування за межами eof. Тож правильним циклом було б:

while (cin) {
    getline(cin, input_line);

    if (!cin.eof())
        line_count++;
};

70
Справді правильним циклом було б: while (getline(cin, input_line)) line_count++;
— Джонатан Вейклі

2
@JonathanWakely Я знаю, що я досить пізно, але використовую ++line_count;і ні line_count++;.
— val каже

7
@val, якщо це має значення, ваш компілятор має помилку. Змінна - a long, і компілятор цілком здатний сказати, що результат приросту не використовується. Якщо він не генерує ідентичний код для післярозвитку та попереднього збільшення, він порушується.
— Джонатан

2
Дійсно, будь-який гідний компілятор зможе виявити неправильне використання після збільшення та замінити його попереднім збільшенням, але компілятори цього не вимагають . Так ні, він не порушений, навіть якщо компілятор не виконує заміну. Більше того, писати ++line_count;замість line_count++;не зашкодило б :)
— Fareanor

1
@valsaysReinstateMonica У цьому конкретному прикладі чому б було віддавати перевагу або одному? Результат тут не використовується в будь-якому випадку, тому він буде прочитаний після while, правда? Було б важливо, чи була якась помилка, і ви хочете переконатися, що line_countправильно? Я просто здогадуюсь, але не розумію, чому це було б важливо.
— TankorSmash

14

У вашому другому прикладі (з scanf ()) причина, чому це все-таки повільніше, може бути тому, що scanf ("% s") аналізує рядок і шукає будь-який пробіл (пробіл, вкладка, новий рядок).

Крім того, так, CPython виконує кешування, щоб уникнути зчитування жорсткого диска.


12

Перший елемент відповіді: <iostream>повільно. Чорт повільно. Я отримую величезне підвищення продуктивності, scanfяк і внизу нижче, але це все-таки в два рази повільніше, ніж Python.

#include <iostream>
#include <time.h>
#include <cstdio>

using namespace std;

int main() {
    char buffer[10000];
    long line_count = 0;
    time_t start = time(NULL);
    int sec;
    int lps;

    int read = 1;
    while(read > 0) {
        read = scanf("%s", buffer);
        line_count++;
    };
    sec = (int) time(NULL) - start;
    line_count--;
    cerr << "Saw " << line_count << " lines in " << sec << " seconds." ;
    if (sec > 0) {
        lps = line_count / sec;
        cerr << "  Crunch speed: " << lps << endl;
    } 
    else
        cerr << endl;
    return 0;
}

Я не бачив цю публікацію, поки я не вніс третю редакцію, але ще раз дякую за вашу пропозицію. Як не дивно, зараз для мене немає жодного хіта проти python, коли рядок scanf в edit3 вище. Я використовую 2.7, до речі.
— JJC

10
Після виправлення версії c ++ ця версія stdio значно повільніша за версію c ++ iostreams на моєму комп’ютері. (3 секунди проти 1 секунди)
— карунський

10

Ну, я бачу, що у вашому другому рішенні ви перейшли cinдо scanf, що було першою пропозицією, яку я збирався зробити вам (cin - sloooooooooooow). Тепер, якщо перейти зscanf на fgets, ви побачите ще одне підвищення продуктивності: fgetsце найшвидша функція C ++ для введення рядків.

До речі, про цю синхронізацію не знав, добре. Але все ж слід спробувати fgets.


2
За винятком fgetsпомилок (як підрахунок рядків, так і з точки зору розбиття рядків на петлі, якщо вам потрібно їх використовувати) для досить великих ліній без додаткових перевірок неповних рядків (а спроба компенсувати це передбачає виділення зайвих великих буферів , де std::getlineобробляється перерозподіл, щоб легко відповідати фактичному вводу). Швидко і неправильно легко, але майже завжди варто використовувати «трохи повільніше, але правильне», відключення якого sync_with_stdioотримує у вас.
— ShadowRanger
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.