-
Notifications
You must be signed in to change notification settings - Fork 44
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #592 from timmah/feature-add-atomics
Feature add atomics
- Loading branch information
Showing
3 changed files
with
334 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
//----------------------------------*-C++-*----------------------------------// | ||
/*! | ||
* \file ds++/atomics.hh | ||
* \author Tim Kelley | ||
* \date Thursday, Sept. 6, 2018, 10:50 am | ||
* \brief Header file for atomic functions on floatint-point (until C++20) | ||
* \note Copyright (C) 2018-2019 Triad National Security, LLC. | ||
* All rights reserved. */ | ||
//---------------------------------------------------------------------------// | ||
|
||
#ifndef __dsxx_Atomics_hh__ | ||
#define __dsxx_Atomics_hh__ | ||
|
||
#include <atomic> | ||
#include <type_traits> // std::is_floating_point_v | ||
|
||
namespace rtt_dsxx { | ||
|
||
//---------------------------------------------------------------------------// | ||
/**\brief atomically add arg to a. | ||
* \tparam FpT: a floating point type (integer types in std lib since C++11). | ||
* \param a: an atomic of type FpT that will be updated. | ||
* \param arg: quantity to add to a | ||
* \param m_o: std memory order, default is memory_order_relaxed | ||
* \return value of a after update | ||
* \remark: By default, uses memory_order_relaxed, meaning (I think) that other | ||
* atomic operations on 'a' can be moved before or after this one. | ||
*/ | ||
template <class FpT> | ||
FpT fetch_add(std::atomic<FpT> &a, FpT arg, | ||
std::memory_order m_o = std::memory_order_relaxed) { | ||
static_assert(std::is_floating_point<FpT>::value, | ||
"Template parameter ought to be floating point, use C++11 std " | ||
"for integral types"); | ||
FpT expected = a.load(); | ||
FpT to_store = expected + arg; | ||
while (!a.compare_exchange_weak(expected, to_store, m_o)) { | ||
expected = a.load(); | ||
to_store = arg + expected; | ||
} | ||
return to_store; | ||
} | ||
|
||
//---------------------------------------------------------------------------// | ||
/**\brief atomically subtract a from arg. | ||
* \tparam FpT: a floating point type (integer types in std lib since C++11). | ||
* \param a: an atomic of type FpT that will be updated. | ||
* \param arg: quantity to subtract from a | ||
* \param m_o: std memory order, default is memory_order_relaxed | ||
* \return value of a after update | ||
* \remark: By default, uses memory_order_relaxed, meaning (I think) that other | ||
* atomic operations on 'a' can be moved before or after this one. | ||
*/ | ||
template <class FpT> | ||
FpT fetch_sub(std::atomic<FpT> &a, FpT arg, | ||
std::memory_order m_o = std::memory_order_relaxed) { | ||
static_assert(std::is_floating_point<FpT>::value, | ||
"Template parameter ought to be floating point, use C++11 std " | ||
"for integral types"); | ||
FpT expected = a.load(); | ||
FpT to_store = expected - arg; | ||
while (!a.compare_exchange_weak(expected, to_store, m_o)) { | ||
expected = a.load(); | ||
to_store = expected - arg; | ||
} | ||
return to_store; | ||
} | ||
|
||
} // namespace rtt_dsxx | ||
|
||
#endif // __dsxx_Atomics_hh__ | ||
|
||
//---------------------------------------------------------------------------// | ||
// end of ds++/atomics.hh | ||
//---------------------------------------------------------------------------// |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,254 @@ | ||
//----------------------------------*-C++-*----------------------------------// | ||
/*! | ||
* \file ds++/test/tstatomics.cc | ||
* \author Tim Kelley | ||
* \date Thursday, Sept. 6, 2018, 10:51 am | ||
* \note Copyright (C) 2018-2019 Triad National Security, LLC. | ||
* All rights reserved. */ | ||
//---------------------------------------------------------------------------// | ||
|
||
#include "ds++/Release.hh" | ||
#include "ds++/ScalarUnitTest.hh" | ||
#include "ds++/Soft_Equivalence.hh" | ||
#include "ds++/atomics.hh" | ||
#include <functional> // std::function | ||
#include <thread> | ||
|
||
using rtt_dsxx::UnitTest; | ||
|
||
//----------------------------------------------------------------------------// | ||
/* Hammer an atomic from each thread. Each iteration, the thread adds | ||
* (tid * iteration) to the counter. The atomic ensures that everyone sees | ||
* a consistent view of the counter: no thread overwrites the contribution | ||
* from any other thread. | ||
*/ | ||
void thread_action(std::atomic<double> &d, size_t N, size_t tid) { | ||
double const did = static_cast<double>(tid); | ||
double d_i = 1; | ||
for (size_t i = 0; i < N; ++i) { | ||
double addend = did * d_i; | ||
rtt_dsxx::fetch_add(d, addend); | ||
d_i += 1.0; | ||
} | ||
return; | ||
} // thread_action | ||
|
||
//----------------------------------------------------------------------------// | ||
/* Test fetch_add using an atomic. Expect to get the correct sum every time.*/ | ||
void fetch_add_atomic_core(UnitTest &ut, size_t const n_threads, | ||
size_t const n_iterations) { | ||
std::atomic<double> a_d(0.0); | ||
|
||
// launch a number of threads | ||
std::vector<std::thread> threads(n_threads); | ||
size_t tid(0); | ||
for (auto &t : threads) { | ||
t = std::thread(thread_action, std::ref(a_d), n_iterations, tid); | ||
tid++; | ||
} | ||
// join | ||
for (auto &t : threads) { | ||
t.join(); | ||
} | ||
|
||
// Compute expected value the easy way | ||
double result = a_d.load(); | ||
double sum = 0.0; | ||
for (size_t i = 0; i < n_iterations; ++i) { | ||
sum += i + 1; | ||
} | ||
double tsum = 0.0; | ||
for (size_t t = 0; t < n_threads; ++t) { | ||
tsum += t; | ||
} | ||
double expected = sum * tsum; | ||
|
||
// check and report | ||
bool const passed = rtt_dsxx::soft_equiv(result, expected); | ||
if (!passed) { | ||
printf("%s:%i tsum = %.0f, isum = %.0f, result = %.0f\n", __FUNCTION__, | ||
__LINE__, tsum, sum, result); | ||
} | ||
FAIL_IF_NOT(passed); | ||
return; | ||
} // fetch_add_atomic_core | ||
|
||
void test_fetch_add_atomic(UnitTest &ut) { | ||
size_t const n_threads(19); | ||
size_t const n_iterations(10001); | ||
fetch_add_atomic_core(ut, n_threads, n_iterations); | ||
return; | ||
} // test_fetch_add_atomic | ||
|
||
void test_fetch_add_atomic_1e6(UnitTest &ut) { | ||
size_t const n_threads(19); | ||
size_t const n_iterations(1000001); | ||
fetch_add_atomic_core(ut, n_threads, n_iterations); | ||
return; | ||
} // test_fetch_add_atomic | ||
|
||
// --------------------- non-atomic version -------------------------- | ||
// This should give the wrong answer nearly every time on any respectable | ||
// thread implementation. | ||
|
||
//----------------------------------------------------------------------------// | ||
/* Similarly, hammer a POD from each thread. Each iteration, the thread adds | ||
* (tid * iteration) to the counter. Since the threads are contending, we expect | ||
* to have a race condition where two threads read the same value from d and | ||
* one of the thread's write (+=) overwrites the other's. | ||
*/ | ||
void thread_action_pod(double &d, size_t N, size_t tid) { | ||
double const did = static_cast<double>(tid); | ||
double d_i = 1; | ||
for (size_t i = 0; i < N; ++i) { | ||
double addend = did * d_i; | ||
d += addend; | ||
d_i += 1.0; | ||
} | ||
return; | ||
} // run_in_a_thread_d | ||
|
||
//----------------------------------------------------------------------------// | ||
// same as above, except does not use an atomic | ||
void test_fetch_add_not_atomic(UnitTest & /*ut*/) { | ||
size_t const n_threads(43); | ||
size_t const n_iterations(10001); | ||
double a_d(0.0); | ||
|
||
// launch a number of threads | ||
std::vector<std::thread> threads(n_threads); | ||
size_t tid(0); | ||
for (auto &t : threads) { | ||
t = std::thread(thread_action_pod, std::ref(a_d), n_iterations, tid); | ||
tid++; | ||
} | ||
// join | ||
for (auto &t : threads) { | ||
t.join(); | ||
} | ||
|
||
// calculate expected value | ||
double result = a_d; | ||
double sum = 0.0; | ||
for (size_t i = 0; i < n_iterations; ++i) { | ||
sum += i + 1; | ||
} | ||
double tsum = 0.0; | ||
for (size_t t = 0; t < n_threads; ++t) { | ||
tsum += t; | ||
} | ||
double expected = sum * tsum; | ||
// check and report | ||
bool const passed = !rtt_dsxx::soft_equiv(result, expected); | ||
if (!passed) { | ||
double diff = (expected - result); | ||
double rel_diff = 100 * diff / expected; | ||
printf("%s:%i Expected these to differ: tsum = %.0f, isum = %.0f, result = " | ||
"%.0f, diff = %.0f, rel. " | ||
"diff = %.2f %% \n", | ||
__FUNCTION__, __LINE__, tsum, sum, result, diff, rel_diff); | ||
} | ||
/* This does not fail on all platforms: on 4 April 2019 failed on appVeyor CI. | ||
* So, hmmm... */ | ||
// FAIL_IF_NOT(passed); | ||
return; | ||
} // test_fetch_add_not_atomic | ||
|
||
// fetch_sub tests | ||
|
||
/* Same as thread_action above, except uses fetch_sub. Total sum is just the | ||
* negative of the preceding test. | ||
*/ | ||
void thread_action_sub(std::atomic<double> &d, size_t N, size_t tid) { | ||
double const did = static_cast<double>(tid); | ||
double d_i = 1; | ||
for (size_t i = 0; i < N; ++i) { | ||
double addend = did * d_i; | ||
rtt_dsxx::fetch_sub(d, addend); | ||
d_i += 1.0; | ||
} | ||
return; | ||
} // thread_action | ||
|
||
//----------------------------------------------------------------------------// | ||
/* Test fetch_add using an atomic. Expect to get the correct sum every time.*/ | ||
void fetch_sub_atomic_core(UnitTest &ut, size_t const n_threads, | ||
size_t const n_iterations) { | ||
std::atomic<double> a_d(0.0); | ||
|
||
// launch a number of threads | ||
std::vector<std::thread> threads(n_threads); | ||
size_t tid(0); | ||
for (auto &t : threads) { | ||
t = std::thread(thread_action_sub, std::ref(a_d), n_iterations, tid); | ||
tid++; | ||
} | ||
// join | ||
for (auto &t : threads) { | ||
t.join(); | ||
} | ||
|
||
// Compute expected value the easy way | ||
double result = a_d.load(); | ||
double sum = 0.0; | ||
for (size_t i = 0; i < n_iterations; ++i) { | ||
sum += i + 1; | ||
} | ||
double tsum = 0.0; | ||
for (size_t t = 0; t < n_threads; ++t) { | ||
tsum += t; | ||
} | ||
double expected = -1.0 * sum * tsum; | ||
|
||
// check and report | ||
bool const passed = rtt_dsxx::soft_equiv(result, expected); | ||
if (!passed) { | ||
printf("%s:%i tsum = %.0f, isum = %.0f, result = %.0f, " | ||
"expected = %.0f\n", | ||
__FUNCTION__, __LINE__, tsum, sum, result, expected); | ||
} | ||
FAIL_IF_NOT(passed); | ||
return; | ||
} // fetch_add_atomic_core | ||
|
||
void test_fetch_sub_atomic(UnitTest &ut) { | ||
size_t const n_threads(19); | ||
size_t const n_iterations(10001); | ||
fetch_sub_atomic_core(ut, n_threads, n_iterations); | ||
return; | ||
} // test_fetch_add_atomic | ||
|
||
void test_fetch_sub_atomic_1e6(UnitTest &ut) { | ||
size_t const n_threads(19); | ||
size_t const n_iterations(1000001); | ||
fetch_sub_atomic_core(ut, n_threads, n_iterations); | ||
return; | ||
} // test_fetch_add_atomic | ||
|
||
using t_func = std::function<void(UnitTest &)>; | ||
|
||
//----------------------------------------------------------------------------// | ||
void run_a_test(UnitTest &u, t_func f, std::string const &msg) { | ||
f(u); | ||
if (u.numFails == 0) { | ||
u.passes(msg); | ||
} | ||
return; | ||
} | ||
|
||
//----------------------------------------------------------------------------// | ||
int main(int argc, char *argv[]) { | ||
rtt_dsxx::ScalarUnitTest ut(argc, argv, rtt_dsxx::release); | ||
try { | ||
run_a_test(ut, test_fetch_add_atomic, "fetch_add ok."); | ||
run_a_test(ut, test_fetch_add_atomic_1e6, "fetch_add ok 1e6 iterations."); | ||
run_a_test(ut, test_fetch_sub_atomic, "fetch_sub ok."); | ||
run_a_test(ut, test_fetch_sub_atomic_1e6, "fetch_sub ok 1e6 iterations."); | ||
run_a_test(ut, test_fetch_add_not_atomic, "non-atomic as expected."); | ||
} // try--catches in the epilog: | ||
UT_EPILOG(ut); | ||
} | ||
|
||
//---------------------------------------------------------------------------// | ||
// end of ds++/test/tstatomics.cc | ||
//---------------------------------------------------------------------------// |