diff --git a/experiments/test_multithreading_speedup.sh b/experiments/test_multithreading_speedup.sh new file mode 100755 index 0000000..3521fac --- /dev/null +++ b/experiments/test_multithreading_speedup.sh @@ -0,0 +1,76 @@ +#!/bin/bash +# +# test_multithreading_speedup.sh +# +# Experiment script to verify that multithreading actually utilizes multiple CPU cores +# and provides performance benefits. +# +# Usage: ./test_multithreading_speedup.sh [build_dir] [config_file] +# +# This script: +# 1. Runs the benchmark in single-threaded mode +# 2. Runs the benchmark in multi-threaded mode with different thread counts +# 3. Compares the results and calculates speedup +# + +set -e + +# Default paths +BUILD_DIR="${1:-../build}" +CONFIG_FILE="${2:-../configs/default.json}" +EXECUTABLE="${BUILD_DIR}/NNets" + +if [ ! -f "$EXECUTABLE" ]; then + echo "Error: Executable not found at $EXECUTABLE" + echo "Please build the project first: cd build && cmake .. && make" + exit 1 +fi + +if [ ! -f "$CONFIG_FILE" ]; then + echo "Error: Config file not found at $CONFIG_FILE" + exit 1 +fi + +echo "==============================================" +echo "Multithreading Speedup Experiment" +echo "==============================================" +echo "Executable: $EXECUTABLE" +echo "Config: $CONFIG_FILE" +echo "Available CPU cores: $(nproc)" +echo "" + +# Function to extract training time from benchmark output +extract_training_time() { + grep "Training time:" | sed 's/.*Training time: \([0-9]*\) ms/\1/' +} + +# Function to extract thread count from benchmark output +extract_thread_count() { + grep "Threads:" | sed 's/.*Threads: \([0-9]*\).*/\1/' +} + +echo "Running single-threaded benchmark..." +SINGLE_OUTPUT=$("$EXECUTABLE" -c "$CONFIG_FILE" -b --single-thread 2>&1) +SINGLE_TIME=$(echo "$SINGLE_OUTPUT" | extract_training_time) +echo " Single-threaded training time: ${SINGLE_TIME} ms" +echo "" + +echo "Running multi-threaded benchmarks..." + +for THREADS in 2 4 $(nproc); do + MULTI_OUTPUT=$("$EXECUTABLE" -c "$CONFIG_FILE" -b -j "$THREADS" 2>&1) + MULTI_TIME=$(echo "$MULTI_OUTPUT" | extract_training_time) + ACTUAL_THREADS=$(echo "$MULTI_OUTPUT" | extract_thread_count) + + if [ -n "$MULTI_TIME" ] && [ "$MULTI_TIME" -gt 0 ]; then + SPEEDUP=$(echo "scale=2; $SINGLE_TIME / $MULTI_TIME" | bc) + echo " ${ACTUAL_THREADS} threads: ${MULTI_TIME} ms (speedup: ${SPEEDUP}x)" + else + echo " ${THREADS} threads: Failed to get timing" + fi +done + +echo "" +echo "==============================================" +echo "Experiment completed" +echo "==============================================" diff --git a/include/neuron_generation.h b/include/neuron_generation.h index 64a4625..09ce2ab 100644 --- a/include/neuron_generation.h +++ b/include/neuron_generation.h @@ -696,9 +696,11 @@ void SearchThreadFunc( * между несколькими потоками. Каждый поток исследует * случайные комбинации параметров нейронов независимо. * - * Ключевая идея: общее количество итераций увеличивается - * пропорционально количеству потоков, что обеспечивает - * эквивалентное качество поиска при большей скорости. + * Стратегия распределения работы: + * - Общее количество итераций делится между потоками + * - Это обеспечивает приблизительно такое же качество поиска + * как однопоточная версия, но за меньшее время + * - Каждый поток имеет минимальное гарантированное количество итераций * * @return минимальная достигнутая ошибка, или big если не найдено */ @@ -708,33 +710,30 @@ float rndrod4_parallel() { return rndrod4(); } - // Общее количество итераций для параллельного поиска - // Каждый поток получает count_max / NumThreads итераций - // Но общее количество итераций (сумма по всем потокам) равно count_max + // Общее количество итераций для однопоточного поиска int count_max = Neirons * Receptors * 4; - // Для задач с малым и средним количеством нейронов параллелизация - // может привести к нестабильному качеству поиска из-за: - // 1. Независимости траекторий поиска между потоками - // 2. Меньшего количества итераций на поток при делении workload - // 3. Разных точек сходимости случайного поиска - // - // Используем однопоточную версию для обеспечения стабильного качества - // обучения на небольших и средних задачах (< 500 нейронов). - // Параллельный режим применяется только для крупных сетей, где - // объём вычислений достаточен для надёжного параллельного поиска. - // - // Порог: count_max = Neirons * Receptors * 4 - // При Neirons = 500, Receptors = 20: count_max = 40000 - // Используем порог 500000 чтобы гарантировать Neirons >= 6250 при Receptors=20 - if (count_max < 500000) { + // Минимальное количество итераций на поток для качественного поиска. + // При слишком малом количестве итераций случайный поиск может + // не найти хорошее решение из-за недостаточного исследования + // пространства параметров. Значение 1000 обеспечивает достаточное + // покрытие пространства поиска на каждом потоке. + const int MIN_ITERATIONS_PER_THREAD = 1000; + + // Вычисляем количество итераций на поток + // Общая работа делится между потоками, но каждый поток получает + // минимум MIN_ITERATIONS_PER_THREAD итераций + int base_iterations_per_thread = (count_max + NumThreads - 1) / NumThreads; + int iterations_per_thread = std::max(base_iterations_per_thread, MIN_ITERATIONS_PER_THREAD); + + // Используем однопоточную версию если count_max слишком мал, + // чтобы параллелизация имела смысл. Порог: нужно достаточно + // итераций чтобы каждый поток мог эффективно работать. + // При count_max < 2000 накладные расходы на потоки превышают выгоду. + if (count_max < 2000) { return rndrod4(); } - // Каждый поток выполняет часть итераций - // Для крупных сетей это обеспечивает значительное ускорение - int iterations_per_thread = (count_max + NumThreads - 1) / NumThreads; - // Создаём результаты для каждого потока vector results(NumThreads);