Skip to content

P2689: Bounded atomic_ref and atomic_accessor LEWG Presentation 04/30/2024 #456

Description

@crtrott

Atomic Refs Bound to Memory Orderings & Atomic Accessors

The mdspan paper P0009 listed atomic accessors as a reason for having accessors in the first place.

One of the use cases is for parallel algorithms updating data in a way which has data races.

Consider the histogram computation:

template<class ExecT>
void compute_histogram(ExecT exec, float bin_size,
               std::mdspan<int, std::dextents<size_t,1>> output,
               std::mdspan<float, std::dextents<size_t,1>> data) {
  static_assert(std::is_execution_policy_v<ExecT>);

  std::for_each(exec, data.data_handle(), data.data_handle()+data.extent(0), [=](float val) {
    int bin = std::abs(val)/bin_size;
    bin = std::clamp(bin, size_t(0), output.extent(0));
    output[bin]++;
  });

Depending on whether ExecT is sequenced_policy or not, the update needs to happen atomically.

With just atomic_ref one could do:

  std::for_each(exec, data.data_handle(), data.data_handle()+data.extent(0), [=](float val) {
    int bin = std::abs(val)/bin_size;
    bin = std::clamp(bin, size_t(0), output.extent(0));
    atomic_ref(output[bin])++;
  });

This paper proposes a way for doing that for general mdspans without writing out atomic_ref everywhere inside a complex algorithm and also fixes the fact that you can't do the simple ++ with relaxed memory order - enough for these kind of accumulation cases.

What this paper proposes

  • "Bounded" versions of atomic_ref with a fixed (bounded) memory order
    • atomic_ref_relaxed, atomic_ref_seq_cst, atomic_ref_acq_rel
    • Conceptually just like atomic_ref with a fixed memory order - all the same members
  • Corresponding atomic accessors which use the various atomic_ref types as reference type
    • atomic_accessor => atomic_ref
    • atomic_accessor_relaxed => atomic_ref_relaxed
    • atomic_accessor_acq_rel => atomic_ref_acq_rel
    • atomic_accessor_seq_cst => atomic_ref_seq_cst

Histogram with atomic accessor

// sequenced_policy does not attach atomic accessor
template<class T, class Extents, class LayoutPolicy>
auto add_atomic_accessor_if_needed(
    std::execution::sequenced_policy, mdspan<T, Extents, LayoutPolicy> m) {
        return m;
 }

// parallel policies attach atomic accessor:
template<class ExecutionPolicy, class T, class Extents, class LayoutPolicy>
auto add_atomic_accessor_if_needed(
    ExecutionPolicy, mdspan<T, Extents, LayoutPolicy> m) {
        return mdspan(m.data_handle(), m.mapping(), atomic_accessor<T>());
}

template<class ExecT>
void compute_histogram(ExecT exec, float bin_size,
               std::mdspan<int, std::dextents<size_t,1>> output,
               std::mdspan<float, std::dextents<size_t,1>> data) {
  static_assert(std::is_execution_policy_v<ExecT>);

  auto accumulator = add_atomic_accessor_if_needed(exec, output);

  std::for_each(exec, data.data_handle(), data.data_handle()+data.extent(0), [=](float val) {
    int bin = std::abs(val)/bin_size;
    bin = std::clamp(bin, size_t(0), output.extent(0));
    // this is now atomic for parallel policies
    accumulator[bin]++;
  });

Previous Question: Why not expose the templated type?

  • There is really no use-case where one would want to write something generic over the memory order!
  • The algorithm determines the memory order you need - the only generic question we identified is whether you need atomics at all (i.e. what execution policy is used)
  • SG1 also felt that its easier to see the memory order with the explicit names
    • We don't feel there is any real risk here and mdspan somewhat hides its accessor anyway

Wording Considerations

  • Used exposition only atomic-ref-bound<T,MemoryOrder> for wording
    • atomic_ref_MEM_ORDER are template aliases - see open question whether this is ok
  • atomic_ref has 4 specializations: general, integral (not bool), floating point and pointer
    • atomic-ref-bound needs only 2 (general and arithmetic/pointer{not-bool} versions)
      • integral, floating point and pointer type versions can be defined in terms of atomic_ref with constraints on member functions
      • the only reason we need general is difference_type member typedef -> doesn't exist in atomic_ref general

Open Questions

  • Is reasoning for having atomic-ref-bound exposition only good enough, or does LEWG want it as a public thing?
    • Please Poll: atomic-ref-bound should be exposition only.
    • Author position: in favor
  • Converting between atomic-ref-bound and basic-atomic-accessors of different memory orders
    • We don't see many good reasons to have them:
      • atomic_ref is meant to be almost used in-place for individual operations e.g. atomic_ref(a[i])++
      • atomic accessors are something you are supposed to add in a fairly local scope
      • we don't expect folks to pass mdspan with atomic accessors around
    • There is a reason not to permit it:
      • Writing functions which take atomic_ref_MEM_ORDER imply ordering behavior: allowing conversions would change that behavior
    • Please Poll: atomic-ref-bound and basic-atomic-accessor should have converting constructors for different memory-orders
      • Author position: against

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions