Разработайте пользовательские алгоритмы длинного массива

Длинные массивы являются мощным, интуитивным способом работать с большими наборами данных с помощью традиционного синтаксиса MATLAB®. Однако, поскольку длинные массивы работают с блоками данных, каждые из которых индивидуально помещаются в память, традиционные алгоритмы большинства функций должны быть обновлены, чтобы использовать параллелизированный подход, чтобы поддержать длинные массивы. Эта тема показывает вам, как разработать ваши собственные параллелизированные алгоритмы, чтобы работать с длинными массивами.

В настоящее время доступные подходы для применения пользовательских функций к длинным массивам:

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

Причины реализовать пользовательские алгоритмы

Наиболее распространенные математические функции и операции MATLAB уже поддерживают длинные массивы. Если функциональность уже поддержана, то написание вашего собственного алгоритма не может быть необходимым.

Вот некоторые причины, почему вы можете хотеть реализовать пользовательский алгоритм для длинных массивов:

  • Реализация В настоящее время Неподдерживаемые Функции — Если конкретная функция в настоящее время не поддерживает длинные массивы, то можно использовать API, обрисованные в общих чертах здесь, чтобы записать версию этой функции, которая поддерживает длинные массивы.

  • Усильте Существующий Код — Если у вас есть существующий код, который выполняет некоторые операции на данных в оперативной памяти, затем только с незначительными модификациями, которыми можно сделать его совместимым, чтобы управлять на длинных массивах. Этот подход избегает потребности преобразовать код, чтобы соответствовать подмножеству языка MATLAB, который поддерживает длинные массивы.

  • Получите Производительность — Например, можно переписать функцию MATLAB как MEX-функцию C++, и затем можно использовать API, обрисованные в общих чертах здесь, чтобы вызвать MEX-функцию, чтобы работать с данными.

  • Пользуйтесь Предпочтительной Внешней Библиотекой — Для совместимости в вашей организации, это иногда требуется, чтобы пользоваться определенной внешней библиотекой для определенных вычислений. Можно использовать API, обрисованные в общих чертах здесь, чтобы повторно реализовать функцию с теми внешними библиотеками.

Поддерживаемые API

Поддерживаемые API предназначаются для усовершенствованного использования и не включают обширную входную проверку. Ожидайте проводить некоторое время, тестируя, что дополнительные функции, которые вы реализуете, удовлетворяют все требования и выполняют вычисления, которые вы ожидаете. В настоящее время поддерживаемые API для авторской разработки алгоритмов длинного массива перечислены здесь.

Имя функции пакетаОписание
matlab.tall.transformПримените заданную функцию к каждому блоку одного или нескольких длинных массивов.
matlab.tall.reduceПримените заданную функцию к каждому блоку одного или нескольких длинных массивов. Затем питайте вывод этой функции во вторую функцию сокращения.
matlab.tall.movingWindowПрименить функцию окна для блоков данных.
matlab.tall.blockMovingWindow

Применить функцию окна и блочное сокращение к заполненным блокам данных.

Теоретическая основа: Блоки Tall Array

Когда вы создаете длинный массив из datastore, базовый datastore упрощает перемещение данных во время вычисления. Перемещения данных в дискретных частях вызвали блоки, где каждый блок является набором последовательных строк, которые могут уместиться в памяти. Например, одним блоком 2D массива (такого как таблица) является X(n:m,:). Размер каждого блока основан на значении свойства ReadSize datastore, но блок не всегда что точный размер. В целях разработки алгоритмов длинного массива длинный массив считается вертикальной конкатенацией многих таких блоков.

Блоки данного массива выбраны во времени выполнения на основе доступной памяти, таким образом, они могут быть динамическими. Поэтому блоки не могут быть точно тем же размером между выполнениями. Если у вас есть изменения на вашем компьютере, которые влияют на доступную память, то это может повлиять на размер блоков.

Несмотря на то, что эта страница относится только к блокам и строкам в 2D смысле, эти концепции расширяют к длинным массивам N-D. Размер блока только ограничивается в первой размерности, таким образом, блок включает все элементы в другие размерности; например, X(n:m,:,:,...). Кроме того, а не строки, массивы N-D имеют срезы, такие как X(p,:,:,...).

Одноступенчатая операция преобразования

Функция matlab.tall.transform применяет одну функцию к каждому блоку длинного массива, таким образом, можно использовать его, чтобы применить мудрое блоком преобразование, фильтрацию или сокращение данных. Например, можно удалить строки с определенными значениями, сосредоточить и масштабировать данные, или обнаружить определенные обстоятельства и преобразовать определенные части данных. Эти данные показывают то, что происходит с блоками в массиве, когда они управляются matlab.tall.transform.

Операция

Описание

Примеры

Преобразование — количество строк в каждом блоке остается то же самое, но изменение значений.

  • A = matlab.tall.transform(@sin, tX) вычисляет синус элементов в каждом блоке.

  • A = matlab.tall.transform(@(X) X.^2, tX) придает элементам квадратную форму в каждом блоке.

При фильтрации — сокращено количество строк в каждом блоке, таким образом, блоки в новом массиве могут включать строки, первоначально существующие в других блоках.

  • A = matlab.tall.transform(@(X) topkrows(X,5), tX) извлекает только лучшие 5 строк от каждого блока, отфильтровывая другие строки.

  • A = matlab.tall.transform(@sum, tX) вычисляет сумму элементов в каждом блоке, который уменьшает каждый блок до скаляра. Число элементов в A равно количеству блоков.

Преобразуйте синтаксис

Типичный синтаксис, чтобы применять одноступенчатое преобразование

[tA, tB, tC, ...] = matlab.tall.transform(fcn, tX, tY, tZ, ...)

Функциональные требования для fcn

Общая функциональная подпись fcn

[a, b, c, ...] = fcn(x, y, z, ...)
fcn должен удовлетворить эти требования:

  1. Входные параметры — входные параметры [x, y, z, ...] являются блоками данных, которые умещаются в памяти. Блоки производятся путем извлечения данных из соответствующих входных параметров длинного массива [tX, tY, tZ, ...]. Входные параметры [x, y, z, ...] удовлетворяют эти свойства:

    • Весь [x, y, z, ...] имеет тот же размер в первой размерности после любого позволенного расширения.

    • Блоки данных в [x, y, z, ...] прибывают из того же индекса в высокой размерности, принимая, что длинный массив является неодиночным элементом в высокой размерности. Например, если tX и tY являются неодиночным элементом в высокой размерности, то первым набором блоков может быть x = tX(1:20000,:) и y = tY(1:20000,:).

    • Если первая размерность какого-либо [tX, tY, tZ, ...] имеет размер 1, то соответствующий блок [x, y, z, ...] состоит из всех данных в том длинном массиве.

  2. Выходные аргументы Выходные параметры [a, b, c, ...] являются блоками, которые умещаются в памяти, чтобы быть отправленными в соответствующие выходные параметры [tA, tB, tC, ...]. Выходные параметры [a, b, c, ...] удовлетворяют эти свойства:

    • Весь [a, b, c, ...] должен иметь тот же размер в первой размерности.

    • Весь [a, b, c, ...] вертикально конкатенирован с соответствующими результатами предыдущих вызовов fcn.

    • Весь [a, b, c, ...] отправляется в тот же индекс в первой размерности в их соответствующих целевых выходных массивах.

  3. Функциональные Правилаfcn должен удовлетворить функциональное правило:

    • F([inputs1; inputs2]) == [F(inputs1); F(inputs2)]: Применение функции к конкатенации входных параметров должно совпасть с применением функции к входным параметрам отдельно и затем конкатенации результатов.

  4. Пустые Входные параметры — Гарантируют, что fcn может обработать вход, который имеет высоту 0. Пустые входные параметры могут произойти, когда файл пуст или если вы сделали большую фильтрацию на данных.

Двухступенчатая операция сокращения

matlab.tall.reduce применяет две функции к длинному массиву с результатом первого шага, подаваемого, как введено к итоговому шагу сокращения. Функция сокращения неоднократно применяется к промежуточным результатам до одного итогового блока, который умещается в памяти, получен. В парадигме MapReduce этот процесс подобен "одной ключевой" операции MapReduce, где промежуточные результаты все имеют тот же ключ и объединены на шаге сокращения.

Первый шаг подобен matlab.tall.transform и имеет те же требования. Однако шаг сокращения всегда уменьшает промежуточные результаты вниз до одного блока, который умещается в памяти. Эти данные показывают то, что происходит с блоками в массиве, когда они управляются matlab.tall.reduce.

Операция

Описание

Примеры

Преобразование + Сокращение — количество строк в каждом блоке остается то же самое после первого шага, и затем промежуточные результаты уменьшаются до одного блока.

  • A = matlab.tall.reduce(@sin,@max,tX) вычисляет синус каждого блока значений, и затем это находит полное максимальное значение во время шага сокращения.

  • A = matlab.tall.reduce(@(X) X.^2, @mean, tX) придает элементам квадратную форму в каждом блоке, и затем это вычисляет полное среднее значение на шаге сокращения.

Фильтрация + Сокращение — количество строк в каждом блоке уменьшается в первом шаге. Затем промежуточные результаты уменьшаются до одного блока.

  • A = matlab.tall.reduce(@sum, @sum, tX) вычисляет сумму элементов в каждом блоке, и затем это находит полную сумму элементов на шаге сокращения.

  • A = matlab.tall.reduce(@(X) X(X>0), @mean, tX) отфильтровывает все отрицательные величины, и затем он вычисляет полное среднее значение остающихся значений.

Синтаксис шага сокращения

Типичный синтаксис, чтобы применить двухступенчатое сокращение

[rA, rB, rC, ...] = matlab.tall.reduce(fcn, reducefcn, tX, tY, tZ, ...)

Функциональная подпись fcn

[a, b, c, ...] = fcn(x, y, z, ...)

Функциональная подпись reducefcn

[rA, rB, rC, ...] = reducefcn(a, b, c, ...)

Таким образом, входные длинные массивы, [tX, tY, tZ, ...] повреждается в блоки [x, y, z, ...], которые являются входными параметрами к fcn. Затем fcn возвращает выходные параметры [a, b, c, ...], которые являются входными параметрами к reducefcn. Наконец, reducefcn возвращает конечные результаты [rA, rB, rC], которые возвращены matlab.tall.reduce.

Функциональные требования для reducefcn

Требования для fcn совпадают с теми, которые были обрисованы в общих чертах в Функциональных требованиях для fcn. Однако требования для reducefcn отличаются.

Общая функциональная подпись reducefcn

[rA, rB, rC, ...] = reducefcn(a, b, c, ...)
reducefcn должен удовлетворить эти требования:

  1. Входные параметры — входные параметры [a, b, c, ...] являются блоками, которые умещаются в памяти. Блоками данных являются или выходные параметры, возвращенные fcn или частично сокращенный выход из reducefcn, который управляется снова для дальнейшего сокращения. Входные параметры [a, b, c, ...] удовлетворяют эти свойства:

    • Входные параметры [a, b, c, ...] имеют тот же размер в первой размерности.

    • Для данного индекса в первой размерности каждой строке блоков данных [a, b, c, ...] или происходит из входа или происходит от того же предыдущего вызова до reducefcn.

    • Для данного индекса в первой размерности каждая строка входных параметров [a, b, c, ...] для того индекса происходит из того же индекса в первой размерности.

  2. Output Arguments — All выходные параметры [rA, rB, rC, ...] должен иметь тот же размер в первой размерности. Кроме того, они должны быть вертикально объединены с соответствующими входными параметрами [a, b, c, ...], чтобы допускать повторные сокращения при необходимости.

  3. Функциональные Правилаreducefcn должен удовлетворить эти функциональные правила (до ошибки округления):

    • F(input) == F(F(input)): Применение функции неоднократно к тем же входным параметрам не должно изменять результат.

    • F([input1; input2]) == F([input2; input1]): результат не должен зависеть от порядка конкатенации.

    • F([input1; input2]) == F([F(input1); F(input2)]): Применение функции однажды к конкатенации некоторых промежуточных результатов должно совпасть с применением его отдельно, конкатенацией и применением его снова.

  4. Пустые Входные параметры — Гарантируют, что reducefcn может обработать вход, который имеет высоту 0. Пустые входные параметры могут произойти, когда файл пуст или если вы сделали большую фильтрацию на данных. Для этого вызова все входные блоки являются пустыми массивами правильного типа и размера в размерностях вне первого.

Операции скользящего окна

matlab.tall.movingWindow и функции matlab.tall.blockMovingWindow применяют функцию к окнам данных в длинном массиве. В то время как matlab.tall.transform и matlab.tall.reduce работают с целыми блоками данных за один раз, функции движущегося окна работают с окнами данных. Окна могут охватить между блоками данных, считанными из диска.

Эти данные показывают то, что происходит с блоками в массиве, когда они управляются matlab.tall.movingWindow или matlab.tall.blockMovingWindow.

ОперацияОписаниеПримеры

Оконное преобразование — количество строк в каждом блоке остается то же самое, но изменение значений. Вывод содержит результаты операций, выполняемых и на неполных и на полных окнах данных.

И matlab.tall.movingWindow и matlab.tall.blockMovingWindow преобразовывают данные, когда 'EndPoints' является значением по умолчанию 'shrink', или когда значение заливки задано. Оба значения гарантируют, что вывод одного размера в первой размерности как вход.

  • A = matlab.tall.movingWindow(@mean, 100, tX) вычисляет скользящее среднее значение с помощью размера окна 100.

Окна Windowed Filtering — Incomplete данных отбрасываются, таким образом, вывод имеет меньше элементов, чем вход. Выходные данные содержат только результаты операций, выполненных в полных окнах данных.

И matlab.tall.movingWindow и matlab.tall.blockMovingWindow удаляют неполные окна данных, когда 'EndPoints' является 'discard'.

  • A = matlab.tall.movingWindow(@mean, 100, tX, 'EndPoints', 'discard') вычисляет скользящее среднее значение на полные окна данных, с помощью размера окна 100.

Можно использовать matlab.tall.movingWindow и matlab.tall.blockMovingWindow, чтобы применить оконные преобразования или фильтры к данным. Например, можно вычислить запаздывающее среднее значение или движущуюся медиану, или можно применить несколько операций целиком к тому же окну. Две функции отличаются этими способами:

  • matlab.tall.movingWindow применяет fcn ко всем окнам данных, независимо от того, завершены ли окна. matlab.tall.blockMovingWindow применяет windowfcn к неполным окнам данных и применяет blockfcn, чтобы завершить окна данных.

  • matlab.tall.movingWindow работает с одним окнами данных за один раз. matlab.tall.blockMovingWindow работает с целыми блоками данных, содержащими несколько полных окон, который сокращает количество вызовов функции, требуемых в вычислении.

Синтаксисы скользящего окна

Синтаксис для применения операции скользящего окна к отдельным окнам данных

[tA, tB, tC, ...] = matlab.tall.movingWindow(fcn, window, tX, tY, tZ, ...)

Функциональная подпись fcn должна быть

[a, b, c, ...] = fcn(x, y, z, ...)

Точно так же синтаксис, чтобы применить движущуюся операцию окна к целым блокам данных

[tA, tB, tC, ...] = matlab.tall.blockMovingWindow(windowfcn, blockfcn, window, tX, tY, tZ, ...)

Функциональные подписи windowfcn и blockfcn должны быть

[a, b, c, ...] = windowfcn(info, x, y, z, ...)
[a, b, c, ...] = blockfcn(info, bX, bY, bZ, ...)

Вход info является структурой, которая содержит поля Window и Stride. Когда вы пишете функцию. используйте эти поля, чтобы выбрать окна данных в каждом блоке.

Для схемы общих правил, что fcn, windowfcn и blockfcn должны следовать, смотрите Функциональные требования для fcn. Кроме входа info, fcn и windowfcn имеют те же требования. Однако требования для blockfcn отличаются, поскольку эта функция работает с целыми блоками данных.

Функциональные требования для windowfcn

Общая функциональная подпись windowfcn

[a, b, c, ...] = windowfcn(info, x, y, ...)
Вход info является структурой, обеспеченной matlab.tall.blockMovingWindow, который включает эти поля:

  • Stride — Заданный размер шага между окнами (значение по умолчанию: 1). Установите это значение с парой "имя-значение" 'Stride'.

  • Window — Заданный размер окна. Установите это значение с входным параметром window.

windowfcn должен удовлетворить эти требования:

  1. Входные параметры — входные параметры [x, y, z, ...] являются блоками данных, которые умещаются в памяти. Блоки производятся путем извлечения данных из соответствующих входных параметров длинного массива [tX, tY, tZ, ...]. Входные параметры [x, y, z, ...] удовлетворяют эти свойства:

    • Все входные параметры [x, y, z, ...] имеют тот же размер в первой размерности после любого позволенного расширения.

    • Блоки данных в [x, y, z, ...] прибывают из того же индекса в высокой размерности, принимая, что длинный массив является неодиночным элементом в высокой размерности. Например, если tX и tY являются неодиночным элементом в высокой размерности, то первым набором блоков может быть x = tX(1:20000,:) и y = tY(1:20000,:).

    • Когда первая размерность любого [tX, tY, tZ, ...] имеет размер 1, соответствующий блок [x, y, z, ...] состоит из всех данных в том длинном массиве.

    • Применение windowfcn должно привести к сокращению входных данных к скаляру или срезу массива высоты 1.

      Когда вход является матрицей, массивом N-D, таблицей, или расписание, применяя windowfcn должно привести к сокращению входных данных в каждом из его столбцов или переменных.

  2. Выходные аргументы Выходные параметры [a, b, c, ...] являются блоками, которые умещаются в памяти, чтобы быть отправленными в соответствующие выходные параметры [tA, tB, tC, ...]. Выходные параметры [a, b, c, ...] удовлетворяют эти свойства:

    • Все выходные параметры [a, b, c, ...] должны иметь тот же размер в первой размерности.

    • Все выходные параметры [a, b, c, ...] вертикально конкатенированы с соответствующими результатами предыдущих вызовов windowfcn.

    • Все выходные параметры [a, b, c, ...] отправляются в тот же индекс в первой размерности в их соответствующих целевых выходных массивах.

  3. Функциональные Правилаwindowfcn должен удовлетворить это функциональное правило:

    • F([inputs1; inputs2]) == [F(inputs1); F(inputs2)]: Применение функции к конкатенации входных параметров должно совпасть с применением функции к входным параметрам отдельно и затем конкатенации результатов.

  4. Пустые Входные параметры — Гарантируют, что windowfcn может обработать вход, который имеет высоту 0. Пустые входные параметры могут произойти, когда файл пуст или если вы сделали большую фильтрацию на данных.

Функциональные требования для blockfcn

Общая функциональная подпись blockfcn

[a, b, c, ...] = blockfcn(info, bX, bY, bZ, ...)
Вход info является структурой, обеспеченной matlab.tall.blockMovingWindow, который включает эти поля:

  • Stride — Заданный размер шага между окнами (значение по умолчанию: 1). Установите это значение с парой "имя-значение" 'Stride'.

  • Window — Заданный размер окна. Установите это значение с входным параметром window.

Блоки данных bX, bY, bZ, ..., который matlab.tall.blockMovingWindow предоставляет blockfcn, имеют эти свойства:

  • Блоки содержат только полноразмерные окна. blockfcn не должен задавать поведение для неполных окон данных.

  • Первое окно данных запускается в первом элементе блока. Последний элемент последнего окна является последним элементом блока.

blockfcn должен удовлетворить эти требования:

  1. Входные параметры — входные параметры [bX, bY, bZ, ...] являются блоками данных, которые умещаются в памяти. Блоки производятся путем извлечения данных из соответствующих входных параметров длинного массива [tX, tY, tZ, ...]. Входные параметры [bX, bY, bZ, ...] удовлетворяют эти свойства:

    • Все входные параметры [bX, bY, bZ, ...] имеют тот же размер в первой размерности после любого позволенного расширения.

    • Блоки данных в [bX, bY, bZ, ...] прибывают из того же индекса в высокой размерности, принимая, что длинный массив является неодиночным элементом в высокой размерности. Например, если tX и tY являются неодиночным элементом в высокой размерности, то первым набором блоков может быть bX = tX(1:20000,:) и bY = tY(1:20000,:).

    • Если первая размерность любого из вводов данных, [tX, tY, tZ, ...] имеет размер 1, то соответствующий блок [bX, bY, bZ, ...] состоит из всех данных в том длинном массиве.

    • Применение blockfcn должно привести к сокращению входных данных, таким образом, что результат имеет высоту, равную количеству окон в блоке. Можно использовать info.Window и info.Stride, чтобы определить количество окон в блоке.

      Если вход является матрицей, массивом N-D, таблицей или расписанием, то применение blockfcn должно привести к сокращению входных данных в каждом из его столбцов или переменных.

  2. Выходные аргументы Выходные параметры [a, b, c, ...] являются блоками, которые умещаются в памяти, чтобы быть отправленными в соответствующие выходные параметры [tA, tB, tC, ...]. Выходные параметры [a, b, c, ...] удовлетворяют эти свойства:

    • Все выходные параметры [a, b, c, ...] должны иметь тот же размер в первой размерности.

    • Все выходные параметры [a, b, c, ...] вертикально конкатенированы с соответствующими результатами предыдущих вызовов blockfcn.

    • Все выходные параметры [a, b, c, ...] отправляются в тот же индекс в первой размерности в их соответствующих целевых выходных массивах.

  3. Функциональные Правилаblockfcn должен удовлетворить это функциональное правило:

    • F([inputs1; inputs2]) == [F(inputs1); F(inputs2)]: Применение функции к конкатенации входных параметров должно совпасть с применением функции к входным параметрам отдельно и затем конкатенации результатов.

  4. Пустые Входные параметры — Гарантируют, что blockfcn может обработать вход, который имеет высоту 0. Пустые входные параметры могут произойти, когда файл пуст или если вы сделали большую фильтрацию на данных.

Управляйте типом выходных данных

Если окончательный результат от каких-либо из Поддерживаемых API имеет различный тип данных от входа, то необходимо задать пару "имя-значение" 'OutputsLike', чтобы обеспечить один или несколько прототипных массивов, которые имеют тот же тип данных и атрибуты как соответствующие выходные параметры. Значение 'OutputsLike' всегда является массивом ячеек с каждой ячейкой, содержащей прототипный массив для соответствующего выходного аргумента.

Например, этот вызов matlab.tall.transform принимает один длинный массив tX как вход и возвращает два выходных параметра с различными типами, заданными прототипными массивами protoA и protoB. Вывод A имеет тот же тип данных и атрибуты как protoA, и аналогично для B и protoB.

C = {protoA protoB};
[A, B] = matlab.tall.transform(fcn, tX, 'OutputsLike', C)

Распространенный способ предоставить прототипные массивы состоит в том, чтобы вызвать fcn с тривиальными входными параметрами соответствующего типа данных, поскольку выходные параметры, возвращенные fcn, имеют правильный тип данных. В этом примере функция преобразования принимает tall double, но возвращает tall table. Прототипный массив сгенерирован путем вызова fcn(0), и прототип задан как значение 'OutputsLike'.

ds = tabularTextDatastore('airlinesmall.csv','TreatAsMissing','NA');
ds.SelectedVariableNames = {'ArrDelay', 'DepDelay'};
tt = tall(ds);
tX = tt.ArrDelay;

fcn = @(x) table(x,'VariableNames',{'MyVar'});
proto_A = fcn(0);
A = matlab.tall.transform(fcn,tX,'OutputsLike',{proto_A});

Кодирование и советы производительности

  • Поместите всю аналитику в одну функцию, которую вы вызываете, чтобы работать непосредственно с данными, вместо того, чтобы использовать ненужные вложенные функции.

  • Эксперимент с помощью небольшого подмножества данных. Профилируйте свой код, чтобы найти и зафиксировать узкие места перед увеличением масштаба к целому набору данных, где узкие места могут быть значительно усилены.

  • Обратите внимание на ориентацию ваших данных, поскольку некоторые функции возвращают выходные параметры в различных формах в зависимости от входных данных. Например, unique может возвратить или вектор - строку или вектор-столбец в зависимости от ориентации входных данных.

  • Блоки динамически сгенерированы во времени выполнения на основе доступной памяти компьютера. Убедитесь, что любая заданная функция сокращения соблюдает функциональное правило F([input1; input2]) == F([F(input1); F(input2)]). Если это правило не соблюдено, то результаты могут значительно отличаться между испытаниями.

  • Блоки могут иметь любой размер в первой размерности, включая 0 или 1. Размер 0 или 1 может произойти в промежуточных вычислениях в результате операций фильтрации или сокращения. Убедитесь, что ваша функция делает правильную вещь для обоих из этих случаев. Один из признаков того, что функция не обрабатывает эти случаи должным образом, когда вы получаете сообщение об ошибке "Output is different size".

Смотрите также

| | |

Похожие темы