diff --git a/.github/workflows/sanitizers.yml b/.github/workflows/sanitizers.yml index acaafa87..200abd67 100644 --- a/.github/workflows/sanitizers.yml +++ b/.github/workflows/sanitizers.yml @@ -46,7 +46,4 @@ jobs: run: cmake --build . --parallel - name: run-unit-tests - run: ctest --output-on-failure --schedule-random - - - name: run-integration-tests - run: ./integration_tests + run: ./tsan_tests diff --git a/.tsan-supressions b/.tsan-supressions index 4e6dadfe..1a873fc1 100644 --- a/.tsan-supressions +++ b/.tsan-supressions @@ -8,4 +8,7 @@ race:^tbb::detail::d1::auto_partition_type::is_divisible race:tbb::detail::d1::small_object_allocator::new_object race:tbb::detail::d1::small_object_allocator::delete_object -race:tbb::detail::d1::dynamic_grainsize_mode<*>::check_being_stolen \ No newline at end of file +race:tbb::detail::d1::dynamic_grainsize_mode<*>::check_being_stolen + +race:tbb::detail::d1::start_for<*>::finalize +race:tbb::detail::d1::start_for<*>::offer_work_impl diff --git a/CMakeLists.txt b/CMakeLists.txt index 758f993e..dabcb6e7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -185,6 +185,7 @@ if(GAPP_BUILD_TESTS AND BUILD_TESTING AND PROJECT_IS_TOP_LEVEL) add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/test/unit") add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/test/integration") add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/test/misc") + add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/test/tsan") endif() if(GAPP_BUILD_BENCHMARKS AND BUILD_TESTING AND PROJECT_IS_TOP_LEVEL) diff --git a/src/algorithm/nd_sort.cpp b/src/algorithm/nd_sort.cpp index 3e61677c..42f29411 100644 --- a/src/algorithm/nd_sort.cpp +++ b/src/algorithm/nd_sort.cpp @@ -188,8 +188,10 @@ namespace gapp::algorithm::dtl const size_t popsize = std::distance(first, last); DominanceMatrix dmat(popsize, popsize /*, MAXIMAL */); + GAPP_BARRIER(); std::for_each(GAPP_EXEC, detail::iota_iterator(0_sz), detail::iota_iterator(first->size()), [&](size_t obj) { + GAPP_BARRIER(); FitnessVector fvec(popsize); std::transform(first, last, fvec.begin(), detail::element_at(obj)); @@ -210,10 +212,13 @@ namespace gapp::algorithm::dtl } }); }); + GAPP_BARRIER(); }); + GAPP_BARRIER(); std::for_each(GAPP_EXEC, detail::iota_iterator(0_sz), detail::iota_iterator(popsize), [&](size_t row) noexcept { + GAPP_BARRIER(); dmat(row, row).store(NONMAXIMAL, std::memory_order_relaxed); // diagonal is all nonmax for (size_t col = row + 1; col < popsize; col++) @@ -224,7 +229,9 @@ namespace gapp::algorithm::dtl dmat(col, row).store(NONMAXIMAL, std::memory_order_relaxed); } } + GAPP_BARRIER(); }); + GAPP_BARRIER(); return dmat; } diff --git a/src/algorithm/nsga3.cpp b/src/algorithm/nsga3.cpp index ed1b6f55..510d02c9 100644 --- a/src/algorithm/nsga3.cpp +++ b/src/algorithm/nsga3.cpp @@ -239,8 +239,10 @@ namespace gapp::algorithm sol_info_.resize(last - first); + GAPP_BARRIER(); std::for_each(GAPP_EXEC, pfirst, plast, [&](const FrontInfo& sol) { + GAPP_BARRIER(); const FitnessVector fnorm = normalizeFitnessVec(first[sol.idx], ideal_point_, nadir_point_); auto idistance = [&](const auto& line) { return std::inner_product(fnorm.begin(), fnorm.end(), line.begin(), 0.0); }; @@ -249,7 +251,9 @@ namespace gapp::algorithm sol_info_[sol.idx].ref_idx = std::distance(ref_lines_.begin(), closest); sol_info_[sol.idx].ref_dist = math::perpendicularDistanceSq(*closest, fnorm); + GAPP_BARRIER(); }); + GAPP_BARRIER(); } inline bool NSGA3::Impl::nichedCompare(size_t lhs, size_t rhs) const noexcept diff --git a/src/algorithm/reference_lines.cpp b/src/algorithm/reference_lines.cpp index f09c556c..43f2ef93 100644 --- a/src/algorithm/reference_lines.cpp +++ b/src/algorithm/reference_lines.cpp @@ -183,12 +183,11 @@ namespace gapp::algorithm::reflines min_distances.pop_back(); /* Calc the distance of each candidate to the closest ref point. */ - std::transform(GAPP_EXEC, candidate_points.begin(), candidate_points.end(), min_distances.begin(), min_distances.begin(), - [&](const Point& candidate, double current_min) noexcept + for (size_t i = 0; i < candidate_points.size(); i++) { - const double dist = math::euclideanDistanceSq(candidate, points.back()); - return std::min(current_min, dist); - }); + double dist = math::euclideanDistanceSq(candidate_points[i], points.back()); + min_distances[i] = std::min(min_distances[i], dist); + } } return points; diff --git a/src/core/ga_base.impl.hpp b/src/core/ga_base.impl.hpp index b3eff970..4aefd1cd 100644 --- a/src/core/ga_base.impl.hpp +++ b/src/core/ga_base.impl.hpp @@ -327,7 +327,7 @@ namespace gapp /* Reset state in case solve() has already been called before. */ generation_cntr_ = 0; - num_fitness_evals_ = 0; + num_fitness_evals_->store(0, std::memory_order_release); solutions_.clear(); population_.clear(); @@ -339,7 +339,9 @@ namespace gapp /* Create and evaluate the initial population of the algorithm. */ num_objectives_ = findNumberOfObjectives(); population_ = generatePopulation(population_size_, std::move(initial_population)); - std::for_each(GAPP_EXEC, population_.begin(), population_.end(), [this](Candidate& sol) { evaluate(sol); }); + GAPP_BARRIER(); + std::for_each(GAPP_EXEC, population_.begin(), population_.end(), [this](Candidate& sol) { GAPP_BARRIER(); evaluate(sol); GAPP_BARRIER(); }); + GAPP_BARRIER(); fitness_matrix_ = detail::toFitnessMatrix(population_); if (keep_all_optimal_sols_) solutions_ = detail::findParetoFront(population_); @@ -465,11 +467,10 @@ namespace gapp * is no point doing it again. */ if (!sol.is_evaluated || fitness_function_->dynamic()) { + num_fitness_evals_->fetch_add(1, std::memory_order_acq_rel); + sol.fitness = (*fitness_function_)(sol.chromosome); sol.is_evaluated = true; - - std::atomic_ref num_evals{ num_fitness_evals_ }; - num_evals.fetch_add(1_sz, std::memory_order_acq_rel); } GAPP_ASSERT(hasValidFitness(sol)); @@ -504,27 +505,35 @@ namespace gapp std::vector> child_pairs(num_children / 2); prepareSelections(); + GAPP_BARRIER(); std::generate(GAPP_EXEC, child_pairs.begin(), child_pairs.end(), [this] { + GAPP_BARRIER(); const auto& parent1 = select(); const auto& parent2 = select(); - - return crossover(parent1, parent2); + auto children = crossover(parent1, parent2); + GAPP_BARRIER(); + return std::move(children); }); + GAPP_BARRIER(); auto children = detail::flatten(std::move(child_pairs)); /* If the population size is odd, one too many child candidates were generated by the crossovers. */ if (children.size() > population_size_) children.pop_back(); + GAPP_BARRIER(); std::for_each(GAPP_EXEC, children.begin(), children.end(), [this](Candidate& child) { + GAPP_BARRIER(); mutate(child); repair(child); evaluate(child); + GAPP_BARRIER(); }); + GAPP_BARRIER(); updatePopulation(std::move(children)); if (keep_all_optimal_sols_) updateOptimalSolutions(solutions_, population_); diff --git a/src/core/ga_info.cpp b/src/core/ga_info.cpp index 0f40a3e9..8bf57252 100644 --- a/src/core/ga_info.cpp +++ b/src/core/ga_info.cpp @@ -4,7 +4,6 @@ #include "../algorithm/single_objective.hpp" #include "../stop_condition/stop_condition.hpp" #include "../utility/utility.hpp" -#include #include #include @@ -13,7 +12,7 @@ namespace gapp GaInfo::GaInfo(GaInfo&&) noexcept = default; GaInfo& GaInfo::operator=(GaInfo&&) noexcept = default; - GaInfo::~GaInfo() = default; + GaInfo::~GaInfo() noexcept = default; GaInfo::GaInfo(Positive population_size, std::unique_ptr algorithm, std::unique_ptr stop_condition) noexcept : @@ -26,8 +25,7 @@ namespace gapp size_t GaInfo::num_fitness_evals() const noexcept { - std::atomic_ref num_fitness_evals{ num_fitness_evals_ }; - return num_fitness_evals.load(std::memory_order_acquire); + return num_fitness_evals_->load(std::memory_order_acquire); } void GaInfo::algorithm(std::unique_ptr f) diff --git a/src/core/ga_info.hpp b/src/core/ga_info.hpp index 6d059f2b..8708c1e5 100644 --- a/src/core/ga_info.hpp +++ b/src/core/ga_info.hpp @@ -5,6 +5,7 @@ #include "../population/population.hpp" #include "../utility/bounded_value.hpp" +#include "../utility/atomic.hpp" #include "../utility/utility.hpp" #include "../metrics/metric_set.hpp" #include @@ -341,7 +342,7 @@ namespace gapp GaInfo& operator=(const GaInfo&) = delete; /** Destructor. */ - virtual ~GaInfo(); + virtual ~GaInfo() noexcept; protected: @@ -359,7 +360,7 @@ namespace gapp Positive max_gen_ = 500; size_t num_objectives_ = 0; size_t generation_cntr_ = 0; - size_t num_fitness_evals_ = 0; + detail::atomic num_fitness_evals_ = 0; bool keep_all_optimal_sols_ = false; bool use_default_algorithm_ = false; diff --git a/src/metrics/pop_stats.cpp b/src/metrics/pop_stats.cpp index e96ea1e1..ddd46e1f 100644 --- a/src/metrics/pop_stats.cpp +++ b/src/metrics/pop_stats.cpp @@ -154,15 +154,19 @@ namespace gapp::detail const FitnessMatrix front = uniqueSortedParetoFront(fmat); std::atomic hypervolume = 0.0; + GAPP_BARRIER(); std::for_each(GAPP_EXEC, detail::iota_iterator(0_sz), detail::iota_iterator(front.size()), [&](size_t idx) { + GAPP_BARRIER(); const auto point = front[idx]; const FitnessMatrix rest = { front.begin() + idx + 1, front.end() }; const double exclusive_hypervolume = exclusiveHypervolume(point, rest, ref_point); hypervolume.fetch_add(exclusive_hypervolume, std::memory_order_acq_rel); + GAPP_BARRIER(); }); + GAPP_BARRIER(); return hypervolume.load(std::memory_order_acquire); } diff --git a/src/population/population.hpp b/src/population/population.hpp index 1f6cd533..afc2a6b9 100644 --- a/src/population/population.hpp +++ b/src/population/population.hpp @@ -106,8 +106,10 @@ namespace gapp::detail std::vector lhs_state(lhs.size()); std::vector> rhs_state(rhs.size()); + GAPP_BARRIER(); std::for_each(GAPP_EXEC, iota_iterator(0_sz), iota_iterator(lhs.size()), [&](size_t i) noexcept { + GAPP_BARRIER(); for (size_t j = 0; j < rhs.size(); j++) { const Dominance rhs_state_j = rhs_state[j].load(std::memory_order_acquire); @@ -145,7 +147,9 @@ namespace gapp::detail } // comp == 0 --> both are OPTIMAL or DOMINATED, can't know } + GAPP_BARRIER(); }); + GAPP_BARRIER(); Candidates optimal_solutions; optimal_solutions.reserve(lhs.size() + rhs.size()); diff --git a/src/utility/atomic.hpp b/src/utility/atomic.hpp new file mode 100644 index 00000000..b3881d9d --- /dev/null +++ b/src/utility/atomic.hpp @@ -0,0 +1,64 @@ +/* Copyright (c) 2023 Krisztián Rugási. Subject to the MIT License. */ + +#ifndef GA_UTILITY_ATOMIC_HPP +#define GA_UTILITY_ATOMIC_HPP + +#include +#include +#include + +namespace gapp::detail +{ + /* + * This is a simple wrapper class around std::atomic with atomic + * initialization, in order to prevent data races between the + * initialization of the variable and later accesses to it. + * + * The wrapper also adds move operators for convenience. + */ + template + class atomic + { + public: + atomic(std::memory_order order = std::memory_order_seq_cst) noexcept + { + data_.store(T{}, order); + } + + atomic(T value, std::memory_order order = std::memory_order_seq_cst) noexcept + { + data_.store(std::move(value), order); + } + + atomic(atomic&& other) noexcept + { + data_.store(other->load()); + } + + atomic& operator=(T value) noexcept + { + data_.store(std::move(value)); + return *this; + } + + atomic& operator=(atomic&& other) noexcept + { + data_.store(other->load()); + return *this; + } + + ~atomic() noexcept = default; // maybe needs release? + + std::atomic& operator*() noexcept { return data_; } + std::atomic* operator->() noexcept { return std::addressof(data_); } + const std::atomic& operator*() const noexcept { return data_; } + const std::atomic* operator->() const noexcept { return std::addressof(data_); } + + private: + std::atomic data_; + }; + + +} // namespace gapp::detail + +#endif // !GA_UTILITY_ATOMIC_HPP diff --git a/src/utility/utility.hpp b/src/utility/utility.hpp index 252b86ba..ca8aa1fe 100644 --- a/src/utility/utility.hpp +++ b/src/utility/utility.hpp @@ -111,6 +111,11 @@ #endif +#include +inline std::atomic_bool global_barrier; +#define GAPP_BARRIER() std::ignore = global_barrier.exchange(true, std::memory_order_seq_cst) + + namespace gapp { constexpr std::size_t operator ""_sz(unsigned long long arg) noexcept diff --git a/test/tsan/CMakeLists.txt b/test/tsan/CMakeLists.txt new file mode 100644 index 00000000..f20746d8 --- /dev/null +++ b/test/tsan/CMakeLists.txt @@ -0,0 +1,15 @@ +find_package(Catch2 3 REQUIRED) + +if(MSVC) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -wd4388") +endif() + +file(GLOB_RECURSE TEST_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/*.cpp") +file(GLOB_RECURSE TEST_HEADERS CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/*.hpp") + +add_executable(tsan_tests ${TEST_SOURCES} ${TEST_HEADERS}) + +target_link_libraries(tsan_tests PRIVATE Catch2::Catch2WithMain gapp) + +include(Catch) +catch_discover_tests(tsan_tests) diff --git a/test/tsan/tsan.cpp b/test/tsan/tsan.cpp new file mode 100644 index 00000000..0783ac52 --- /dev/null +++ b/test/tsan/tsan.cpp @@ -0,0 +1,35 @@ +#include +#include +#include + +std::vector numbers = { 0, 1, 2, 3 }; +std::mutex mtx; + +TEST_CASE("empty", "[tsan]") +{ + std::for_each(std::execution::par, numbers.begin(), numbers.end(), [](const auto&) {}); +} + +TEST_CASE("simple", "[tsan]") +{ + { std::scoped_lock _{ mtx }; } + std::for_each(std::execution::par, numbers.begin(), numbers.end(), [](auto& n) { + std::scoped_lock _{ mtx }; ++n; + }); +} + +TEST_CASE("race1", "[tsan]") +{ + std::vector numbers2{ 0, 1, 2, 3 }; + std::for_each(std::execution::par, numbers2.begin(), numbers2.end(), [](auto& n) { + ++n; + }); +} + +//TEST_CASE("race2", "[tsan]") +//{ +// std::vector numbers2{ 0, 1, 2, 3 }; +// std::for_each(std::execution::par, numbers2.begin(), numbers2.end(), [](auto& n) { +// numbers[0] += n; +// }); +//}