Чому читання рядків із 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.