Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
5be9af0
feat: add utility library for load and write images
DaryaNechaeva Apr 30, 2026
49f8a57
feat: add filters of different types and sizes
DaryaNechaeva Apr 30, 2026
f96a06e
chore: add Makefile and clang configuration
DaryaNechaeva Apr 30, 2026
b08a0e1
tests: add images examples
DaryaNechaeva Apr 30, 2026
b4b0fcd
feat: add simple cli
DaryaNechaeva Apr 30, 2026
6e9c695
feat: add sequential convolution
DaryaNechaeva Apr 30, 2026
841ef07
tests: add tests for sequential convolution
DaryaNechaeva Apr 30, 2026
9dcfb30
chore: add README
DaryaNechaeva Apr 30, 2026
8e5787a
feat: add main function
DaryaNechaeva Apr 30, 2026
ccadaff
feat: add utility library file
DaryaNechaeva Apr 30, 2026
3ea999e
ci: add simple build
DaryaNechaeva Apr 30, 2026
b90c0a7
refactor: filters formatting
DaryaNechaeva May 1, 2026
d4ee305
feat: add convolution time measurement
DaryaNechaeva May 1, 2026
73ee91c
ci: add format and linter check
DaryaNechaeva May 1, 2026
2b4c882
chore: update Makefile
DaryaNechaeva May 1, 2026
ae52dc9
ci: update
DaryaNechaeva May 1, 2026
1067673
feat: add strategy for parallel
DaryaNechaeva May 1, 2026
3d664b4
feat: add helper functions for convolution
DaryaNechaeva May 1, 2026
f15d6cd
feat: add parallel convolution
DaryaNechaeva May 1, 2026
eb4db27
feat: update cli for parallel
DaryaNechaeva May 1, 2026
cb348aa
feat: update main for parallel
DaryaNechaeva May 1, 2026
b44042f
refactor: update processing --repeat
DaryaNechaeva May 1, 2026
6a4b2e2
chore: small update
DaryaNechaeva May 1, 2026
1f3b534
tests: add tests for parallel convolution
DaryaNechaeva May 1, 2026
a68018a
chore: update files
DaryaNechaeva May 1, 2026
c532f3e
feat: add benchmarks
DaryaNechaeva May 1, 2026
94c3bb6
feat: add script to construct plots
DaryaNechaeva May 1, 2026
05888e3
docs: add benchmark results
DaryaNechaeva May 1, 2026
7f82f26
chore: add readme with bencmark results
DaryaNechaeva May 1, 2026
a9c2920
refactor: delete repeat option
DaryaNechaeva May 1, 2026
05e1ae4
refactor: update Makefile
DaryaNechaeva May 1, 2026
6adc7ea
docs: small changes
DaryaNechaeva May 1, 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
7 changes: 7 additions & 0 deletions .clang-format
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
BasedOnStyle: LLVM
UseTab: Always
TabWidth: 4
IndentWidth: 4
ColumnLimit: 100
AllowShortIfStatementsOnASingleLine: true
AllowShortLoopsOnASingleLine: true
12 changes: 12 additions & 0 deletions .clang-tidy
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Checks: >
clang-diagnostic-*,
clang-analyzer-*,
bugprone-*,
-bugprone-easily-swappable-parameters,
performance-*,
readability-*,
-readability-magic-numbers,
-readability-function-cognitive-complexity,
-readability-identifier-length,
-readability-braces-around-statements
HeaderFilterRegex: '^(src|include)/.*'
36 changes: 36 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: C CI

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

jobs:
build-and-test:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y build-essential clang libcmocka-dev
sudo apt-get install -y build-essential clang clang-format clang-tidy libcmocka-dev bear

- name: Build
run: make CC=gcc

- name: Run tests
run: make test

- name: Check code formatting
run: |
find src/ -name '*.c' -o -name '*.h' | xargs clang-format --dry-run -Werror

- name: Generate compile_commands.json
run: bear -- make

- name: Static analysis
run: |
find src/ -name '*.c' | xargs clang-tidy --quiet
62 changes: 62 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
CC = gcc
CFLAGS = -std=c11 -Wall -Wextra -O2 -D_POSIX_C_SOURCE=199309L \
-Iinclude -Ithird_party/stb -fopenmp
CFLAGS_FAST = $(CFLAGS) -O3 -march=native
LDFLAGS = -lm -fopenmp
TARGET = conv
VPATH = src src/lib benchmarks

SRCS = main.c conv.c cli.c filters.c
OBJS = $(SRCS:.c=.o)

BENCH_DIR = benchmarks
BENCH_BIN = conv_bench

LIB_OBJS = conv.o cli.o filters.o
TEST_SRC = tests/test_conv.c
TEST_BIN = test_runner

all: $(TARGET)

$(TARGET): $(OBJS)
$(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS)

bench_conv.o: src/lib/conv.c
$(CC) $(CFLAGS_FAST) -c $< -o $@

bench_filters.o: src/lib/filters.c
$(CC) $(CFLAGS_FAST) -c $< -o $@

$(BENCH_DIR)/bench_main.o: $(BENCH_DIR)/bench_main.c
$(CC) $(CFLAGS_FAST) -c $< -o $@

$(BENCH_DIR)/benchmark.o: $(BENCH_DIR)/benchmark.c
$(CC) $(CFLAGS_FAST) -c $< -o $@

$(BENCH_BIN): $(BENCH_DIR)/bench_main.o $(BENCH_DIR)/benchmark.o bench_conv.o bench_filters.o
$(CC) $(CFLAGS_FAST) $^ -o $@ $(LDFLAGS)

bench: $(BENCH_BIN)
mkdir -p results
./$(BENCH_BIN) > results/bench.csv
@echo "Done → results/bench.csv"

%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@

test: $(TEST_BIN)
./$(TEST_BIN)

$(TEST_BIN): $(LIB_OBJS) $(TEST_SRC)
$(CC) -std=c11 -Wall -Wextra -O0 -g -Iinclude -Ithird_party/stb -fopenmp \
$^ -o $@ -lm -lcmocka

results:
mkdir -p results

clean:
rm -f $(TARGET) $(BENCH_BIN) $(OBJS) $(BENCH_DIR)/bench_main.o \
$(BENCH_DIR)/benchmark.o bench_conv.o bench_filters.o $(TEST_BIN)
rm -rf results

.PHONY: all test bench clean results
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Image Convolution

Simple CLI tool for applying convolution filters to images.
Supports sequential and parallel execution via OpenMP.

## Build & Run

```bash
make # build
make test # run tests
make bench # run benchmarks → results/bench.csv
```

## Usage

```bash
./conv <input> <output> --filter <name> [--strategy <name>]
```

**Filters**: identity, blur, gaussian, sharpen, edges, emboss
(add suffix 5/7/9 for larger kernels, e.g. `gaussian9`)

**Strategies**: sequential, rows, cols, pixels, tiles

**Formats**: BMP, PNG, JPEG

## Example

```bash
./conv photo.jpg result.png --filter gaussian7 --strategy rows
```

## Performance

See [BENCHMARK.md](benchmarks/README.md) for full analysis.
151 changes: 151 additions & 0 deletions benchmarks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
# Анализ производительности параллельной свёртки

Проведён анализ производительности параллельных реализаций
двумерной свёртки изображений. Измерялось время применения свёртки при разных стратегиях распараллеливания и разном числе потоков.

## Методика измерений

**Тестовые изображения** – 2 сгенерированных 3-канальных изображения 512×512 и 1024×1024 пикселей

- **Фильтр** – `gaussian5` (5×5, factor=1/256, bias=0).
- **Прогрев** – 5 холостых вызовов свёртки для каждого измерения, чтобы стабилизировать кэш и пул потоков OpenMP.
- **Количество измеряемых прогонов** – 50 (для каждой комбинации стратегии и числа потоков).
- **Таймер** – `omp_get_wtime()`
- **Метрики** – для каждого прогона сохранялось время в миллисекундах, затем вычислялись: среднее арифметическое, минимум, максимум, стандартное отклонение, а также ускорение (speedup) относительно последовательной версии.
- **Число потоков** – задавалось через `omp_set_num_threads()`; тестировались 1, 2, 4 и 8 потоков.

## Конфигурация тестовой системы

Все замеры производительности выполнены на следующей аппаратно-программной платформе:

| Параметр | Значение |
| ------------------------ | ------------------------------------------------- |
| **Процессор** | Intel(R) Celeron(R) N5095A @ 2.00GHz |
| **Архитектура** | x86_64, Jasper Lake (10nm) |
| **Ядра** | 4 физических ядра (1 поток на ядро) |
| **Кэш L1d** | 128 KiB (4×32 KiB) |
| **Кэш L1i** | 128 KiB (4×32 KiB) |
| **Кэш L2** | 1.5 MiB |
| **Кэш L3** | 4 MiB |
| **NUMA** | 1 узел |
| **Оперативная память** | 15 ГиБ |
| **Операционная система** | Ubuntu 24.04.3 LTS |
| **Компилятор** | GCC 13.3.0 |
| **Флаги компиляции** | `-std=c11 -Wall -Wextra -O3 -fopenmp` (бенчмарка) |

## Результаты

### Изображение 512×512 запуск 1

| Стратегия | Потоки | Mean (мс) | Min (мс) | Max (мс) | StdDev (мс) | Speedup |
| ---------- | ------ | --------- | -------- | -------- | ----------- | ------- |
| sequential | 1 | 394.877 | 393.201 | 415.805 | 3.291 | 1.00× |
| per_pixel | 1 | 404.817 | 402.709 | 421.737 | 2.693 | 0.98× |
| per_pixel | 2 | 216.767 | 203.375 | 305.470 | 19.510 | 1.82× |
| per_pixel | 4 | 267.428 | 186.651 | 414.969 | 64.210 | 1.48× |
| per_pixel | 8 | 187.870 | 140.583 | 242.681 | 27.423 | 2.10× |
| by_rows | 1 | 400.066 | 399.369 | 402.782 | 0.567 | 0.99× |
| by_rows | 2 | 200.357 | 199.674 | 206.340 | 0.947 | 1.97× |
| by_rows | 4 | 242.183 | 190.044 | 275.931 | 24.299 | 1.63× |
| by_rows | 8 | 156.517 | 141.894 | 177.730 | 8.408 | 2.52× |
| by_cols | 1 | 400.708 | 399.654 | 403.005 | 0.817 | 0.99× |
| by_cols | 2 | 200.553 | 199.668 | 209.442 | 1.437 | 1.97× |
| by_cols | 4 | 219.569 | 184.152 | 266.234 | 29.584 | 1.80× |
| by_cols | 8 | 166.910 | 140.044 | 267.140 | 23.838 | 2.37× |
| tiles | 1 | 400.716 | 399.054 | 405.016 | 1.394 | 0.99× |
| tiles | 2 | 201.223 | 199.922 | 215.190 | 2.815 | 1.96× |
| tiles | 4 | 234.076 | 185.226 | 319.881 | 29.856 | 1.69× |
| tiles | 8 | 163.673 | 140.482 | 247.685 | 20.275 | 2.41× |

### Изображение 512×512 запуск 2

| Стратегия | Потоки | Mean (мс) | Min (мс) | Max (мс) | StdDev (мс) | Speedup |
| ---------- | ------ | --------- | -------- | -------- | ----------- | ------- |
| sequential | 1 | 396.685 | 392.295 | 491.766 | 14.699 | 1.00× |
| per_pixel | 1 | 407.701 | 401.142 | 546.467 | 26.603 | 0.97× |
| per_pixel | 2 | 235.941 | 200.885 | 562.304 | 67.582 | 1.68× |
| per_pixel | 4 | 260.416 | 187.290 | 522.834 | 57.915 | 1.52× |
| per_pixel | 8 | 237.420 | 152.667 | 410.943 | 57.331 | 1.67× |
| by_rows | 1 | 408.769 | 398.782 | 835.993 | 61.085 | 0.97× |
| by_rows | 2 | 224.683 | 199.346 | 871.216 | 100.947 | 1.77× |
| by_rows | 4 | 251.944 | 185.681 | 406.967 | 41.157 | 1.57× |
| by_rows | 8 | 198.327 | 144.418 | 367.375 | 40.965 | 2.00× |
| by_cols | 1 | 410.676 | 399.556 | 750.760 | 51.924 | 0.97× |
| by_cols | 2 | 212.351 | 199.784 | 387.411 | 31.226 | 1.87× |
| by_cols | 4 | 277.652 | 184.641 | 433.786 | 72.877 | 1.43× |
| by_cols | 8 | 200.871 | 141.111 | 356.383 | 45.981 | 1.97× |
| tiles | 1 | 401.210 | 400.139 | 413.963 | 1.926 | 0.99× |
| tiles | 2 | 216.975 | 199.949 | 519.042 | 54.387 | 1.83× |
| tiles | 4 | 277.132 | 187.353 | 489.164 | 50.669 | 1.43× |
| tiles | 8 | 191.309 | 140.358 | 328.814 | 42.359 | 2.07× |

### Изображение 1024×1024

| Стратегия | Потоки | Mean (мс) | Min (мс) | Max (мс) | StdDev (мс) | Speedup |
| ---------- | ------ | --------- | -------- | -------- | ----------- | ------- |
| sequential | 1 | 1580.865 | 1573.878 | 1743.640 | 23.432 | 1.00× |
| per_pixel | 1 | 1617.078 | 1609.452 | 1632.257 | 4.178 | 0.98× |
| per_pixel | 2 | 832.426 | 806.513 | 1314.812 | 86.645 | 1.90× |
| per_pixel | 4 | 785.752 | 628.071 | 1439.441 | 152.227 | 2.01× |
| per_pixel | 8 | 639.336 | 534.342 | 1106.961 | 105.842 | 2.47× |
| by_rows | 1 | 1601.459 | 1598.370 | 1607.782 | 1.707 | 0.99× |
| by_rows | 2 | 836.852 | 799.117 | 1298.846 | 101.671 | 1.89× |
| by_rows | 4 | 831.702 | 628.030 | 1560.148 | 165.335 | 1.90× |
| by_rows | 8 | 581.172 | 533.927 | 674.505 | 36.856 | 2.72× |
| by_cols | 1 | 1615.292 | 1607.833 | 1630.588 | 5.699 | 0.98× |
| by_cols | 2 | 812.461 | 808.365 | 821.976 | 3.073 | 1.95× |
| by_cols | 4 | 754.739 | 631.750 | 853.555 | 68.012 | 2.09× |
| by_cols | 8 | 601.183 | 552.373 | 722.492 | 42.842 | 2.63× |
| tiles | 1 | 1604.246 | 1602.227 | 1608.627 | 1.414 | 0.99× |
| tiles | 2 | 803.133 | 800.838 | 817.890 | 3.004 | 1.97× |
| tiles | 4 | 756.144 | 620.790 | 838.037 | 57.856 | 2.09× |
| tiles | 8 | 574.597 | 533.354 | 756.702 | 38.014 | 2.75× |

### Изображение 512×512 время выполнения запуска 1

![время 512 1](results/time_plot_512_1.png)

### Изображение 512×512 время выполнения запуска 2

![время 512 2](results/time_plot_512_2.png)

### Изображение 1024×1024 время выполнения

![время 1024](results/time_plot_1024.png)

### Изображение 512×512 ускорение запуска 1

![ускорение 512 1](results/speedup_plot_512_1.png)

### Изображение 512×512 ускорение запуска 2

![ускорение 512 2](results/speedup_plot_512_2.png)

### Изображение 1024×1024 ускорение

![ускорение 1024](results/speedup_plot_1024.png)

## Анализ результатов

### Масштабируемость

- При одном потоке любая параллельная стратегия работает на 1–3% медленнее последовательной из‑за накладных расходов на создание команды потоков и синхронизацию.
- Переход на 2 потока даёт практически двукратное ускорение (до 1.97×), что подтверждает хорошую независимость вычислений.
- При 4 потоках на изображениях 512×512 наблюдается снижение производительности (stddev растёт, среднее время выше, чем на 2 потоках)
**Возможные причины**:
- **Конкуренция за кэш**: 4 ядра делят общий L2 (1.5 MiB) и L3 (4 MiB),
что увеличивает промахи при работе с изображением 512×512×3 ≈ 768 КБ.
- **Планировщик ОС**: на пользовательской системе фоновые процессы могут
«перехватывать» ядра, увеличивая дисперсию.
- **Накладные расходы OpenMP**: при малом объёме работы на поток (512×512 / 4)
синхронизация начинает доминировать над вычислениями.
- Для 1024×1024 масштабирование лучше: 8 потоков дают ускорение до 2.75× (tiles).

### Сравнение стратегий

-**by_rows** и **by_cols** дают схожие результаты на 512×512. На 1024×1024
при 4 потоках by_cols неожиданно быстрее by_rows (754 vs 831 ms) —
вероятно, особенность планировщика OMP на данном CPU при этом размере задачи.

- **tiles** стабильно лучшая стратегия на 8 потоках: блочное разбиение улучшает локальность, уменьшает промахи кэша и обеспечивает равномерную загрузку ядер.
- **per_pixel** показывает наибольший разброс. Причина — вычисление координат через целочисленное деление (`idx / width`, `idx % width`) на каждый пиксель добавляет накладные расходы, которые заметны при лёгком вычислении на пиксель.
85 changes: 85 additions & 0 deletions benchmarks/bench_main.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
#include "benchmark.h"
#include "filters.h"
#include <math.h>
#include <omp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static Image *create_synthetic_image(size_t w, size_t h, int ch) {
Image *img = malloc(sizeof(Image));
if (!img) return NULL;
img->width = w;
img->height = h;
img->channels = ch;
size_t total = w * h * ch;
img->pixels = malloc(total);
if (!img->pixels) {
free(img);
return NULL;
}

for (size_t i = 0; i < total; ++i) {
img->pixels[i] = (uint8_t)((i * 17 + (i / w) * 31) % 256);
}
return img;
}

static void free_image(Image *img) {
if (img) {
free(img->pixels);
free(img);
}
}

int main(void) {
bench_print_header();
const size_t IMG_SIZE = 512;
const char *FILTER_NAME = "gaussian5";
const char *strat_names[] = {"sequential", "per_pixel", "by_rows", "by_cols", "tiles"};
const ParallelStrategy strats[] = {STRATEGY_SEQUENTIAL, STRATEGY_PER_PIXEL, STRATEGY_BY_ROWS,
STRATEGY_BY_COLS, STRATEGY_TILES};
const int threads[] = {1, 2, 4, 8};

fprintf(stderr, "[bench] %zux%zu filter=%s\n", IMG_SIZE, IMG_SIZE, FILTER_NAME);

struct Filter f = {0};
int found = 0;
for (size_t k = 0; k < filter_count; ++k) {
if (strcmp(filter_list[k].name, FILTER_NAME) == 0) {
f = filter_list[k].create();
found = 1;
break;
}
}
if (!found) {
fprintf(stderr, "Filter '%s' not found\n", FILTER_NAME);
return 1;
}

Image *src = create_synthetic_image(IMG_SIZE, IMG_SIZE, 3);
Image *dst = create_synthetic_image(IMG_SIZE, IMG_SIZE, 3);
if (!src || !dst) {
fprintf(stderr, "OOM\n");
return 1;
}

omp_set_num_threads(1);
BenchStats baseline_stats = bench_run_conv(src, dst, &f, STRATEGY_SEQUENTIAL, 50);
bench_print_csv("sequential", 1, IMG_SIZE, IMG_SIZE, FILTER_NAME, &baseline_stats, 0.0);

for (size_t st = 1; st < 5; ++st) {
for (size_t ti = 0; ti < 4; ++ti) {
int t = threads[ti];
omp_set_num_threads(t);
BenchStats stats = bench_run_conv(src, dst, &f, strats[st], 50);
bench_print_csv(strat_names[st], t, IMG_SIZE, IMG_SIZE, FILTER_NAME, &stats,
baseline_stats.mean);
}
}
free_image(src);
free_image(dst);

fprintf(stderr, "[bench] Done\n");
return 0;
}
Loading
Loading