Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
9be3a0f
feat: add border modes
semrosin Apr 30, 2026
4071124
feat: add kernel type
semrosin Apr 30, 2026
0b84ba7
feat: add type for grey image
semrosin Apr 30, 2026
41884f3
feat: add image io
semrosin Apr 30, 2026
538bf02
feat: add main convolver
semrosin Apr 30, 2026
ca4f74b
feat: add cli
semrosin Apr 30, 2026
b90f74f
test: add core tests
semrosin Apr 30, 2026
494d039
test: add cli tests
semrosin Apr 30, 2026
8122e35
ci: add ci tests
semrosin Apr 30, 2026
2417152
add readme
semrosin May 13, 2026
115753c
feat: add work partirioning modes
semrosin Apr 30, 2026
0dbf03a
feat: add parallel convolution
semrosin Apr 30, 2026
ac7a183
feat: add benchmarks
semrosin Apr 30, 2026
cb4fbe4
feat: add parallel realiztion usage to cli
semrosin Apr 30, 2026
de2c3fb
feat: add benchmarks to readme
semrosin Apr 30, 2026
8d98c70
test: add tests for parallel realization
semrosin Apr 30, 2026
d9e6de0
feat: add benchmarks error to readme
semrosin May 12, 2026
9610cff
feat: add benchmarks to readme
semrosin Apr 30, 2026
fb76c1d
feat: add batch convolution helper types
semrosin May 8, 2026
81eb1b8
feat: add batch convolution pipeline
semrosin May 8, 2026
e1e5587
test: add tests for pipeline
semrosin May 8, 2026
ef5a27c
feat: update cli
semrosin May 8, 2026
26ee4b4
bench: add benchmarks for pipeline io
semrosin May 8, 2026
d6e3bb3
feat: update readme
semrosin May 8, 2026
b7ecbf4
feat: add pipeline io auto balancing
semrosin May 13, 2026
8e1659a
update readme with benchmarks
semrosin May 13, 2026
233b9d4
refactor: move files to directries
semrosin May 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# This workflow will build a .NET project
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net

name: .NET

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

jobs:
build:

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- name: Install OpenCV
run: sudo apt-get update && sudo apt-get install -y libjpeg-dev libpng-dev libtiff-dev libavcodec-dev libavformat-dev libswscale-dev libv4l-dev libxvidcore-dev libx264-dev libopencv-dev
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --no-restore
- name: Test
run: dotnet test --no-build --verbosity normal
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ bld/
# .NET Core
project.lock.json
project.fragment.lock.json
BenchmarkDotNet.Artifacts

# ASP.NET Scaffolding
ScaffoldingReadMe.txt
Expand Down
13 changes: 13 additions & 0 deletions Convolver.sln
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{E7170ACE-9AA
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BMPConvolver.Core", "src\BMPConvolver.Core\BMPConvolver.Core.csproj", "{523C0934-7353-45C5-AB89-DFB54C98071F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BMPConvolver.Cli", "src\BMPConvolver.Cli\BMPConvolver.Cli.csproj", "{A99E9047-15E4-492A-8C77-B2C82CAF0B0B}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{5F4B7EAD-0AB8-4FBF-85F1-EA0BEE09832F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BMPConvolver.Tests", "test\BMPConvolver.Tests\BMPConvolver.Tests.csproj", "{69A51A1D-CE2E-4C75-AD2E-64ADC5F8E593}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "bench", "bench", "{2507B97A-9F2C-4A9C-A7A3-58541DDFA976}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BMPConvolver.Benchmarks", "bench\BMPConvolver.Benchmarks\BMPConvolver.Benchmarks.csproj", "{4E6BFCD2-D954-4FB9-AC30-1024FA7A9A3C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -35,5 +45,8 @@ Global
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{523C0934-7353-45C5-AB89-DFB54C98071F} = {E7170ACE-9AAD-4689-ADC6-22E048D93B80}
{A99E9047-15E4-492A-8C77-B2C82CAF0B0B} = {E7170ACE-9AAD-4689-ADC6-22E048D93B80}
{69A51A1D-CE2E-4C75-AD2E-64ADC5F8E593} = {5F4B7EAD-0AB8-4FBF-85F1-EA0BEE09832F}
{4E6BFCD2-D954-4FB9-AC30-1024FA7A9A3C} = {2507B97A-9F2C-4A9C-A7A3-58541DDFA976}
EndGlobalSection
EndGlobal
156 changes: 156 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,157 @@
# Запуск

Для запуска приложения выполните следующую команду:

```bash
dotnet run --project src/BMPConvolver.Cli/BMPConvolver.Cli.csproj <input.bmp> <output.bmp> [опции]
```

Для потоковой обработки каталога изображений:

```bash
dotnet run --project src/BMPConvolver.Cli/BMPConvolver.Cli.csproj <input-dir> <output-dir> --batch --mode par --partition grid --grid 8x8
```

Pipeline сам управляет ресурсами: выбирает стартовое количество reader/convolver/writer worker'ов, ограничивает очереди между стадиями и при `--mode par` делит CPU-бюджет между количеством одновременно обрабатываемых изображений и внутренним параллелизмом одной свертки. Во время работы балансировщик следит за заполненностью очередей: если загруженные изображения копятся, он добавляет активные свертки; если очередь перед записью забита, добавляет writers и притормаживает свертку; если чтение забегает вперед, уменьшает readers.

Опции включают:
- `--mode seq|par`: режим выполнения (последовательный или параллельный)
- `--partition pixels|rows|cols|grid`: способ разделения работы
- `--grid <block width>x<block height>`: размер сетки для grid partition
- `--border zero|clamp`: режим обработки границ
- `--kernel box3|sharpen|identity`: предустановленный kernel
- `--kernel-text "..."`: kernel в текстовом формате
- `--kernel-file path.txt`: kernel из файла

## Тесты

Для запуска тестов:

```bash
dotnet test
```

## Бенчмарки

Для запуска бенчмарков:

```bash
dotnet run --project bench/BMPConvolver.Benchmarks/BMPConvolver.Benchmarks.csproj
```

Запуск только pipeline IO benchmark:

```bash
dotnet run -c Release --project bench/BMPConvolver.Benchmarks/BMPConvolver.Benchmarks.csproj --filter "*PipelineIoBench*"
```
В этом бенчмарке сравниваются две стратегии IO (`Sequential IO` и `Pipeline IO`) для каждой стратегии свертки.

# Анализ производительнсти (последовательная и параллельная свертка)
- **Зависимость от ширины и высоты**: При фиксированной высоте увеличение ширины приводит к росту времени в 4-5 раз, тогда как при фиксированной ширине увеличение высоты дает рост в 2-3 раза. Такая разнца связана с особенностями кэширования памяти и последовательным доступом к данным в массивах.
- **Зависимость от способа параллелизма**:
- **Sequential**: Базовый последовательный метод, без параллелизма. Примерно в 5 раз медленнее самого эффективного параллельного метода
- **Parallel.Pixels**: Параллелизм по каждому пикселю создает огромное количество задач, что приводит к высоким накладным расходам на синхронизацию. Самый медленный метод с высокой дисперсией.
- **Parallel.Rows**: Параллелизм по строкам эффективен для изображений с большим количеством строк. Разделяет изображение на горизонтальные полосы, минимизируя конфликты памяти. Часто быстрее Parallel.Columns.
- **Parallel.Columns**: Параллелизм по столбцам показывает себя лучше на изображениях с большим количеством столбцов. Однако он может страдать от неравномерности нагрузки при широких изображениях.
- **Parallel.Grid**: Наиболее эффективный метод, разделяющий изображение на прямоугольные блоки (сетку). Балансирует нагрузку между потоками, минимизирует накладные расходы и обеспечивает лучшую масштабируемость. Часто в 2-3 раза быстрее Sequential и в 10+ раз быстрее Parallel.Pixels.

### Диаграмма
![Столбчатая_диаграмма_Zero](public/img/Border_mode_Zero_columnar.png)

### График
![График_Zero](public/img/Border_mode_Zero_Graphics.png)

### Таблица

| Method | Width | Height | Mean | Error |
|----------------- |------ |------- |-----------:|-----------:|
| Sequential | 1024 | 1024 | 11.347 ms | 0.1680 ms |
| Parallel.Pixels | 1024 | 1024 | 24.677 ms | 1.3195 ms |
| Parallel.Rows | 1024 | 1024 | 2.510 ms | 0.0188 ms |
| Parallel.Columns | 1024 | 1024 | 2.313 ms | 0.0259 ms |
| Parallel.Grid | 1024 | 1024 | 2.247 ms | 0.0162 ms |
| | | | | |
| Sequential | 1024 | 2048 | 22.219 ms | 0.0945 ms |
| Parallel.Pixels | 1024 | 2048 | 51.281 ms | 5.7572 ms |
| Parallel.Rows | 1024 | 2048 | 4.961 ms | 0.0466 ms |
| Parallel.Columns | 1024 | 2048 | 5.252 ms | 0.0444 ms |
| Parallel.Grid | 1024 | 2048 | 4.517 ms | 0.0601 ms |
| | | | | |
| Sequential | 1024 | 4096 | 48.451 ms | 0.1076 ms |
| Parallel.Pixels | 1024 | 4096 | 110.977 ms | 19.1888 ms |
| Parallel.Rows | 1024 | 4096 | 9.724 ms | 0.0335 ms |
| Parallel.Columns | 1024 | 4096 | 10.589 ms | 0.0641 ms |
| Parallel.Grid | 1024 | 4096 | 8.937 ms | 0.0491 ms |
| | | | | |
| Sequential | 2048 | 1024 | 24.379 ms | 0.1265 ms |
| Parallel.Pixels | 2048 | 1024 | 49.390 ms | 6.5355 ms |
| Parallel.Rows | 2048 | 1024 | 4.982 ms | 0.0440 ms |
| Parallel.Columns | 2048 | 1024 | 6.388 ms | 0.1713 ms |
| Parallel.Grid | 2048 | 1024 | 5.264 ms | 0.1405 ms |
| | | | | |
| Sequential | 2048 | 2048 | 48.880 ms | 0.2759 ms |
| Parallel.Pixels | 2048 | 2048 | 102.091 ms | 8.1502 ms |
| Parallel.Rows | 2048 | 2048 | 9.650 ms | 0.0547 ms |
| Parallel.Columns | 2048 | 2048 | 9.829 ms | 0.1386 ms |
| Parallel.Grid | 2048 | 2048 | 9.884 ms | 0.0655 ms |
| | | | | |
| Sequential | 2048 | 4096 | 98.503 ms | 0.8038 ms |
| Parallel.Pixels | 2048 | 4096 | 236.901 ms | 4.6947 ms |
| Parallel.Rows | 2048 | 4096 | 18.314 ms | 0.2121 ms |
| Parallel.Columns | 2048 | 4096 | 20.790 ms | 0.1510 ms |
| Parallel.Grid | 2048 | 4096 | 19.169 ms | 0.1776 ms |
| | | | | |
| Sequential | 4096 | 1024 | 48.863 ms | 0.2360 ms |
| Parallel.Pixels | 4096 | 1024 | 103.113 ms | 11.9344 ms |
| Parallel.Rows | 4096 | 1024 | 8.819 ms | 0.0642 ms |
| Parallel.Columns | 4096 | 1024 | 9.795 ms | 0.0708 ms |
| Parallel.Grid | 4096 | 1024 | 9.115 ms | 0.2993 ms |
| | | | | |
| Sequential | 4096 | 2048 | 96.191 ms | 3.7277 ms |
| Parallel.Pixels | 4096 | 2048 | 223.957 ms | 24.3798 ms |
| Parallel.Rows | 4096 | 2048 | 16.587 ms | 0.0625 ms |
| Parallel.Columns | 4096 | 2048 | 22.102 ms | 0.0916 ms |
| Parallel.Grid | 4096 | 2048 | 20.067 ms | 0.2590 ms |
| | | | | |
| Sequential | 4096 | 4096 | 203.417 ms | 3.6494 ms |
| Parallel.Pixels | 4096 | 4096 | 483.104 ms | 3.5202 ms |
| Parallel.Rows | 4096 | 4096 | 37.907 ms | 0.3498 ms |
| Parallel.Columns | 4096 | 4096 | 45.954 ms | 0.4596 ms |
| Parallel.Grid | 4096 | 4096 | 38.499 ms | 0.2745 ms |

# Анализ производительности (потоковая и последовательная обработка каталога изображений)

В этом бенчмарке сравнивается обработка одного и того же каталога BMP-файлов двумя способами:
- **Sequential IO**: каждый файл полностью проходит цепочку `чтение -> свертка -> запись`, после чего начинается следующий файл.
- **Pipeline IO**: чтение, свертка и запись разделены на стадии, между которыми стоят ограниченные очереди. Это позволяет читать следующий файл, пока предыдущий сворачивается или записывается.

Основные выводы:
- **Sequential convolution**: pipeline дает заметный выигрыш, потому что несколько независимых изображений могут сворачиваться разными worker'ами, а чтение и запись перекрываются с вычислениями. Время уменьшается с `461.7 ms` до `361.3 ms`, то есть примерно в `1.28x`.
- **ParallelPixels**: pipeline тоже дает заметное ускорение (`326.4 ms` против `231.2 ms`, примерно `1.41x`). Несмотря на высокие накладные расходы этой стратегии, перекрытие чтения, свертки и записи в каталоге изображений оказывается полезным.
- **ParallelRows** и **ParallelColumns**: pipeline дает небольшой выигрыш (`1.07x` и `1.13x`). Эти стратегии уже эффективно используют CPU внутри одного изображения, поэтому дополнительный эффект от pipeline ограничен перекрытием чтения и записи; у `ParallelRows` погрешность pipeline-измерения заметно выше, поэтому результат стоит трактовать осторожно.
- **ParallelGrid**: лучший результат для pipeline (`188.4 ms` против `136.4 ms`, примерно `1.38x`). Grid хорошо балансирует вычисления внутри изображения, а pipeline дополнительно убирает простои между чтением, обработкой и записью.

### Диаграмма
![Столбчатая_диаграмма_Pipeline](public/img/Pipeline_IO_columnar.png)

### График
![График_Pipeline](public/img/Pipeline_IO_graphics.png)

### Таблица

| Method | Strategy | Mean | Error |
|---------------- |---------------- |---------:|---------:|
| 'Sequential IO' | Sequential | 461.7 ms | 8.70 ms |
| 'Pipeline IO' | Sequential | 361.3 ms | 33.37 ms |
| | | | |
| 'Sequential IO' | ParallelPixels | 326.4 ms | 28.00 ms |
| 'Pipeline IO' | ParallelPixels | 231.2 ms | 32.51 ms |
| | | | |
| 'Sequential IO' | ParallelRows | 178.1 ms | 9.45 ms |
| 'Pipeline IO' | ParallelRows | 166.9 ms | 57.76 ms |
| | | | |
| 'Sequential IO' | ParallelColumns | 193.4 ms | 29.41 ms |
| 'Pipeline IO' | ParallelColumns | 171.1 ms | 41.00 ms |
| | | | |
| 'Sequential IO' | ParallelGrid | 188.4 ms | 15.65 ms |
| 'Pipeline IO' | ParallelGrid | 136.4 ms | 7.53 ms |
18 changes: 18 additions & 0 deletions bench/BMPConvolver.Benchmarks/BMPConvolver.Benchmarks.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">

<ItemGroup>
<ProjectReference Include="..\..\src\BMPConvolver.Core\BMPConvolver.Core.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.15.8" />
</ItemGroup>

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

</Project>
87 changes: 87 additions & 0 deletions bench/BMPConvolver.Benchmarks/ConvolverBench.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Jobs;
using BMPConvolver.Core;
using BMPConvolver.Core.WorkPartitioning;

namespace BMPConvolver.Benchmarks;

[Config(typeof(Config))]
[HideColumns(Column.RatioSD, Column.AllocRatio, Column.Gen0, Column.Gen1, Column.Gen2)]
public class ConvolutionBench
{
private sealed class Config : ManualConfig
{
public Config()
{
AddJob(Job.Default.WithWarmupCount(3).WithIterationCount(15));
}
}
private const int Seed = 1;

[Params(1024, 2048, 4096)]
public int Width { get; set; }

[Params(1024, 2048, 4096)]
public int Height { get; set; }

[Params(BorderMode.Zero, BorderMode.Clamp)]
public BorderMode Border { get; set; }

public int KernelSize { get; set; } = 3;

public int Grid { get; set; } = 8;

private GrayImage _input = null!;
private Kernel _kernel = null!;

[GlobalSetup]
public void Setup()
{
_input = RandomImage(Width, Height);
_kernel = RandomKernelOddFrom(KernelSize, KernelSize);
}

[Benchmark(Baseline = true, Description = "Sequential")]
public GrayImage Sequential()
=> Convolver.ConvolveSequential(_input, _kernel, Border);

[Benchmark(Description = "Parallel.Pixels")]
public GrayImage ParallelByPixels()
=> Convolver.ConvolveParallel(_input, _kernel, Border, PartitioningMode.Pixels, gridX: Grid, gridY: Grid);

[Benchmark(Description = "Parallel.Rows")]
public GrayImage ParallelByRows()
=> Convolver.ConvolveParallel(_input, _kernel, Border, PartitioningMode.Rows, gridX: Grid, gridY: Grid);

[Benchmark(Description = "Parallel.Columns")]
public GrayImage ParallelByColumns()
=> Convolver.ConvolveParallel(_input, _kernel, Border, PartitioningMode.Columns, gridX: Grid, gridY: Grid);

[Benchmark(Description = "Parallel.Grid")]
public GrayImage ParallelByGrid()
=> Convolver.ConvolveParallel(_input, _kernel, Border, PartitioningMode.Grid, gridX: Grid, gridY: Grid);

private static GrayImage RandomImage(int width, int height)
{
var rnd = new Random(Seed);
var pixels = new float[width * height];
for (var i = 0; i < pixels.Length; i++)
pixels[i] = (float)rnd.NextDouble();
return new GrayImage(width, height, pixels);
}

private static Kernel RandomKernelOddFrom(int widthRequested, int heightRequested)
{
var width = widthRequested <= 1 ? 1 : (widthRequested % 2 == 1 ? widthRequested : widthRequested + 1);
var height = heightRequested <= 1 ? 1 : (heightRequested % 2 == 1 ? heightRequested : heightRequested + 1);

var rnd = new Random(Seed);
var weights = new float[width * height];
for (var i = 0; i < weights.Length; i++)
weights[i] = ((float)rnd.NextDouble() - 0.5f) * 0.5f;
return new Kernel(width, height, width / 2, height / 2, weights);
}
}

Loading
Loading