Skip to content

Commit

Permalink
Merge pull request #592 from timmah/feature-add-atomics
Browse files Browse the repository at this point in the history
Feature add atomics
  • Loading branch information
KineticTheory authored Apr 11, 2019
2 parents fbfef0d + 9133c58 commit b279b1a
Show file tree
Hide file tree
Showing 3 changed files with 334 additions and 5 deletions.
10 changes: 5 additions & 5 deletions src/ds++/DracoStrings.hh
Original file line number Diff line number Diff line change
Expand Up @@ -24,21 +24,21 @@ namespace rtt_dsxx {
/*!
* \brief Convert a string to all lower case
*
* \param[in] string_in This string will be converted letter by letter to
* \param[in] string_in This string will be converted letter by letter to
* lowercase.
* \return A string that contains no uppercase letters.
*
* There are many complexities not considered here (e.g.: non-ASCI character
* sets) and many third party libraries like Boost provide a more complete
* solution.
* There are many complexities not considered here (e.g.: non-ASCI character
* sets) and many third party libraries like Boost provide a more complete
* solution.
*/
std::string string_tolower(std::string const &string_in);

//----------------------------------------------------------------------------//
/*!
* \brief Convert a string to all upper case
*
* \param[in] string_in This string will be converted letter by letter to
* \param[in] string_in This string will be converted letter by letter to
* uppercase.
* \return A string that contains no lowercase letters.
*
Expand Down
75 changes: 75 additions & 0 deletions src/ds++/atomics.hh
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
//---------------------------------------------------------------------------//
254 changes: 254 additions & 0 deletions src/ds++/test/tstatomics.cc
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
//---------------------------------------------------------------------------//

0 comments on commit b279b1a

Please sign in to comment.