Реализует свёртку (convolution) изображений с различными фильтрами, демонстрируя разные стратегии распараллеливания задачи.
- 24 фильтра четырёх размеров (3×3 … 9×9): каждый размер реализует одинаковый набор типов — Identity, Box Blur, Gaussian Blur, Sharpen, Edges, Emboss
- 5 стратегий параллелизации: GPU (OpenCL with JOCL), по пикселям, по строкам, по столбцам, по сетке (2D grid с настраиваемой формой)
- Цепочки фильтров: последовательное применение нескольких фильтров; опциональная математическая композиция в одно ядро
- Кэш-friendly тайлинг: флаг
--tile-sizeзадаёт размер тайла в пикселях для подбора размера сетки под L1/L2 кэш - Пайплайн: потоковая пакетная обработка с ридером, N воркерами и врайтером
- GUI-режим: интерактивный интерфейс на Compose Desktop
- CLI apply-режим: применить фильтр(ы) и сохранить результат — быстро, один проход
- CLI benchmark-режим: полный бенчмарк всех стратегий с прогревом JIT и подробной статистикой
./gradlew runИнтерфейс разделён на два блока:
Сайдбар (левая панель) — все настройки:
- Цепочка фильтров — список фильтров, применяемых по порядку. Кнопка «+ Добавить фильтр» добавляет ещё один шаг. Каждый шаг можно удалить кнопкой «×».
- Объединить в одно ядро — чекбокс появляется при двух и более фильтрах. Показывает итоговый размер составного ядра (например,
5×5для двух3×3). - Алгоритм — выбор стратегии распараллеливания.
- Потоки — слайдер 1 до числа ядер (виден для параллельных режимов).
- Строк/Столбцов сетки — два слайдера 1–16 (видны только при «По сетке»).
- Кнопка «Применить», время последнего запуска, сообщения об ошибках.
Область изображений (правая часть) — два панели рядом: оригинал и результат. Кнопки «Загрузить» и «Сохранить» находятся под соответствующей панелью.
Применяет фильтр(ы) одним проходом и опционально сохраняет результат на диск.
./gradlew run --args='<путь> <фильтр...> [--strategy <стратегия>] [--output <файл>] [--threads N] [--compose] [--tile-size N] [--grid-rows R] [--grid-cols C]'Пример вывода:
Изображение : photo.png (1920×1080)
Стратегия : По строкам, 8 потоков
Фильтры : Gaussian Blur 3×3
Время : 91 мс
Сохранено : result.png
Прогревает JIT, затем делает 30 замеров каждой стратегии и выводит полную статистику.
./gradlew run --args='<путь> [фильтр...] --benchmark [--threads N] [--compose] [--tile-size N] [--grid-rows R] [--grid-cols C] [--kernel-size N[,N…]] [--csv <файл>]'Пример вывода:
Изображение : photo.png (1920×1080)
Процессоров : 8 | Потоков : 8
═══ Фильтр: Gaussian Blur 3×3
прогрев: ▪▪▪▪▪▪▪
Стратегия среднее ±std p50 p95 p99 CV% ускор
─────────────────────────────────────────────────────────────────────────────
Последовательный 312 мс ±8 мс 311 мс 324 мс 340 мс 2.5%
По пикселям 89 мс ±3 мс 88 мс 94 мс 99 мс 3.1% ×3.51
По строкам 85 мс ±2 мс 85 мс 89 мс 93 мс 2.8% ×3.67
По столбцам 91 мс ±4 мс 90 мс 97 мс 103 мс 4.0% ×3.43
По сетке (3×3) 88 мс ±3 мс 87 мс 93 мс 98 мс 3.2% ×3.55
Обрабатывает все изображения в директории (или один файл) потоком через конвейер ридер → воркеры → врайтер.
./gradlew run --args='<путь|директория> <фильтр...> --pipeline [--workers N] [--worker-strategy <стратегия>] [--worker-threads N] [--input-buffer N] [--output-buffer N] [--output <директория>]'Пример вывода:
Изображений : 120
Воркеры : 4 | Режим: Последовательный
Буферы : вход=8 выход=8
Фильтры : Gaussian Blur 3×3
Вывод : results/
обработано: 120 / 120
Всего : 4312 мс (27.8 изобр/с)
чтение : 890 мс (суммарно)
свёртка : 14200 мс (суммарно)
запись : 620 мс (суммарно)
Прогоняет синтетический батч одного изображения через пайплайн с разным количеством воркеров и показывает масштабирование throughput.
./gradlew run --args='<путь> [фильтр...] --pipeline --benchmark [--workers N] [--batch-size N] [--csv <файл>]'Пример вывода:
Изображение : photo.png (1920×1080)
Фильтры : Gaussian Blur 3×3
Батч : 16 изображений
воркеры изобр/с всего мс чтение мс свёртка мс запись мс ускор
─────────────────────────────────────────────────────────────────────────────
1 3.1 5161 420 14820 0 ×1.00
2 5.9 2702 410 7610 0 ×1.90
4 11.2 1428 390 3980 0 ×3.61
8 18.7 855 405 2100 0 ×6.03
| Флаг | Описание | Пример |
|---|---|---|
<путь> |
Путь к изображению (обязательный) | photo.png |
[фильтр ...] |
Один или несколько фильтров — образуют цепочку | "Gaussian Blur 3×3" "Sharpen 3×3" |
--threads N |
Максимальное число потоков для стратегий параллелизации | --threads 4 |
--compose |
Скомпоновать фильтры цепочки в одно ядро перед применением | --compose |
--tile-size N |
Размер тайла в пикселях для BY_GRID (вместо --grid-rows/--grid-cols) |
--tile-size 64 |
--grid-rows R |
Явное число строк сетки | --grid-rows 2 |
--grid-cols C |
Явное число столбцов сетки | --grid-cols 8 |
| Флаг | Описание | Пример |
|---|---|---|
--strategy <s> |
Стратегия: seq, pixels, rows, cols, grid (по умолчанию: seq) |
--strategy rows |
--output <файл> |
Путь для сохранения результата (PNG); без флага не сохраняется | --output result.png |
| Флаг | Описание |
|---|---|
--benchmark |
Включает режим: прогрев JIT + 30 замеров всех стратегий |
--kernel-size N[,N…] |
Ограничить фильтрами указанных размеров (3, 5, 7, 9) |
--csv <файл> |
Сохранить результаты в CSV |
| Флаг | Описание | Пример |
|---|---|---|
--pipeline |
Включает режим пакетной обработки | --pipeline |
<путь> |
Директория с изображениями или путь к одному файлу | /images/ |
--workers N |
Количество параллельных воркеров-свёртки (по умолчанию: число процессоров) | --workers 4 |
--worker-strategy <s> |
Стратегия свёртки внутри каждого воркера: те же ключи что у --strategy, включая gpu |
--worker-strategy gpu |
--worker-threads N |
Потоков внутри воркера при параллельном --worker-strategy |
--worker-threads 2 |
--worker-grid-rows R |
Строк сетки для --worker-strategy grid (0 = авто) |
--worker-grid-rows 2 |
--worker-grid-cols C |
Столбцов сетки для --worker-strategy grid (0 = авто) |
--worker-grid-cols 4 |
--gpu-workers K |
K воркеров из --workers используют GPU; остальные K−N работают по --worker-strategy. При недоступном GPU тихо переходят на CPU |
--gpu-workers 1 |
--input-buffer N |
Ёмкость канала ридер→воркеры; ридер приостанавливается при заполнении (по умолчанию: workers × 2) | --input-buffer 8 |
--output-buffer N |
Ёмкость канала воркеры→врайтер; воркеры приостанавливаются при заполнении (по умолчанию: workers × 2) | --output-buffer 8 |
--output <директория> |
Куда сохранять результаты (для pipeline apply) | --output results/ |
| Флаг | Описание | Пример |
|---|---|---|
--batch-size N |
Сколько раз повторить изображение в батче (по умолчанию: 16) | --batch-size 32 |
--workers N |
Максимальное количество воркеров (бенчмарк проходит 1, 2, 4 … до этого значения) | --workers 8 |
--csv <файл> |
Сохранить таблицу throughput в CSV | --csv out.csv |
Все числовые флаги принимают форму --flag N и --flag=N.
# Последовательно, сохранить результат
./gradlew run --args='photo.png "Gaussian Blur 3×3" --output result.png'
# Параллельно по строкам, 4 потока
./gradlew run --args='photo.png "Gaussian Blur 3×3" --strategy rows --threads 4 --output result.png'
# Цепочка фильтров
./gradlew run --args='photo.png "Gaussian Blur 3×3" "Sharpen 3×3" --strategy rows --output result.png'
# Цепочка скомпонована в одно ядро
./gradlew run --args='photo.png "Gaussian Blur 3×3" "Sharpen 3×3" --compose --strategy rows --output result.png'
# По сетке с тайлом под L1-кэш
./gradlew run --args='photo.png "Gaussian Blur 3×3" --strategy grid --tile-size 90 --output result.png'
# Конкретная форма сетки: 2 строки × 8 столбцов
./gradlew run --args='photo.png "Edges 3×3" --strategy grid --grid-rows 2 --grid-cols 8 --output result.png'# Все фильтры, все доступные потоки
./gradlew run --args='photo.png --benchmark'
# Только 3×3 фильтры
./gradlew run --args='photo.png --benchmark --kernel-size 3'
# Несколько размеров
./gradlew run --args='photo.png --benchmark --kernel-size 3,5'
# Один фильтр, не более 4 потоков, сохранить CSV
./gradlew run --args='photo.png "Gaussian Blur 3×3" --benchmark --threads 4 --csv bench.csv'
# Цепочка + сравнение с составным ядром
./gradlew run --args='photo.png "Gaussian Blur 3×3" "Sharpen 3×3" --benchmark --compose'# GPU-свёртка одного изображения
./gradlew run --args='photo.png "Gaussian Blur 3×3" --strategy gpu --output result.png'# Обработать все изображения из директории, 4 воркера
./gradlew run --args='/images/ "Gaussian Blur 3×3" --pipeline --workers 4 --output results/'
# Воркеры сами параллельные (параллельная свёртка внутри каждого воркера)
./gradlew run --args='/images/ "Gaussian Blur 3×3" --pipeline --workers 2 --worker-strategy rows --worker-threads 4 --output results/'
# GPU-воркеры: изображения раскидываются по воркерам, свёртка на GPU
./gradlew run --args='/images/ "Gaussian Blur 3×3" --pipeline --workers 1 --worker-strategy gpu --output results/'
# Гибрид (4b): 1 GPU-воркер + 3 CPU-воркера обрабатывают разные изображения параллельно
./gradlew run --args='/images/ "Gaussian Blur 3×3" --pipeline --workers 4 --gpu-workers 1 --output results/'
# Ограничить память: маленькие буферы при большом батче
./gradlew run --args='/images/ "Gaussian Blur 3×3" --pipeline --workers 4 --input-buffer 4 --output-buffer 4 --output results/'# Сравнить throughput при 1, 2, 4, 8 воркерах
./gradlew run --args='photo.png "Gaussian Blur 3×3" --pipeline --benchmark --batch-size 32'
# До 4 воркеров, сохранить результат
./gradlew run --args='photo.png "Gaussian Blur 3×3" --pipeline --benchmark --workers 4 --batch-size 32 --csv pipeline_bench.csv'Важно: все аргументы программы передаются как одна строка в
--args='...'. Не разделяйте их — gradle примет остальное за свои собственные задачи.Числовые флаги принимают оба формата:
--threads 4и--threads=4— оба равнозначны.
Граничные пиксели обрабатываются с zero-padding (выход за границу считается нулём).
Каждый размер реализует одинаковый набор типов фильтров — удобно сравнивать визуальный эффект и производительность при росте ядра.
| Тип фильтра | 3×3 | 5×5 | 7×7 | 9×9 | Примечание |
|---|---|---|---|---|---|
| Identity | ✓ | ✓ | ✓ | ✓ | Не изменяет изображение; все варианты дают одинаковый результат (zero-padding invariance) |
| Box Blur | ✓ | ✓ | ✓ | ✓ | Все веса = 1/N²; при большем ядре размытие заметно сильнее |
| Gaussian Blur | ✓ | ✓ | ✓ | ✓ | σ = 0.85 / 1.0 / 1.5 / 2.0; нормированная сумма = 1 |
| Sharpen | ✓ | ✓ | ✓ | ✓ | Ромбовидная «корона» отрицательных весов; центр компенсирует, сумма = 1 |
| Edges | ✓ | ✓ | ✓ | ✓ | Лапласиан; центр = N²−1, остальные = −1, сумма = 0 |
| Emboss | ✓ | ✓ | ✓ | ✓ | Диагональный градиент; сумма = 1 |
| Ядро | Операций/пиксель | ×3×3 |
|---|---|---|
| 3×3 | 9 | 1× |
| 5×5 | 25 | ~3× |
| 7×7 | 49 | ~5× |
| 9×9 | 81 | ~9× |
| Стратегия | CLI-ключ | Описание |
|---|---|---|
| Последовательный | seq |
Один поток, строка за строкой |
| GPU (OpenCL) | gpu |
Свёртка выполняется на GPU через OpenCL (JOCL). При отсутствии GPU автоматически падает на CPU-устройство OpenCL. |
| По пикселям | pixels |
Все пиксели делятся на N чанков, каждый поток обрабатывает свой чанк |
| По строкам | rows |
Строки изображения делятся между потоками |
| По столбцам | cols |
Столбцы изображения делятся между потоками |
| По сетке | grid |
2D-разбиение на R × C ячеек. Без явных --grid-rows/--grid-cols форма авто: √T × ⌈T/√T⌉. В benchmark дополнительно тестируются полосы 1×T и T×1 для наглядного сравнения кэш-эффектов. Горизонтальные полосы дружелюбнее к row-major данным. |
GrayImage хранит пиксели в row-major порядке: пиксели одной строки лежат подряд в памяти, соседние строки — рядом. При обходе по строкам (BY_ROW) поток читает и пишет в непрерывные адреса — HW-prefetcher предсказывает следующий кэш-линии и подтягивает их заранее, промахи кэша минимальны.
При обходе по столбцам (BY_COLUMN) каждый следующий пиксель находится на width × 4 байт дальше в памяти (страйд ~7–28 KB для типичных изображений). Для каждого пикселя процессор обращается к новой кэш-линии, которой ещё нет в кэше, — prefetcher не справляется со скачкообразным паттерном доступа, и возникают постоянные cache miss, которые вынуждают ждать данные из RAM.
BY_ROW (поток 0 обрабатывает строки 0..k):
чтение: [px0, px1, px2, px3, ...] ← непрерывно, L1/L2 hit
запись: [px0, px1, px2, px3, ...] ← непрерывно, L1/L2 hit
BY_COLUMN (поток 0 обрабатывает столбцы 0..k):
чтение: [px0, px_width, px_2*width, ...] ← страйд width*4 байт, cache miss на каждом шаге
запись: [px0, px_width, px_2*width, ...] ← то же самое
На практике BY_COLUMN проигрывает BY_ROW на ~5–15% при небольших изображениях и заметно больше при изображениях, не помещающихся в L3-кэш (>50–100 МБ), — именно потому, что стоимость каждого cache miss растёт при работе с DRAM.
BY_GRID с квадратными тайлами занимает промежуточное положение: каждый тайл имеет и строчный, и столбцовый обход, но тайл помещается в кэш целиком, поэтому внутри тайла промахов нет.
ридер ──[inputChannel]──► воркер 1 ──┐
► воркер 2 ──┼──[outputChannel]──► врайтер
► воркер N ──┘
Все три стадии работают одновременно как отдельные корутины:
- Ридер (Dispatchers.IO) — читает файлы один за другим и отправляет в
inputChannel. Приостанавливается, если воркеры не успевают (канал заполнен). - Воркеры (Dispatchers.Default) — берут задания из
inputChannel, применяют свёртку, кладут результат вoutputChannel. Несколько воркеров позволяют одному ридеру прокормить несколько параллельных свёрток. - Врайтер (Dispatchers.IO) — читает готовые результаты из
outputChannelи сохраняет PNG. Если записывает медленно,outputChannelзаполняется и воркеры тормозят.
--workers |
--gpu-workers |
--worker-strategy |
Когда использовать |
|---|---|---|---|
| 1 | 0 | seq |
Ридер быстрее свёртки незначительно; минимальный overhead |
| N | 0 | seq |
Ридер быстрый, свёртка медленная — один ридер кормит N CPU-свёрток |
| 1 | 0 | rows / pixels |
Один образ, максимально параллельная CPU-свёртка |
| N | 0 | rows |
Ридер быстрый, каждая свёртка параллельная (максимальный CPU) |
| N | 1 | seq |
1 GPU-воркер + N−1 CPU-воркеров обрабатывают разные изображения параллельно (4b) |
| N | N | — | Только GPU-воркеры; вызовы сериализуются на @Synchronized, N>1 не даёт ускорения — зато ридер и врайтер не простаивают |
Файл на диске
│
▼
BufferedImage ──toGrayImage()──▶ GrayImage(width, height, FloatArray)
│
│ для каждого фильтра в pipeline:
│ applyKernelAt(x, y, kernel) → Float
│ (zero-padding на границах)
▼
GrayImage (результат)
│
│ clamp [0, 255] → Int
▼
BufferedImage ──▶ PNG на диск / GUI
Два режима применения фильтров:
- Pipeline — фильтры применяются последовательно: выход предыдущего становится входом следующего.
- Compose (
--compose) — фильтры математически сворачиваются в одно ядро (composeKernels), затем применяется единственный проход. Результат тот же, количество проходов по изображению — одно.
src/main/kotlin/
├── Main.kt — точка входа: GUI если нет аргументов, иначе диспетчер CLI-команд
├── cli/
│ ├── Cli.kt — CliCommand, parseArgs(), parseStrategy(), resolveThreads(), tileToGrid()
│ ├── ApplyCommands.kt — runApply, runPipelineApply
│ └── BenchmarkCommands.kt — runBenchmark, runPipelineBenchmark
├── convolution/
│ ├── Convolution.kt — алгоритмы свёртки (sequential + parallel)
│ ├── GpuConvolution.kt — GPU-свёртка через OpenCL (JOCL): GpuContext singleton, convolveGpu()
│ ├── Kernels.kt — реестр всех фильтров (Kernels.all)
│ └── kernels/
│ ├── Kernels3x3.kt
│ ├── Kernels5x5.kt
│ ├── Kernels7x7.kt
│ └── Kernels9x9.kt
├── pipeline/
│ └── ImagePipeline.kt — runPipeline, PipelineConfig, PipelineStats, collectImagePaths
└── ui/
├── App.kt — Compose Desktop GUI (только вёрстка)
└── AppViewModel.kt — состояние UI и бизнес-логика (загрузка/сохранение, запуск свёртки)
src/test/kotlin/
├── convolution/
│ ├── ConvolutionTest.kt — тесты задач 1 и 2
│ └── GpuConvolutionTest.kt — тесты задачи 4a (пропускаются если OpenCL недоступен)
└── pipeline/
└── PipelineTest.kt — тесты задач 3 и 4b
Гибридные CPU (Intel 12th gen+, некоторые ARM): на процессорах с P-ядрами (производительными) и E-ядрами (эффективными) планировщик ОС сам решает, на каких ядрах запускать потоки. При
--threads Nнет гарантии, что задействуются именно P-ядра — часть потоков может попасть на E-ядра и замедлить результат. Это объясняет повышенный CV% в отдельных запусках бенчмарка.
GPU на Intel Iris Xe (интегрированная графика, без дискретной GPU): замеры задачи 4a проводились на ноутбуке с Intel Core i5-12450H + Intel Iris Xe Graphics. Дискретной GPU нет — OpenCL-устройством выступает встроенная графика. На Linux ICD-лоадер может не найти нужный драйвер автоматически; в этом случае нужно явно указать путь к PoCL:
OCL_ICD_FILENAMES=/etc/OpenCL/vendors/pocl.icd ./gradlew run # GUIВ CLI (
--strategy gpu) OpenCL также работает, но переменная нужна только при запуске GUI через./gradlew run. На системах с корректно установленным GPU-драйвером (NVIDIA, AMD, Intel Arc) эта переменная не нужна.
./gradlew testПосле запуска доступны два HTML-отчёта:
| Отчёт | Путь |
|---|---|
| Результаты тестов | build/reports/tests/test/index.html |
| Покрытие (JaCoCo) | build/reports/jacoco/test/html/index.html |
Корректность на простых фильтрах (identity, zero kernel), свойство композициональности, zero-expansion invariance, граничные размеры изображений (1×1, 1×N, N×1, изображение равного с ядром размера).
Все 4 режима (BY_PIXEL, BY_ROW, BY_COLUMN, BY_GRID) совпадают с последовательным эталоном на 1/2/4/8 потоках, всех 24 предопределённых фильтрах и случайных ядрах 1×1…7×7. Отдельно проверяются крупные ядра (5×5, 7×7, 9×9): граничные размеры, zero-padding invariance, корректность размера и результата композиции.
Результат пайплайна совпадает с последовательным эталоном при 1/2/4 воркерах, разных размерах буферов (backpressure), цепочках фильтров и Parallel-режиме воркеров. Дополнительно: корректность статистики, колбэк прогресса, сканирование директории.
Гибридный конфиг (gpuWorkerCount > 0) даёт тот же результат, что и sequential-эталон. Проверяется: gpuWorkerCount > workerCount молча обрезается, GPU недоступен — воркеры тихо переходят на CPU без ошибок.
Тесты автоматически пропускаются (assumeTrue) если OpenCL недоступен на машине — CI не ломается. Проверяется: корректность на всех 24 предопределённых фильтрах, граничные размеры изображений (1×1, 1×N, N×1), ядра 3×3…9×9, случайные ядра, GPU-конвейер из нескольких фильтров, стабильность при многократных вызовах без утечек.
- JVM 21